aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts22
-rw-r--r--editors/code/src/config.ts94
-rw-r--r--editors/code/src/ctx.ts12
-rw-r--r--editors/code/src/installation/download_file.ts44
-rw-r--r--editors/code/src/installation/fetch_latest_artifact_metadata.ts46
-rw-r--r--editors/code/src/installation/interfaces.ts55
-rw-r--r--editors/code/src/installation/language_server.ts146
7 files changed, 393 insertions, 26 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 7e7e909dd..2e3d4aba2 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -1,24 +1,18 @@
1import { homedir } from 'os';
2import * as lc from 'vscode-languageclient'; 1import * as lc from 'vscode-languageclient';
3import { spawnSync } from 'child_process';
4 2
5import { window, workspace } from 'vscode'; 3import { window, workspace } from 'vscode';
6import { Config } from './config'; 4import { Config } from './config';
5import { ensureLanguageServerBinary } from './installation/language_server';
7 6
8export function createClient(config: Config): lc.LanguageClient { 7export async function createClient(config: Config): Promise<null | lc.LanguageClient> {
9 // '.' Is the fallback if no folder is open 8 // '.' Is the fallback if no folder is open
10 // TODO?: Workspace folders support Uri's (eg: file://test.txt). 9 // TODO?: Workspace folders support Uri's (eg: file://test.txt).
11 // It might be a good idea to test if the uri points to a file. 10 // It might be a good idea to test if the uri points to a file.
12 const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; 11 const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
13 12
14 const raLspServerPath = expandPathResolving(config.raLspServerPath); 13 const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource);
15 if (spawnSync(raLspServerPath, ["--version"]).status !== 0) { 14 if (!raLspServerPath) return null;
16 window.showErrorMessage( 15
17 `Unable to execute '${raLspServerPath} --version'\n\n` +
18 `Perhaps it is not in $PATH?\n\n` +
19 `PATH=${process.env.PATH}\n`
20 );
21 }
22 const run: lc.Executable = { 16 const run: lc.Executable = {
23 command: raLspServerPath, 17 command: raLspServerPath,
24 options: { cwd: workspaceFolderPath }, 18 options: { cwd: workspaceFolderPath },
@@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
87 res.registerProposedFeatures(); 81 res.registerProposedFeatures();
88 return res; 82 return res;
89} 83}
90function expandPathResolving(path: string) {
91 if (path.startsWith('~/')) {
92 return path.replace('~', homedir());
93 }
94 return path;
95}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 524620433..418845436 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,4 +1,6 @@
1import * as os from "os";
1import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import { BinarySource } from "./installation/interfaces";
2 4
3const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; 5const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
4 6
@@ -16,10 +18,11 @@ export interface CargoFeatures {
16} 18}
17 19
18export class Config { 20export class Config {
21 langServerSource!: null | BinarySource;
22
19 highlightingOn = true; 23 highlightingOn = true;
20 rainbowHighlightingOn = false; 24 rainbowHighlightingOn = false;
21 enableEnhancedTyping = true; 25 enableEnhancedTyping = true;
22 raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
23 lruCapacity: null | number = null; 26 lruCapacity: null | number = null;
24 displayInlayHints = true; 27 displayInlayHints = true;
25 maxInlayHintLength: null | number = null; 28 maxInlayHintLength: null | number = null;
@@ -45,11 +48,89 @@ export class Config {
45 private prevCargoWatchOptions: null | CargoWatchOptions = null; 48 private prevCargoWatchOptions: null | CargoWatchOptions = null;
46 49
47 constructor(ctx: vscode.ExtensionContext) { 50 constructor(ctx: vscode.ExtensionContext) {
48 vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions); 51 vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
49 this.refresh(); 52 this.refresh(ctx);
53 }
54
55 private static expandPathResolving(path: string) {
56 if (path.startsWith('~/')) {
57 return path.replace('~', os.homedir());
58 }
59 return path;
50 } 60 }
51 61
52 private refresh() { 62 /**
63 * Name of the binary artifact for `ra_lsp_server` that is published for
64 * `platform` on GitHub releases. (It is also stored under the same name when
65 * downloaded by the extension).
66 */
67 private static prebuiltLangServerFileName(
68 platform: NodeJS.Platform,
69 arch: string
70 ): null | string {
71 // See possible `arch` values here:
72 // https://nodejs.org/api/process.html#process_process_arch
73
74 switch (platform) {
75
76 case "linux": {
77 switch (arch) {
78 case "arm":
79 case "arm64": return null;
80
81 default: return "ra_lsp_server-linux";
82 }
83 }
84
85 case "darwin": return "ra_lsp_server-mac";
86 case "win32": return "ra_lsp_server-windows.exe";
87
88 // Users on these platforms yet need to manually build from sources
89 case "aix":
90 case "android":
91 case "freebsd":
92 case "openbsd":
93 case "sunos":
94 case "cygwin":
95 case "netbsd": return null;
96 // The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
97 }
98 }
99
100 private static langServerBinarySource(
101 ctx: vscode.ExtensionContext,
102 config: vscode.WorkspaceConfiguration
103 ): null | BinarySource {
104 const langServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
105
106 if (langServerPath) {
107 return {
108 type: BinarySource.Type.ExplicitPath,
109 path: Config.expandPathResolving(langServerPath)
110 };
111 }
112
113 const prebuiltBinaryName = Config.prebuiltLangServerFileName(
114 process.platform, process.arch
115 );
116
117 if (!prebuiltBinaryName) return null;
118
119 return {
120 type: BinarySource.Type.GithubRelease,
121 dir: ctx.globalStoragePath,
122 file: prebuiltBinaryName,
123 repo: {
124 name: "rust-analyzer",
125 owner: "rust-analyzer",
126 }
127 };
128 }
129
130
131 // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
132 // values only in one place (i.e. remove default values from non-readonly members declarations)
133 private refresh(ctx: vscode.ExtensionContext) {
53 const config = vscode.workspace.getConfiguration('rust-analyzer'); 134 const config = vscode.workspace.getConfiguration('rust-analyzer');
54 135
55 let requireReloadMessage = null; 136 let requireReloadMessage = null;
@@ -82,10 +163,7 @@ export class Config {
82 this.prevEnhancedTyping = this.enableEnhancedTyping; 163 this.prevEnhancedTyping = this.enableEnhancedTyping;
83 } 164 }
84 165
85 if (config.has('raLspServerPath')) { 166 this.langServerSource = Config.langServerBinarySource(ctx, config);
86 this.raLspServerPath =
87 RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
88 }
89 167
90 if (config.has('cargo-watch.enable')) { 168 if (config.has('cargo-watch.enable')) {
91 this.cargoWatchOptions.enable = config.get<boolean>( 169 this.cargoWatchOptions.enable = config.get<boolean>(
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index aa75943bf..70042a479 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -11,6 +11,9 @@ export class Ctx {
11 // deal with it. 11 // deal with it.
12 // 12 //
13 // Ideally, this should be replaced with async getter though. 13 // Ideally, this should be replaced with async getter though.
14 // FIXME: this actually needs syncronization of some kind (check how
15 // vscode deals with `deactivate()` call when extension has some work scheduled
16 // on the event loop to get a better picture of what we can do here)
14 client: lc.LanguageClient | null = null; 17 client: lc.LanguageClient | null = null;
15 private extCtx: vscode.ExtensionContext; 18 private extCtx: vscode.ExtensionContext;
16 private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = []; 19 private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
@@ -26,7 +29,14 @@ export class Ctx {
26 await old.stop(); 29 await old.stop();
27 } 30 }
28 this.client = null; 31 this.client = null;
29 const client = createClient(this.config); 32 const client = await createClient(this.config);
33 if (!client) {
34 throw new Error(
35 "Rust Analyzer Language Server is not available. " +
36 "Please, ensure its [proper installation](https://github.com/rust-analyzer/rust-analyzer/tree/master/docs/user#vs-code)."
37 );
38 }
39
30 this.pushCleanup(client.start()); 40 this.pushCleanup(client.start());
31 await client.onReady(); 41 await client.onReady();
32 42
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts
new file mode 100644
index 000000000..f1f9f4a25
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,44 @@
1import fetch from "node-fetch";
2import * as fs from "fs";
3import { strict as assert } from "assert";
4
5/**
6 * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
7 * `onProgress` callback is called on recieveing each chunk of bytes
8 * to track the progress of downloading, it gets the already read and total
9 * amount of bytes to read as its parameters.
10 */
11export async function downloadFile(
12 url: string,
13 destFilePath: fs.PathLike,
14 destFilePermissions: number,
15 onProgress: (readBytes: number, totalBytes: number) => void
16): Promise<void> {
17 const res = await fetch(url);
18
19 if (!res.ok) {
20 console.log("Error", res.status, "while downloading file from", url);
21 console.dir({ body: await res.text(), headers: res.headers }, { depth: 3 });
22
23 throw new Error(`Got response ${res.status} when trying to download a file`);
24 }
25
26 const totalBytes = Number(res.headers.get('content-length'));
27 assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
28
29 let readBytes = 0;
30
31 console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
32
33 return new Promise<void>((resolve, reject) => res.body
34 .on("data", (chunk: Buffer) => {
35 readBytes += chunk.length;
36 onProgress(readBytes, totalBytes);
37 })
38 .on("error", reject)
39 .pipe(fs
40 .createWriteStream(destFilePath, { mode: destFilePermissions })
41 .on("close", resolve)
42 )
43 );
44}
diff --git a/editors/code/src/installation/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
new file mode 100644
index 000000000..7e3700603
--- /dev/null
+++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
@@ -0,0 +1,46 @@
1import fetch from "node-fetch";
2import { GithubRepo, ArtifactMetadata } from "./interfaces";
3
4const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
5
6/**
7 * Fetches the latest release from GitHub `repo` and returns metadata about
8 * `artifactFileName` shipped with this release or `null` if no such artifact was published.
9 */
10export async function fetchLatestArtifactMetadata(
11 repo: GithubRepo, artifactFileName: string
12): Promise<null | ArtifactMetadata> {
13
14 const repoOwner = encodeURIComponent(repo.owner);
15 const repoName = encodeURIComponent(repo.name);
16
17 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
18 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
19
20 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
21
22 console.log("Issuing request for released artifacts metadata to", requestUrl);
23
24 const response: GithubRelease = await fetch(requestUrl, {
25 headers: { Accept: "application/vnd.github.v3+json" }
26 })
27 .then(res => res.json());
28
29 const artifact = response.assets.find(artifact => artifact.name === artifactFileName);
30
31 if (!artifact) return null;
32
33 return {
34 releaseName: response.name,
35 downloadUrl: artifact.browser_download_url
36 };
37
38 // We omit declaration of tremendous amount of fields that we are not using here
39 interface GithubRelease {
40 name: string;
41 assets: Array<{
42 name: string;
43 browser_download_url: string;
44 }>;
45 }
46}
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
new file mode 100644
index 000000000..8039d0b90
--- /dev/null
+++ b/editors/code/src/installation/interfaces.ts
@@ -0,0 +1,55 @@
1export interface GithubRepo {
2 name: string;
3 owner: string;
4}
5
6/**
7 * Metadata about particular artifact retrieved from GitHub releases.
8 */
9export interface ArtifactMetadata {
10 releaseName: string;
11 downloadUrl: string;
12}
13
14/**
15 * Represents the source of a binary artifact which is either specified by the user
16 * explicitly, or bundled by this extension from GitHub releases.
17 */
18export type BinarySource = BinarySource.ExplicitPath | BinarySource.GithubRelease;
19
20export namespace BinarySource {
21 /**
22 * Type tag for `BinarySource` discriminated union.
23 */
24 export const enum Type { ExplicitPath, GithubRelease }
25
26 export interface ExplicitPath {
27 type: Type.ExplicitPath;
28
29 /**
30 * Filesystem path to the binary specified by the user explicitly.
31 */
32 path: string;
33 }
34
35 export interface GithubRelease {
36 type: Type.GithubRelease;
37
38 /**
39 * Repository where the binary is stored.
40 */
41 repo: GithubRepo;
42
43 /**
44 * Directory on the filesystem where the bundled binary is stored.
45 */
46 dir: string;
47
48 /**
49 * Name of the binary file. It is stored under the same name on GitHub releases
50 * and in local `.dir`.
51 */
52 file: string;
53 }
54
55}
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts
new file mode 100644
index 000000000..52c5cbe7d
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,146 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { strict as assert } from "assert";
4import { promises as fs } from "fs";
5import { promises as dns } from "dns";
6import { spawnSync } from "child_process";
7import { throttle } from "throttle-debounce";
8
9import { BinarySource } from "./interfaces";
10import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
11import { downloadFile } from "./download_file";
12
13export async function downloadLatestLanguageServer(
14 {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease
15) {
16 const { releaseName, downloadUrl } = (await fetchLatestArtifactMetadata(
17 repo, artifactFileName
18 ))!;
19
20 await fs.mkdir(installationDir).catch(err => assert.strictEqual(
21 err?.code,
22 "EEXIST",
23 `Couldn't create directory "${installationDir}" to download `+
24 `language server binary: ${err.message}`
25 ));
26
27 const installationPath = path.join(installationDir, artifactFileName);
28
29 console.time("Downloading ra_lsp_server");
30 await vscode.window.withProgress(
31 {
32 location: vscode.ProgressLocation.Notification,
33 cancellable: false, // FIXME: add support for canceling download?
34 title: `Downloading language server (${releaseName})`
35 },
36 async (progress, _cancellationToken) => {
37 let lastPrecentage = 0;
38 const filePermissions = 0o755; // (rwx, r_x, r_x)
39 await downloadFile(downloadUrl, installationPath, filePermissions, throttle(
40 200,
41 /* noTrailing: */ true,
42 (readBytes, totalBytes) => {
43 const newPercentage = (readBytes / totalBytes) * 100;
44 progress.report({
45 message: newPercentage.toFixed(0) + "%",
46 increment: newPercentage - lastPrecentage
47 });
48
49 lastPrecentage = newPercentage;
50 })
51 );
52 }
53 );
54 console.timeEnd("Downloading ra_lsp_server");
55}
56export async function ensureLanguageServerBinary(
57 langServerSource: null | BinarySource
58): Promise<null | string> {
59
60 if (!langServerSource) {
61 vscode.window.showErrorMessage(
62 "Unfortunately we don't ship binaries for your platform yet. " +
63 "You need to manually clone rust-analyzer repository and " +
64 "run `cargo xtask install --server` to build the language server from sources. " +
65 "If you feel that your platform should be supported, please create an issue " +
66 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
67 "will consider it."
68 );
69 return null;
70 }
71
72 switch (langServerSource.type) {
73 case BinarySource.Type.ExplicitPath: {
74 if (isBinaryAvailable(langServerSource.path)) {
75 return langServerSource.path;
76 }
77
78 vscode.window.showErrorMessage(
79 `Unable to run ${langServerSource.path} binary. ` +
80 `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` +
81 "value to `null` or remove it from the settings to use it by default."
82 );
83 return null;
84 }
85 case BinarySource.Type.GithubRelease: {
86 const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file);
87
88 if (isBinaryAvailable(prebuiltBinaryPath)) {
89 return prebuiltBinaryPath;
90 }
91
92 const userResponse = await vscode.window.showInformationMessage(
93 "Language server binary for rust-analyzer was not found. " +
94 "Do you want to download it now?",
95 "Download now", "Cancel"
96 );
97 if (userResponse !== "Download now") return null;
98
99 try {
100 await downloadLatestLanguageServer(langServerSource);
101 } catch (err) {
102 vscode.window.showErrorMessage(
103 `Failed to download language server from ${langServerSource.repo.name} ` +
104 `GitHub repository: ${err.message}`
105 );
106
107 dns.resolve('example.com').then(
108 addrs => console.log("DNS resolution for example.com was successful", addrs),
109 err => {
110 console.error(
111 "DNS resolution for example.com failed, " +
112 "there might be an issue with Internet availability"
113 );
114 console.error(err);
115 }
116 );
117
118 return null;
119 }
120
121 if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false,
122 `Downloaded language server binary is not functional.` +
123 `Downloaded from: ${JSON.stringify(langServerSource)}`
124 );
125
126
127 vscode.window.showInformationMessage(
128 "Rust analyzer language server was successfully installed 🦀"
129 );
130
131 return prebuiltBinaryPath;
132 }
133 }
134
135 function isBinaryAvailable(binaryPath: string) {
136 const res = spawnSync(binaryPath, ["--version"]);
137
138 // ACHTUNG! `res` type declaration is inherently wrong, see
139 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
140
141 console.log("Checked binary availablity via --version", res);
142 console.log(binaryPath, "--version output:", res.output?.map(String));
143
144 return res.status === 0;
145 }
146}