Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VSCode: Add Scarb class and move Scarb's LS discovery there #4974

Merged
merged 1 commit into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 19 additions & 52 deletions vscode-cairo/src/cairols.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as child_process from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
Expand All @@ -7,6 +6,7 @@ import { SemanticTokensFeature } from "vscode-languageclient/lib/common/semantic

import * as lc from "vscode-languageclient/node";
import { Context } from "./context";
import { Scarb } from "./scarb";

// Tries to find the development version of the language server executable,
// assuming the workspace directory is inside the Cairo repository.
Expand Down Expand Up @@ -149,52 +149,14 @@ function notifyScarbMissing(ctx: Context) {
ctx.log.error(errorMessage);
}

async function listScarbCommandsOutput(
scarbPath: undefined | string,
ctx: Context,
) {
if (!scarbPath) {
return undefined;
}
const child = child_process.spawn(scarbPath, ["--json", "commands"], {
stdio: "pipe",
cwd: rootPath(ctx),
});
let stdout = "";
for await (const chunk of child.stdout) {
stdout += chunk;
}
return stdout;
}

async function isScarbLsPresent(
scarbPath: undefined | string,
ctx: Context,
): Promise<boolean> {
if (!scarbPath) {
return false;
}
const scarbOutput = await listScarbCommandsOutput(scarbPath, ctx);
if (!scarbOutput) return false;
return scarbOutput
.split("\n")
.map((v) => v.trim())
.filter((v) => !!v)
.map((v) => JSON.parse(v))
.some(
(commands: Record<string, unknown>) =>
!!commands["cairo-language-server"],
);
}

enum ServerType {
Standalone,
Scarb,
}

async function getServerType(
isScarbEnabled: boolean,
scarbPath: string | undefined,
scarb: Scarb | undefined,
configLanguageServerPath: string | undefined,
ctx: Context,
) {
Expand All @@ -203,7 +165,7 @@ async function getServerType(
// If Scarb manifest is missing, and Cairo-LS path is explicit.
return ServerType.Standalone;
}
if (await isScarbLsPresent(scarbPath, ctx)) return ServerType.Scarb;
if (await scarb?.hasCairoLS(ctx)) return ServerType.Scarb;
return ServerType.Standalone;
}

Expand Down Expand Up @@ -317,20 +279,33 @@ async function getServerOptions(ctx: Context): Promise<lc.ServerOptions> {
ctx.log.debug(`using Scarb: ${scarbPath}`);
}

let scarb: Scarb | undefined;
if (isScarbEnabled && scarbPath != undefined) {
scarb = new Scarb(scarbPath, vscode.workspace.workspaceFolders?.[0]);
}

const serverType = await getServerType(
isScarbEnabled,
scarbPath,
scarb,
configLanguageServerPath,
ctx,
);

let serverExecutable: lc.Executable | undefined;
if (serverType === ServerType.Scarb) {
serverExecutable = { command: scarbPath!, args: ["cairo-language-server"] };
serverExecutable = scarb!.languageServerExecutable();
} else {
const command = findLanguageServerExecutable(ctx);
if (command) {
serverExecutable = { command };
serverExecutable = {
command,
options: {
cwd: rootPath(ctx),
env: {
SCARB: scarb?.path,
},
},
};
} else {
ctx.log.error("could not find Cairo language server executable");
ctx.log.error(
Expand All @@ -346,14 +321,6 @@ async function getServerOptions(ctx: Context): Promise<lc.ServerOptions> {
`using CairoLS: ${serverExecutable.command} ${serverExecutable.args?.join(" ") ?? ""}`.trimEnd(),
);

serverExecutable.options ??= {};
serverExecutable.options.cwd = rootPath(ctx);

// Pass path to Scarb to standalone CairoLS. This is not needed for Scarb's wrapper.
if (serverExecutable.command != scarbPath) {
serverExecutable.options.env["SCARB"] = scarbPath;
}

return {
run: serverExecutable,
debug: serverExecutable,
Expand Down
94 changes: 94 additions & 0 deletions vscode-cairo/src/scarb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { spawn } from "child_process";
import * as vscode from "vscode";
import * as lc from "vscode-languageclient/node";
import type { Context } from "./context";

let globalExecId = 0;

export class Scarb {
public constructor(
/**
* The path to the Scarb binary on the local filesystem.
*/
public readonly path: string,
/**
* The exact Scarb binary used can vary depending on workspace configs,
* hence we associate workspace folder reference with the Scarb instance.
*/
public readonly workspaceFolder?: vscode.WorkspaceFolder | undefined,
) {}

public languageServerExecutable(): lc.Executable {
const exec: lc.Executable = {
command: this.path,
args: ["cairo-language-server"],
};

const cwd = this.workspaceFolder?.uri.fsPath;
if (cwd != undefined) {
exec.options ??= {};
exec.options.cwd = cwd;
}

return exec;
}

public hasCairoLS(ctx: Context): Promise<boolean> {
return this.hasCommand("cairo-language-server", ctx);
}

private async hasCommand(command: string, ctx: Context): Promise<boolean> {
const output = await this.execWithOutput(["--json", "commands"], ctx);

if (!output) {
return false;
}

return output
.split("\n")
.map((v) => v.trim())
.filter((v) => !!v)
.map((v) => JSON.parse(v))
.some((commands: Record<string, unknown>) => !!commands[command]);
}

private async execWithOutput(
args: readonly string[],
ctx: Context,
): Promise<string> {
const execId = globalExecId++;

ctx.log.trace(`scarb[${execId}]: ${this.path} ${args.join(" ")}`.trimEnd());

const child = spawn(this.path, args, {
stdio: "pipe",
cwd: this.workspaceFolder?.uri.fsPath,
});

let stdout = "";
for await (const chunk of child.stdout) {
stdout += chunk;
}

if (ctx.log.logLevel <= vscode.LogLevel.Trace) {
if (stdout.length > 0) {
for (const line of stdout.trimEnd().split("\n")) {
ctx.log.trace(`scarb[${execId}]:stdout: ${line}`);
}
}

let stderr = "";
for await (const chunk of child.stderr) {
stderr += chunk;
}

if (stderr.length > 0) {
for (const line of stderr.trimEnd().split("\n")) {
ctx.log.trace(`scarb[${execId}]:stderr: ${line}`);
}
}
}

return stdout;
}
}
Loading