Skip to content

Commit

Permalink
Add integration for Swift-MesonLSP
Browse files Browse the repository at this point in the history
  • Loading branch information
JCWasmx86 committed Apr 13, 2023
1 parent df85286 commit 72ec732
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 18 deletions.
31 changes: 30 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@
]
},
"description": "Give an object that's merged into the debug configuration provider"
},
"mesonbuild.languageServer": {
"type": "string",
"default": "Swift-MesonLSP",
"enum": [
"Swift-MesonLSP"
],
"description": "Select which language server to use"
},
"mesonbuild.languageServerPath": {
"type": "string",
"description": "Binary name or path to language server",
"default": ""
},
"mesonbuild.downloadLanguageServer": {
"type": "string",
"default": "ask",
"enum": [
"ask",
"never",
"yes"
],
"description": "Have VSCode download the language server automatically (MacOS/Windows only)"
}
}
},
Expand Down Expand Up @@ -326,9 +349,15 @@
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
"@types/node": "^16.11.7",
"@types/vscode": "^1.1.59",
"@types/which": "^2.0.2",
"typescript": "^4.4.4"
},
"dependencies": {}
"dependencies": {
"adm-zip": "0.5.10",
"vscode-languageclient": "8.0.2",
"which": "3.0.0"
}
}
43 changes: 43 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { getMesonTasks } from "./tasks";
import { MesonProjectExplorer } from "./treeview";
import { TargetNode } from "./treeview/nodes/targets"
import {
canDownloadLanguageServer,
setupLanguageServer,
extensionConfiguration,
execAsTask,
workspaceRelative,
Expand All @@ -37,6 +39,7 @@ import {
import {
activateFormatters
} from "./formatters"
import { SwiftMesonLspLanguageClient } from "./lsp/swift-mesonlsp";

export let extensionPath: string;
let explorer: MesonProjectExplorer;
Expand Down Expand Up @@ -207,6 +210,44 @@ export async function activate(ctx: vscode.ExtensionContext) {
}
}

const downloadLanguageServerKey = "downloadLanguageServer";
const downloadLanguageServer = extensionConfiguration(downloadLanguageServerKey);
const languageServerKey = "languageServer"
const lsKey = extensionConfiguration(languageServerKey);
const possibleServers = {
"Swift-MesonLSP": new SwiftMesonLspLanguageClient(ctx)
}
let client = possibleServers[lsKey];
const canDownload = client.canDownloadLanguageServer();
if (canDownload && downloadLanguageServer == "ask") {
enum Options {
yes = "Yes",
no = "Not this time",
never = "Never"
}
const response = await vscode.window.showInformationMessage(
"Would you like to download a language server? (https://github.com/JCWasmx86/Swift-MesonLSP)",
...Object.values(Options)
);

switch (response) {
case Options.no:
extensionConfigurationSet(downloadLanguageServerKey, "ask", vscode.ConfigurationTarget.Global);
break;

case Options.never:
extensionConfigurationSet(downloadLanguageServerKey, "never", vscode.ConfigurationTarget.Global);
break;

case Options.yes:
client.setupLanguageServer();
extensionConfigurationSet(downloadLanguageServerKey, "yes", vscode.ConfigurationTarget.Global);
break;
}
} else if (canDownload && downloadLanguageServer == "yes") {
client.setupLanguageServer();
}

if (configureOnOpen === true) {
await vscode.commands.executeCommand("mesonbuild.configure");
}
Expand Down Expand Up @@ -292,4 +333,6 @@ export async function activate(ctx: vscode.ExtensionContext) {
// Pick cancelled.
}
}

ctx.subscriptions.push(client)
}
59 changes: 59 additions & 0 deletions src/lsp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as which from "which";
import {
canDownloadLanguageServer,
findLanguageServer,
setupLanguageServer,
} from "./utils";
import { ExtensionContext, WorkspaceConfiguration, workspace } from "vscode";
import { LanguageClient } from "vscode-languageclient/node";

export abstract class LanguageServerClient {
config: WorkspaceConfiguration
ls: LanguageClient | null = null
executableName: string
context: ExtensionContext

constructor(executableName: string, context: ExtensionContext) {
this.executableName = executableName
this.context = context
this.config = workspace.getConfiguration("mesonbuild");
const serverModule = this.languageServerPath;

if (serverModule == null)
return;
this.startLanguageServer(serverModule);
}

abstract startLanguageServer(serverModule: string): void

abstract get downloadInfo(): [url: string, hash: string] | null;

restart(): void {
this.dispose();
const serverModule = this.languageServerPath;

if (serverModule == null)
return;
this.startLanguageServer(serverModule);
}

canDownloadLanguageServer(): boolean {
return canDownloadLanguageServer(this)
}

get languageServerPath(): string | null {
return findLanguageServer(this) || this.config.languageServerPath || which.sync(this.executableName, { nothrow: true })
}

async setupLanguageServer() {
setupLanguageServer(this)
}

dispose() {
if (this.ls) {
this.ls.stop()

this.ls = null
}
}
}
60 changes: 60 additions & 0 deletions src/lsp/swift-mesonlsp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as os from "os";

import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
Executable,
TransportKind
} from "vscode-languageclient/node";
import {
ExtensionContext,
} from "vscode"
import { LanguageServerClient } from "../lsp";

