Skip to content

Commit

Permalink
VSCode: Add Scarb class and move Scarb's LS discovery there
Browse files Browse the repository at this point in the history
commit-id:67d23268
  • Loading branch information
mkaput committed Feb 12, 2024
1 parent 2dd0369 commit 81732f5
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 52 deletions.
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;
}
}

0 comments on commit 81732f5

Please sign in to comment.