From 5d88c1db38200896d2e4af7836fec95097adf509 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 8 Feb 2020 04:22:44 +0200 Subject: [PATCH] vscode: amended config to use binary from globalStoragePath, added ui for downloading --- editors/code/package-lock.json | 13 ++ editors/code/package.json | 6 +- editors/code/src/client.ts | 22 +--- editors/code/src/config.ts | 56 +++++++-- editors/code/src/ctx.ts | 6 +- .../{github => installation}/download_file.ts | 2 +- .../fetch_latest_artifact_metadata.ts | 18 +-- editors/code/src/installation/interfaces.ts | 26 ++++ .../code/src/installation/language_server.ts | 119 ++++++++++++++++++ editors/code/tsconfig.json | 2 + 10 files changed, 229 insertions(+), 41 deletions(-) rename editors/code/src/{github => installation}/download_file.ts (91%) rename editors/code/src/{github => installation}/fetch_latest_artifact_metadata.ts (75%) create mode 100644 editors/code/src/installation/interfaces.ts create mode 100644 editors/code/src/installation/language_server.ts diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 5c056463e0..1b7c8910e1 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -753,6 +753,19 @@ "os-tmpdir": "~1.0.1" } }, + "ts-not-nil": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-not-nil/-/ts-not-nil-1.0.1.tgz", + "integrity": "sha512-19+u+3okJddVZlrIdTOdFBaMsHYDInIGDPiujxfRa0RS2Ch5055zVG4GAqa+CZ/Rd1a+7ORSm8O4+2kesPymtw==", + "requires": { + "ts-typedefs": ">=3.2.0" + } + }, + "ts-typedefs": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ts-typedefs/-/ts-typedefs-3.2.0.tgz", + "integrity": "sha512-NglEH2YiY40YxNAvwBISqqXRTKlQq6x+qoCF+tkjPxwrPbrkmq7V3LXavmxrD63fENtMhFkcqgMJtOirtow9iA==" + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", diff --git a/editors/code/package.json b/editors/code/package.json index 8e23718cda..c0a62619de 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -27,6 +27,7 @@ "jsonc-parser": "^2.1.0", "node-fetch": "^2.6.0", "throttle-debounce": "^2.1.0", + "ts-not-nil": "^1.0.1", "vscode-languageclient": "^6.1.0" }, "devDependencies": { @@ -173,10 +174,11 @@ }, "rust-analyzer.raLspServerPath": { "type": [ + "null", "string" ], - "default": "ra_lsp_server", - "description": "Path to ra_lsp_server executable" + "default": null, + "description": "Path to ra_lsp_server executable (points to bundled binary by default)" }, "rust-analyzer.excludeGlobs": { "type": "array", diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 7e7e909dda..7639ed44b3 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -1,24 +1,18 @@ -import { homedir } from 'os'; import * as lc from 'vscode-languageclient'; -import { spawnSync } from 'child_process'; import { window, workspace } from 'vscode'; import { Config } from './config'; +import { ensureLanguageServerBinary } from './installation/language_server'; -export function createClient(config: Config): lc.LanguageClient { +export async function createClient(config: Config): Promise { // '.' Is the fallback if no folder is open // TODO?: Workspace folders support Uri's (eg: file://test.txt). // It might be a good idea to test if the uri points to a file. const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; - const raLspServerPath = expandPathResolving(config.raLspServerPath); - if (spawnSync(raLspServerPath, ["--version"]).status !== 0) { - window.showErrorMessage( - `Unable to execute '${raLspServerPath} --version'\n\n` + - `Perhaps it is not in $PATH?\n\n` + - `PATH=${process.env.PATH}\n` - ); - } + const raLspServerPath = await ensureLanguageServerBinary(config.raLspServerSource); + if (!raLspServerPath) return null; + const run: lc.Executable = { command: raLspServerPath, options: { cwd: workspaceFolderPath }, @@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient { res.registerProposedFeatures(); return res; } -function expandPathResolving(path: string) { - if (path.startsWith('~/')) { - return path.replace('~', homedir()); - } - return path; -} diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 5246204339..aca5dab5a9 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -1,4 +1,6 @@ +import * as os from "os"; import * as vscode from 'vscode'; +import { BinarySource, BinarySourceType } from "./installation/interfaces"; const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; @@ -16,10 +18,24 @@ export interface CargoFeatures { } export class Config { + readonly raLspServerGithubArtifactName = { + linux: "ra_lsp_server-linux", + darwin: "ra_lsp_server-mac", + win32: "ra_lsp_server-windows.exe", + aix: null, + android: null, + freebsd: null, + openbsd: null, + sunos: null, + cygwin: null, + netbsd: null, + }[process.platform]; + + raLspServerSource!: null | BinarySource; + highlightingOn = true; rainbowHighlightingOn = false; enableEnhancedTyping = true; - raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; lruCapacity: null | number = null; displayInlayHints = true; maxInlayHintLength: null | number = null; @@ -45,11 +61,20 @@ export class Config { private prevCargoWatchOptions: null | CargoWatchOptions = null; constructor(ctx: vscode.ExtensionContext) { - vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions); - this.refresh(); + vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions); + this.refresh(ctx); } - private refresh() { + private static expandPathResolving(path: string) { + if (path.startsWith('~/')) { + return path.replace('~', os.homedir()); + } + return path; + } + + // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default + // values only in one place (i.e. remove default values from non-readonly members declarations) + private refresh(ctx: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration('rust-analyzer'); let requireReloadMessage = null; @@ -82,9 +107,26 @@ export class Config { this.prevEnhancedTyping = this.enableEnhancedTyping; } - if (config.has('raLspServerPath')) { - this.raLspServerPath = - RA_LSP_DEBUG || (config.get('raLspServerPath') as string); + { + const raLspServerPath = RA_LSP_DEBUG ?? config.get("raLspServerPath"); + if (raLspServerPath) { + this.raLspServerSource = { + type: BinarySourceType.ExplicitPath, + path: Config.expandPathResolving(raLspServerPath) + }; + } else if (this.raLspServerGithubArtifactName) { + this.raLspServerSource = { + type: BinarySourceType.GithubBinary, + dir: ctx.globalStoragePath, + file: this.raLspServerGithubArtifactName, + repo: { + name: "rust-analyzer", + owner: "rust-analyzer", + } + }; + } else { + this.raLspServerSource = null; + } } if (config.has('cargo-watch.enable')) { diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index a4dcc3037b..f0e2d72f75 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -29,7 +29,11 @@ export class Ctx { await old.stop(); } this.client = null; - const client = createClient(this.config); + const client = await createClient(this.config); + if (!client) { + throw new Error("Rust Analyzer Language Server is not available"); + } + this.pushCleanup(client.start()); await client.onReady(); diff --git a/editors/code/src/github/download_file.ts b/editors/code/src/installation/download_file.ts similarity index 91% rename from editors/code/src/github/download_file.ts rename to editors/code/src/installation/download_file.ts index f40750be90..7b537e114c 100644 --- a/editors/code/src/github/download_file.ts +++ b/editors/code/src/installation/download_file.ts @@ -7,7 +7,7 @@ export async function downloadFile( destFilePath: fs.PathLike, onProgress: (readBytes: number, totalBytes: number) => void ): Promise { - onProgress = throttle(100, /* noTrailing: */ true, onProgress); + onProgress = throttle(1000, /* noTrailing: */ true, onProgress); const response = await fetch(url); diff --git a/editors/code/src/github/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts similarity index 75% rename from editors/code/src/github/fetch_latest_artifact_metadata.ts rename to editors/code/src/installation/fetch_latest_artifact_metadata.ts index 52641ca67d..f07431aac4 100644 --- a/editors/code/src/github/fetch_latest_artifact_metadata.ts +++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts @@ -1,25 +1,19 @@ import fetch from "node-fetch"; +import { GithubRepo, ArtifactMetadata } from "./interfaces"; const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; export interface FetchLatestArtifactMetadataOpts { - repoName: string; - repoOwner: string; + repo: GithubRepo; artifactFileName: string; } -export interface ArtifactMetadata { - releaseName: string; - releaseDate: Date; - downloadUrl: string; -} - export async function fetchLatestArtifactMetadata( opts: FetchLatestArtifactMetadataOpts -): Promise { +): Promise { - const repoOwner = encodeURIComponent(opts.repoOwner); - const repoName = encodeURIComponent(opts.repoName); + const repoOwner = encodeURIComponent(opts.repo.owner); + const repoName = encodeURIComponent(opts.repo.name); const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; @@ -35,14 +29,12 @@ export async function fetchLatestArtifactMetadata( return !artifact ? null : { releaseName: response.name, - releaseDate: new Date(response.published_at), downloadUrl: artifact.browser_download_url }; // Noise denotes tremendous amount of data that we are not using here interface GithubRelease { name: string; - published_at: Date; assets: Array<{ browser_download_url: string; diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts new file mode 100644 index 0000000000..f54e24e262 --- /dev/null +++ b/editors/code/src/installation/interfaces.ts @@ -0,0 +1,26 @@ +export interface GithubRepo { + name: string; + owner: string; +} + +export interface ArtifactMetadata { + releaseName: string; + downloadUrl: string; +} + + +export enum BinarySourceType { ExplicitPath, GithubBinary } + +export type BinarySource = EplicitPathSource | GithubBinarySource; + +export interface EplicitPathSource { + type: BinarySourceType.ExplicitPath; + path: string; +} + +export interface GithubBinarySource { + type: BinarySourceType.GithubBinary; + repo: GithubRepo; + dir: string; + file: string; +} diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts new file mode 100644 index 0000000000..2b3ce6621b --- /dev/null +++ b/editors/code/src/installation/language_server.ts @@ -0,0 +1,119 @@ +import { unwrapNotNil } from "ts-not-nil"; +import { spawnSync } from "child_process"; +import * as vscode from "vscode"; +import * as path from "path"; +import { strict as assert } from "assert"; +import { promises as fs } from "fs"; + +import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces"; +import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata"; +import { downloadFile } from "./download_file"; + +export async function downloadLatestLanguageServer( + {file: artifactFileName, dir: installationDir, repo}: GithubBinarySource +) { + const binaryMetadata = await fetchLatestArtifactMetadata({ artifactFileName, repo }); + + const { + releaseName, + downloadUrl + } = unwrapNotNil(binaryMetadata, `Latest GitHub release lacks "${artifactFileName}" file`); + + await fs.mkdir(installationDir).catch(err => assert.strictEqual( + err && err.code, + "EEXIST", + `Couldn't create directory "${installationDir}" to download `+ + `language server binary: ${err.message}` + )); + + const installationPath = path.join(installationDir, artifactFileName); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, // FIXME: add support for canceling download? + title: `Downloading language server ${releaseName}` + }, + async (progress, _) => { + let lastPrecentage = 0; + await downloadFile(downloadUrl, installationPath, (readBytes, totalBytes) => { + const newPercentage = (readBytes / totalBytes) * 100; + progress.report({ + message: newPercentage.toFixed(0) + "%", + increment: newPercentage - lastPrecentage + }); + + lastPrecentage = newPercentage; + }); + } + ); + + await fs.chmod(installationPath, 111); // Set xxx permissions +} +export async function ensureLanguageServerBinary( + langServerSource: null | BinarySource +): Promise { + + if (!langServerSource) { + vscode.window.showErrorMessage( + "Unfortunately we don't ship binaries for your platform yet. " + + "You need to manually clone rust-analyzer repository and " + + "run `cargo xtask install --server` to build the language server from sources. " + + "If you feel that your platform should be supported, please create an issue " + + "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + + "will consider it." + ); + return null; + } + + switch (langServerSource.type) { + case BinarySourceType.ExplicitPath: { + if (isBinaryAvailable(langServerSource.path)) { + return langServerSource.path; + } + vscode.window.showErrorMessage( + `Unable to execute ${'`'}${langServerSource.path} --version${'`'}. ` + + "To use the bundled language server, set `rust-analyzer.raLspServerPath` " + + "value to `null` or remove it from the settings to use it by default." + ); + return null; + } + case BinarySourceType.GithubBinary: { + const bundledBinaryPath = path.join(langServerSource.dir, langServerSource.file); + + if (!isBinaryAvailable(bundledBinaryPath)) { + const userResponse = await vscode.window.showInformationMessage( + `Language server binary for rust-analyzer was not found. ` + + `Do you want to download it now?`, + "Download now", "Cancel" + ); + if (userResponse !== "Download now") return null; + + try { + await downloadLatestLanguageServer(langServerSource); + } catch (err) { + await vscode.window.showErrorMessage( + `Failed to download language server from ${langServerSource.repo.name} ` + + `GitHub repository: ${err.message}` + ); + return null; + } + + + assert( + isBinaryAvailable(bundledBinaryPath), + "Downloaded language server binary is not functional" + ); + + vscode.window.showInformationMessage( + "Rust analyzer language server was successfully installed" + ); + } + return bundledBinaryPath; + } + } + + function isBinaryAvailable(binaryPath: string) { + return spawnSync(binaryPath, ["--version"]).status === 0; + } +} diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json index e60eb8e5e5..0c7702974a 100644 --- a/editors/code/tsconfig.json +++ b/editors/code/tsconfig.json @@ -6,6 +6,8 @@ "lib": [ "es2019" ], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "sourceMap": true, "rootDir": "src", "strict": true,