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.ts56
-rw-r--r--editors/code/src/ctx.ts6
-rw-r--r--editors/code/src/installation/download_file.ts (renamed from editors/code/src/github/download_file.ts)2
-rw-r--r--editors/code/src/installation/fetch_latest_artifact_metadata.ts (renamed from editors/code/src/github/fetch_latest_artifact_metadata.ts)18
-rw-r--r--editors/code/src/installation/interfaces.ts26
-rw-r--r--editors/code/src/installation/language_server.ts119
7 files changed, 210 insertions, 39 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 7e7e909dd..7639ed44b 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.raLspServerSource);
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..aca5dab5a 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, BinarySourceType } 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,24 @@ export interface CargoFeatures {
16} 18}
17 19
18export class Config { 20export class Config {
21 readonly raLspServerGithubArtifactName = {
22 linux: "ra_lsp_server-linux",
23 darwin: "ra_lsp_server-mac",
24 win32: "ra_lsp_server-windows.exe",
25 aix: null,
26 android: null,
27 freebsd: null,
28 openbsd: null,
29 sunos: null,
30 cygwin: null,
31 netbsd: null,
32 }[process.platform];
33
34 raLspServerSource!: null | BinarySource;
35
19 highlightingOn = true; 36 highlightingOn = true;
20 rainbowHighlightingOn = false; 37 rainbowHighlightingOn = false;
21 enableEnhancedTyping = true; 38 enableEnhancedTyping = true;
22 raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
23 lruCapacity: null | number = null; 39 lruCapacity: null | number = null;
24 displayInlayHints = true; 40 displayInlayHints = true;
25 maxInlayHintLength: null | number = null; 41 maxInlayHintLength: null | number = null;
@@ -45,11 +61,20 @@ export class Config {
45 private prevCargoWatchOptions: null | CargoWatchOptions = null; 61 private prevCargoWatchOptions: null | CargoWatchOptions = null;
46 62
47 constructor(ctx: vscode.ExtensionContext) { 63 constructor(ctx: vscode.ExtensionContext) {
48 vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions); 64 vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
49 this.refresh(); 65 this.refresh(ctx);
66 }
67
68 private static expandPathResolving(path: string) {
69 if (path.startsWith('~/')) {
70 return path.replace('~', os.homedir());
71 }
72 return path;
50 } 73 }
51 74
52 private refresh() { 75 // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
76 // values only in one place (i.e. remove default values from non-readonly members declarations)
77 private refresh(ctx: vscode.ExtensionContext) {
53 const config = vscode.workspace.getConfiguration('rust-analyzer'); 78 const config = vscode.workspace.getConfiguration('rust-analyzer');
54 79
55 let requireReloadMessage = null; 80 let requireReloadMessage = null;
@@ -82,9 +107,26 @@ export class Config {
82 this.prevEnhancedTyping = this.enableEnhancedTyping; 107 this.prevEnhancedTyping = this.enableEnhancedTyping;
83 } 108 }
84 109
85 if (config.has('raLspServerPath')) { 110 {
86 this.raLspServerPath = 111 const raLspServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
87 RA_LSP_DEBUG || (config.get('raLspServerPath') as string); 112 if (raLspServerPath) {
113 this.raLspServerSource = {
114 type: BinarySourceType.ExplicitPath,
115 path: Config.expandPathResolving(raLspServerPath)
116 };
117 } else if (this.raLspServerGithubArtifactName) {
118 this.raLspServerSource = {
119 type: BinarySourceType.GithubBinary,
120 dir: ctx.globalStoragePath,
121 file: this.raLspServerGithubArtifactName,
122 repo: {
123 name: "rust-analyzer",
124 owner: "rust-analyzer",
125 }
126 };
127 } else {
128 this.raLspServerSource = null;
129 }
88 } 130 }
89 131
90 if (config.has('cargo-watch.enable')) { 132 if (config.has('cargo-watch.enable')) {
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index a4dcc3037..f0e2d72f7 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -29,7 +29,11 @@ export class Ctx {
29 await old.stop(); 29 await old.stop();
30 } 30 }
31 this.client = null; 31 this.client = null;
32 const client = createClient(this.config); 32 const client = await createClient(this.config);
33 if (!client) {
34 throw new Error("Rust Analyzer Language Server is not available");
35 }
36
33 this.pushCleanup(client.start()); 37 this.pushCleanup(client.start());
34 await client.onReady(); 38 await client.onReady();
35 39
diff --git a/editors/code/src/github/download_file.ts b/editors/code/src/installation/download_file.ts
index f40750be9..7b537e114 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(
7 destFilePath: fs.PathLike, 7 destFilePath: fs.PathLike,
8 onProgress: (readBytes: number, totalBytes: number) => void 8 onProgress: (readBytes: number, totalBytes: number) => void
9): Promise<void> { 9): Promise<void> {
10 onProgress = throttle(100, /* noTrailing: */ true, onProgress); 10 onProgress = throttle(1000, /* noTrailing: */ true, onProgress);
11 11
12 const response = await fetch(url); 12 const response = await fetch(url);
13 13
diff --git a/editors/code/src/github/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
index 52641ca67..f07431aac 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 @@
1import fetch from "node-fetch"; 1import fetch from "node-fetch";
2import { GithubRepo, ArtifactMetadata } from "./interfaces";
2 3
3const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; 4const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
4 5
5export interface FetchLatestArtifactMetadataOpts { 6export interface FetchLatestArtifactMetadataOpts {
6 repoName: string; 7 repo: GithubRepo;
7 repoOwner: string;
8 artifactFileName: string; 8 artifactFileName: string;
9} 9}
10 10
11export interface ArtifactMetadata {
12 releaseName: string;
13 releaseDate: Date;
14 downloadUrl: string;
15}
16
17export async function fetchLatestArtifactMetadata( 11export async function fetchLatestArtifactMetadata(
18 opts: FetchLatestArtifactMetadataOpts 12 opts: FetchLatestArtifactMetadataOpts
19): Promise<ArtifactMetadata | null> { 13): Promise<null | ArtifactMetadata> {
20 14
21 const repoOwner = encodeURIComponent(opts.repoOwner); 15 const repoOwner = encodeURIComponent(opts.repo.owner);
22 const repoName = encodeURIComponent(opts.repoName); 16 const repoName = encodeURIComponent(opts.repo.name);
23 17
24 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; 18 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
25 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; 19 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
@@ -35,14 +29,12 @@ export async function fetchLatestArtifactMetadata(
35 29
36 return !artifact ? null : { 30 return !artifact ? null : {
37 releaseName: response.name, 31 releaseName: response.name,
38 releaseDate: new Date(response.published_at),
39 downloadUrl: artifact.browser_download_url 32 downloadUrl: artifact.browser_download_url
40 }; 33 };
41 34
42 // Noise denotes tremendous amount of data that we are not using here 35 // Noise denotes tremendous amount of data that we are not using here
43 interface GithubRelease { 36 interface GithubRelease {
44 name: string; 37 name: string;
45 published_at: Date;
46 assets: Array<{ 38 assets: Array<{
47 browser_download_url: string; 39 browser_download_url: string;
48 40
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
new file mode 100644
index 000000000..f54e24e26
--- /dev/null
+++ b/editors/code/src/installation/interfaces.ts
@@ -0,0 +1,26 @@
1export interface GithubRepo {
2 name: string;
3 owner: string;
4}
5
6export interface ArtifactMetadata {
7 releaseName: string;
8 downloadUrl: string;
9}
10
11
12export enum BinarySourceType { ExplicitPath, GithubBinary }
13
14export type BinarySource = EplicitPathSource | GithubBinarySource;
15
16export interface EplicitPathSource {
17 type: BinarySourceType.ExplicitPath;
18 path: string;
19}
20
21export interface GithubBinarySource {
22 type: BinarySourceType.GithubBinary;
23 repo: GithubRepo;
24 dir: string;
25 file: string;
26}
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts
new file mode 100644
index 000000000..2b3ce6621
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,119 @@
1import { unwrapNotNil } from "ts-not-nil";
2import { spawnSync } from "child_process";
3import * as vscode from "vscode";
4import * as path from "path";
5import { strict as assert } from "assert";
6import { promises as fs } from "fs";
7
8import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces";
9import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
10import { downloadFile } from "./download_file";
11
12export async function downloadLatestLanguageServer(
13 {file: artifactFileName, dir: installationDir, repo}: GithubBinarySource
14) {
15 const binaryMetadata = await fetchLatestArtifactMetadata({ artifactFileName, repo });
16
17 const {
18 releaseName,
19 downloadUrl
20 } = unwrapNotNil(binaryMetadata, `Latest GitHub release lacks "${artifactFileName}" file`);
21
22 await fs.mkdir(installationDir).catch(err => assert.strictEqual(
23 err && err.code,
24 "EEXIST",
25 `Couldn't create directory "${installationDir}" to download `+
26 `language server binary: ${err.message}`
27 ));
28
29 const installationPath = path.join(installationDir, artifactFileName);
30
31 await vscode.window.withProgress(
32 {
33 location: vscode.ProgressLocation.Notification,
34 cancellable: false, // FIXME: add support for canceling download?
35 title: `Downloading language server ${releaseName}`
36 },
37 async (progress, _) => {
38 let lastPrecentage = 0;
39 await downloadFile(downloadUrl, installationPath, (readBytes, totalBytes) => {
40 const newPercentage = (readBytes / totalBytes) * 100;
41 progress.report({
42 message: newPercentage.toFixed(0) + "%",
43 increment: newPercentage - lastPrecentage
44 });
45
46 lastPrecentage = newPercentage;
47 });
48 }
49 );
50
51 await fs.chmod(installationPath, 111); // Set xxx permissions
52}
53export async function ensureLanguageServerBinary(
54 langServerSource: null | BinarySource
55): Promise<null | string> {
56
57 if (!langServerSource) {
58 vscode.window.showErrorMessage(
59 "Unfortunately we don't ship binaries for your platform yet. " +
60 "You need to manually clone rust-analyzer repository and " +
61 "run `cargo xtask install --server` to build the language server from sources. " +
62 "If you feel that your platform should be supported, please create an issue " +
63 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
64 "will consider it."
65 );
66 return null;
67 }
68
69 switch (langServerSource.type) {
70 case BinarySourceType.ExplicitPath: {
71 if (isBinaryAvailable(langServerSource.path)) {
72 return langServerSource.path;
73 }
74 vscode.window.showErrorMessage(
75 `Unable to execute ${'`'}${langServerSource.path} --version${'`'}. ` +
76 "To use the bundled language server, set `rust-analyzer.raLspServerPath` " +
77 "value to `null` or remove it from the settings to use it by default."
78 );
79 return null;
80 }
81 case BinarySourceType.GithubBinary: {
82 const bundledBinaryPath = path.join(langServerSource.dir, langServerSource.file);
83
84 if (!isBinaryAvailable(bundledBinaryPath)) {
85 const userResponse = await vscode.window.showInformationMessage(
86 `Language server binary for rust-analyzer was not found. ` +
87 `Do you want to download it now?`,
88 "Download now", "Cancel"
89 );
90 if (userResponse !== "Download now") return null;
91
92 try {
93 await downloadLatestLanguageServer(langServerSource);
94 } catch (err) {
95 await vscode.window.showErrorMessage(
96 `Failed to download language server from ${langServerSource.repo.name} ` +
97 `GitHub repository: ${err.message}`
98 );
99 return null;
100 }
101
102
103 assert(
104 isBinaryAvailable(bundledBinaryPath),
105 "Downloaded language server binary is not functional"
106 );
107
108 vscode.window.showInformationMessage(
109 "Rust analyzer language server was successfully installed"
110 );
111 }
112 return bundledBinaryPath;
113 }
114 }
115
116 function isBinaryAvailable(binaryPath: string) {
117 return spawnSync(binaryPath, ["--version"]).status === 0;
118 }
119}