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 10, 2023
1 parent 0a73ec8 commit 49ac4ec
Show file tree
Hide file tree
Showing 6 changed files with 377 additions and 19 deletions.
20 changes: 18 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@
]
},
"description": "Give an object that's merged into the debug configuration provider"
},
"mesonbuild.languageServerPath": {
"type": "string",
"description": "Binary name or path to language server",
"default": ""
},
"mesonbuild.neverDownloadLanguageServer": {
"type": "boolean",
"description": "Have VSCode never download the language server automatically (MacOS/Windows only)",
"default": false
}
}
},
Expand Down Expand Up @@ -323,7 +333,13 @@
"devDependencies": {
"@types/node": "^16.11.7",
"@types/vscode": "^1.1.59",
"typescript": "^4.4.4"
"typescript": "^4.4.4",
"@types/which": "^2.0.2",
"@types/adm-zip": "^0.5.0"
},
"dependencies": {}
"dependencies": {
"vscode-languageclient": "8.0.2",
"which": "3.0.0",
"adm-zip": "0.5.10"
}
}
34 changes: 34 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 @@ -36,6 +38,7 @@ import {
import {
activateFormatters
} from "./formatters"
import { MesonLanguageClient } from './lsp'

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

let client = new MesonLanguageClient(ctx);
const neverDownloadLanguageServerKey = "neverDownloadLanguageServer"
if (canDownloadLanguageServer() && !extensionConfiguration(neverDownloadLanguageServerKey)) {
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(neverDownloadLanguageServerKey, false, vscode.ConfigurationTarget.Global);
break;

case Options.never:
extensionConfigurationSet(neverDownloadLanguageServerKey, true, vscode.ConfigurationTarget.Global);
break;

case Options.yes:
setupLanguageServer(client);
extensionConfigurationSet(neverDownloadLanguageServerKey, false, vscode.ConfigurationTarget.Global);
break;
}
}

if (configureOnOpen === true) {
await vscode.commands.executeCommand("mesonbuild.configure");
explorer.refresh();
Expand Down Expand Up @@ -308,4 +340,6 @@ export async function activate(ctx: vscode.ExtensionContext) {

explorer.refresh();
}

ctx.subscriptions.push(client)
}
77 changes: 77 additions & 0 deletions src/lsp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
Executable,
TransportKind
} from "vscode-languageclient/node";

import {
ExtensionContext,
workspace,
WorkspaceConfiguration,
} from "vscode"

import {
findLanguageServer,
} from "./utils";

import * as which from "which"

export class MesonLanguageClient {
config: WorkspaceConfiguration
ls: LanguageClient | null = null

constructor(_context: ExtensionContext) {
this.config = workspace.getConfiguration('vala');
let serverModule = this.languageServerPath;

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

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

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

this.ls = new LanguageClient("Swift-MesonLSP", "Meson Language Server", serverOptions, clientOptions, true);
this.ls.start();
}

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

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

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

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

this.ls = null
}
}
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ export interface ExtensionConfiguration {
muonConfig: string | null,
};
debugOptions: object;
languageServerPath: string;
neverDownloadLanguageServer: boolean;
}
138 changes: 136 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 { MesonLanguageClient } from "./lsp";

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

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

function createDownloadURL(): string {
const platform = os.platform()
const filename = platform == "win32" ? "Swift-MesonLSP-win64.zip" : "Swift-MesonLSP-macos12.zip"
return `https://github.com/JCWasmx86/Swift-MesonLSP/releases/download/v1.6/${filename}`
}

function createHashForLanguageServer(): string {
const platform = os.platform()
return platform == "win32" ?
"3707be8c4aabcbe366f1d7ada66da9f6def38ced8eaee5784fb1701360292c57"
: "888929c9abeada1a16b50312146b33741255f88ddf5ff357fbe67dbe7a7a7c98"
}

export function findLanguageServer(): string | null {
const platform = os.platform();
if (platform != "win32" && platform != "darwin") {
return null;
}
const arch = os.arch();
if (arch != "x64") {
return null;
}
const lspDir = path.join(getExtensionDir(), "lsp");
const filename = platform == "win32" ? "Swift-MesonLSP.exe" : "Swift-MesonLSP";
const fullpath = path.join(lspDir, filename);
if (fs.existsSync(fullpath)) {
return fullpath;
}
return null;
}

export async function setupLanguageServer(client: MesonLanguageClient) {
const lspDir = path.join(getExtensionDir(), "lsp");
fs.rmSync(
lspDir,
{
recursive: true,
force: true
}
)
fs.mkdirSync(
lspDir,
{ recursive: true }
)
const tmpPath = path.join(os.tmpdir(), `lsp-${Date.now()}.zip`);
try {
const promise = downloadFile(createDownloadURL(), tmpPath);
promise.then(() => {
const hashPromise = computeFileHash(tmpPath);
hashPromise.then(str => {
const expected = createHashForLanguageServer();
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 = (): string => {
return path.join(os.homedir(), ".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 @@ -192,6 +326,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 49ac4ec

Please sign in to comment.