export class SwiftMesonLspLanguageClient extends LanguageServerClient {
ls: LanguageClient | null = null

constructor(ctx: ExtensionContext) {
super("Swift-MesonLSP", ctx);
}

get downloadInfo(): [url: string, hash: string] | null {
const platform = os.platform();
if (platform != "win32" && platform != "darwin") {
return null;
}
const arch = os.arch();
if (arch != "x64") {
return null;
}
if (platform == "win32")
return ["https://github.com/JCWasmx86/Swift-MesonLSP/releases/download/v2.0/Swift-MesonLSP-win64.zip",
"fc0d930eb67525309291907c07df8655920cdc651f17f2320d09cc5271a23876"]
return ["https://github.com/JCWasmx86/Swift-MesonLSP/releases/download/v2.0/Swift-MesonLSP-macos12.zip",
"c10999081cef8b56ea164d9a170470c268045254829e48bf4ac137a07a8a874c"]
}

startLanguageServer(serverModule: string): void {
const clientOptions: LanguageClientOptions = {
documentSelector: ["meson", { "scheme": "file", language: "meson" }]
};

const runExe: Executable = {
command: serverModule,
args: ["--lsp"],
};
const debugExe: Executable = {
command: serverModule,
args: ["--lsp"],
};
const serverOptions: ServerOptions = {
run: runExe,
debug: debugExe,
transport: TransportKind.stdio
};

this.ls = new LanguageClient("Swift-MesonLSP", "Meson Language Server", serverOptions, clientOptions, true);
this.ls.start();
}
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ export interface ExtensionConfiguration {
muonConfig: string | null,
};
debugOptions: object;
languageServer: string
languageServerPath: string;
downloadLanguageServer: string;
}
121 changes: 119 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import * as Admzip from "adm-zip";
import * as cp from "child_process";
import * as crypto from "crypto";
import * as fs from "fs";
import * as https from "https";
import * as os from "os";
import * as path from "path";
import * as cp from "child_process";
import * as vscode from "vscode";
import { createHash, BinaryLike } from "crypto";
import { Target } from "./meson/types";
import { ExtensionConfiguration } from "./types";
import { getMesonBuildOptions } from "./meson/introspection";
import { extensionPath } from "./extension";
import { LanguageServerClient } from "./lsp";

export async function exec(
command: string,
Expand Down Expand Up @@ -164,6 +169,118 @@ export function extensionConfigurationSet<
return getConfiguration().update(key, value, target);
}

export function canDownloadLanguageServer(client: LanguageServerClient): boolean {
const platform = os.platform()
if (platform != "win32" && platform != "darwin")
return false
const arch = os.arch()
return arch == "x64" && findLanguageServer(client) == null;
}

export function findLanguageServer(client: LanguageServerClient): string | null {
const platform = os.platform();
const executableName = client.executableName
if (platform != "win32" && platform != "darwin")
return null;
const arch = os.arch();
if (arch != "x64")
return null;
const lspDir = path.join(getExtensionDir(client.context), "lsp", executableName);
const suffix = platform == "win32" ? ".exe" : "";
const fullpath = path.join(lspDir, executableName + suffix);
if (fs.existsSync(fullpath)) {
return fullpath;
}
return null;
}

export async function setupLanguageServer(client: LanguageServerClient) {
const lspDir = path.join(getExtensionDir(client.context), "lsp", client.executableName);
const downloadInfo = client.downloadInfo
fs.rmSync(
lspDir,
{
recursive: true,
force: true
}
)
fs.mkdirSync(
lspDir,
{ recursive: true }
)
const tmpPath = path.join(os.tmpdir(), `lsp-${Date.now()}.zip`);
try {
downloadFile(downloadInfo[0], tmpPath).then(() => {
const hashPromise = computeFileHash(tmpPath);
hashPromise.then(str => {
const expected = downloadInfo[1];
if (str != expected) {
vscode.window.showErrorMessage(`Bad hash: Expected ${expected}, got ${str}!`);
return;
}
const zip = new Admzip(tmpPath);
zip.extractAllTo(lspDir);
if (os.platform() != "win32")
fs.chmodSync(path.join(lspDir, "Swift-MesonLSP"), 0o755);
vscode.window.showInformationMessage("Language server is setup correctly!");
fs.unlinkSync(tmpPath);
client.restart();
}).catch((err: Error) => vscode.window.showErrorMessage(err.message));
}).catch((err: Error) => vscode.window.showErrorMessage(err.message));

} catch (err) {
vscode.window.showErrorMessage(JSON.stringify(err));
return;
}
}

async function downloadFile(url: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const request = https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
const redirectUrl = response.headers.location;
downloadFile(redirectUrl, dest)
.then(() => resolve())
.catch((err) => reject(err));
} else {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
vscode.window.showErrorMessage(`Error writing to file: ${err}`);
reject(err);
});
}
});
request.on('error', (err) => {
vscode.window.showErrorMessage(`Request error: ${err}`);
reject(err);
});
});
}

const getExtensionDir = (ctx: vscode.ExtensionContext): string => {
return path.join(ctx.globalStorageUri.path, ".vscode-meson");
};

const computeFileHash = async (filePath: string): Promise<string> => {
const hash = crypto.createHash("sha256");
const stream = fs.createReadStream(filePath);
stream.on("data", (data) => {
hash.update(data);
});
return new Promise<string>((resolve, reject) => {
stream.on("error", reject);
stream.on("end", () => {
resolve(hash.digest("hex"));
});
});
};


export function arrayIncludes<T>(array: T[], value: T) {
return array.indexOf(value) !== -1;
}
Expand Down Expand Up @@ -226,6 +343,6 @@ export async function patchCompileCommands(buildDir: string) {
const conf = vscode.workspace.getConfiguration("C_Cpp");
conf.update("default.compileCommands", relFilePath, vscode.ConfigurationTarget.Workspace);
} catch {
// Ignore, C/C++ extension might not be installed
// Ignore, C/C++ extension might not be installed
}
}
Loading

0 comments on commit 72ec732

Please sign in to comment.