diff options
Diffstat (limited to 'editors/code')
-rw-r--r-- | editors/code/package-lock.json | 20 | ||||
-rw-r--r-- | editors/code/package.json | 39 | ||||
-rw-r--r-- | editors/code/rollup.config.js | 3 | ||||
-rw-r--r-- | editors/code/src/client.ts | 32 | ||||
-rw-r--r-- | editors/code/src/config.ts | 299 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 16 | ||||
-rw-r--r-- | editors/code/src/inlay_hints.ts | 51 | ||||
-rw-r--r-- | editors/code/src/installation/download_artifact.ts | 58 | ||||
-rw-r--r-- | editors/code/src/installation/download_file.ts | 43 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_artifact_release_info.ts (renamed from editors/code/src/installation/fetch_latest_artifact_metadata.ts) | 20 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 15 | ||||
-rw-r--r-- | editors/code/src/installation/language_server.ts | 141 | ||||
-rw-r--r-- | editors/code/src/installation/server.ts | 124 | ||||
-rw-r--r-- | editors/code/src/status_display.ts | 4 |
14 files changed, 443 insertions, 422 deletions
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 5c056463e..c74078735 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json | |||
@@ -1,6 +1,6 @@ | |||
1 | { | 1 | { |
2 | "name": "rust-analyzer", | 2 | "name": "rust-analyzer", |
3 | "version": "0.1.0", | 3 | "version": "0.2.0-dev", |
4 | "lockfileVersion": 1, | 4 | "lockfileVersion": 1, |
5 | "requires": true, | 5 | "requires": true, |
6 | "dependencies": { | 6 | "dependencies": { |
@@ -107,9 +107,9 @@ | |||
107 | "dev": true | 107 | "dev": true |
108 | }, | 108 | }, |
109 | "@types/vscode": { | 109 | "@types/vscode": { |
110 | "version": "1.41.0", | 110 | "version": "1.42.0", |
111 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.41.0.tgz", | 111 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.42.0.tgz", |
112 | "integrity": "sha512-7SfeY5u9jgiELwxyLB3z7l6l/GbN9CqpCQGkcRlB7tKRFBxzbz2PoBfGrLxI1vRfUCIq5+hg5vtDHExwq5j3+A==", | 112 | "integrity": "sha512-ds6TceMsh77Fs0Mq0Vap6Y72JbGWB8Bay4DrnJlf5d9ui2RSe1wis13oQm+XhguOeH1HUfLGzaDAoupTUtgabw==", |
113 | "dev": true | 113 | "dev": true |
114 | }, | 114 | }, |
115 | "acorn": { | 115 | "acorn": { |
@@ -662,9 +662,9 @@ | |||
662 | } | 662 | } |
663 | }, | 663 | }, |
664 | "readable-stream": { | 664 | "readable-stream": { |
665 | "version": "3.4.0", | 665 | "version": "3.6.0", |
666 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", | 666 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", |
667 | "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", | 667 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", |
668 | "dev": true, | 668 | "dev": true, |
669 | "requires": { | 669 | "requires": { |
670 | "inherits": "^2.0.3", | 670 | "inherits": "^2.0.3", |
@@ -860,9 +860,9 @@ | |||
860 | "dev": true | 860 | "dev": true |
861 | }, | 861 | }, |
862 | "vsce": { | 862 | "vsce": { |
863 | "version": "1.71.0", | 863 | "version": "1.73.0", |
864 | "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.71.0.tgz", | 864 | "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.73.0.tgz", |
865 | "integrity": "sha512-7k+LPC4oJYPyyxs0a5nh4A8CleQ6+2EMPiAiX/bDyN+PmwJFm2FFPqLRxdIsIWfFnkW4ZMQBf10+W62dCRd9kQ==", | 865 | "integrity": "sha512-6W37Ebbkj3uF3WhT+SCfRtsneRQEFcGvf/XYz+b6OAgDCj4gPurWyDVrqw/HLsbP1WflGIyUfVZ8t5M7kQp6Uw==", |
866 | "dev": true, | 866 | "dev": true, |
867 | "requires": { | 867 | "requires": { |
868 | "azure-devops-node-api": "^7.2.0", | 868 | "azure-devops-node-api": "^7.2.0", |
diff --git a/editors/code/package.json b/editors/code/package.json index f687eb8d4..46acbfe76 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -5,7 +5,8 @@ | |||
5 | "preview": true, | 5 | "preview": true, |
6 | "private": true, | 6 | "private": true, |
7 | "icon": "icon.png", | 7 | "icon": "icon.png", |
8 | "version": "0.1.0", | 8 | "//": "The real version is in release.yaml, this one just needs to be bigger", |
9 | "version": "0.2.20200211-dev", | ||
9 | "publisher": "matklad", | 10 | "publisher": "matklad", |
10 | "repository": { | 11 | "repository": { |
11 | "url": "https://github.com/rust-analyzer/rust-analyzer.git", | 12 | "url": "https://github.com/rust-analyzer/rust-analyzer.git", |
@@ -15,7 +16,7 @@ | |||
15 | "Other" | 16 | "Other" |
16 | ], | 17 | ], |
17 | "engines": { | 18 | "engines": { |
18 | "vscode": "^1.41.0" | 19 | "vscode": "^1.42.0" |
19 | }, | 20 | }, |
20 | "scripts": { | 21 | "scripts": { |
21 | "vscode:prepublish": "tsc && rollup -c", | 22 | "vscode:prepublish": "tsc && rollup -c", |
@@ -35,13 +36,13 @@ | |||
35 | "@types/node": "^12.12.25", | 36 | "@types/node": "^12.12.25", |
36 | "@types/node-fetch": "^2.5.4", | 37 | "@types/node-fetch": "^2.5.4", |
37 | "@types/throttle-debounce": "^2.1.0", | 38 | "@types/throttle-debounce": "^2.1.0", |
38 | "@types/vscode": "^1.41.0", | 39 | "@types/vscode": "^1.42.0", |
39 | "rollup": "^1.31.0", | 40 | "rollup": "^1.31.0", |
40 | "tslib": "^1.10.0", | 41 | "tslib": "^1.10.0", |
41 | "tslint": "^5.20.1", | 42 | "tslint": "^5.20.1", |
42 | "typescript": "^3.7.5", | 43 | "typescript": "^3.7.5", |
43 | "typescript-formatter": "^7.2.2", | 44 | "typescript-formatter": "^7.2.2", |
44 | "vsce": "^1.71.0" | 45 | "vsce": "^1.73.0" |
45 | }, | 46 | }, |
46 | "activationEvents": [ | 47 | "activationEvents": [ |
47 | "onLanguage:rust", | 48 | "onLanguage:rust", |
@@ -181,9 +182,20 @@ | |||
181 | }, | 182 | }, |
182 | "rust-analyzer.excludeGlobs": { | 183 | "rust-analyzer.excludeGlobs": { |
183 | "type": "array", | 184 | "type": "array", |
185 | "items": { | ||
186 | "type": "string" | ||
187 | }, | ||
184 | "default": [], | 188 | "default": [], |
185 | "description": "Paths to exclude from analysis" | 189 | "description": "Paths to exclude from analysis" |
186 | }, | 190 | }, |
191 | "rust-analyzer.rustfmtArgs": { | ||
192 | "type": "array", | ||
193 | "items": { | ||
194 | "type": "string" | ||
195 | }, | ||
196 | "default": [], | ||
197 | "description": "Additional arguments to rustfmt" | ||
198 | }, | ||
187 | "rust-analyzer.useClientWatching": { | 199 | "rust-analyzer.useClientWatching": { |
188 | "type": "boolean", | 200 | "type": "boolean", |
189 | "default": true, | 201 | "default": true, |
@@ -196,6 +208,9 @@ | |||
196 | }, | 208 | }, |
197 | "rust-analyzer.cargo-watch.arguments": { | 209 | "rust-analyzer.cargo-watch.arguments": { |
198 | "type": "array", | 210 | "type": "array", |
211 | "items": { | ||
212 | "type": "string" | ||
213 | }, | ||
199 | "description": "`cargo-watch` arguments. (e.g: `--features=\"shumway,pdf\"` will run as `cargo watch -x \"check --features=\"shumway,pdf\"\"` )", | 214 | "description": "`cargo-watch` arguments. (e.g: `--features=\"shumway,pdf\"` will run as `cargo watch -x \"check --features=\"shumway,pdf\"\"` )", |
200 | "default": [] | 215 | "default": [] |
201 | }, | 216 | }, |
@@ -227,10 +242,12 @@ | |||
227 | }, | 242 | }, |
228 | "rust-analyzer.lruCapacity": { | 243 | "rust-analyzer.lruCapacity": { |
229 | "type": [ | 244 | "type": [ |
230 | "number", | 245 | "null", |
231 | "null" | 246 | "integer" |
232 | ], | 247 | ], |
233 | "default": null, | 248 | "default": null, |
249 | "minimum": 0, | ||
250 | "exclusiveMinimum": true, | ||
234 | "description": "Number of syntax trees rust-analyzer keeps in memory" | 251 | "description": "Number of syntax trees rust-analyzer keeps in memory" |
235 | }, | 252 | }, |
236 | "rust-analyzer.displayInlayHints": { | 253 | "rust-analyzer.displayInlayHints": { |
@@ -239,8 +256,13 @@ | |||
239 | "description": "Display additional type and parameter information in the editor" | 256 | "description": "Display additional type and parameter information in the editor" |
240 | }, | 257 | }, |
241 | "rust-analyzer.maxInlayHintLength": { | 258 | "rust-analyzer.maxInlayHintLength": { |
242 | "type": "number", | 259 | "type": [ |
260 | "null", | ||
261 | "integer" | ||
262 | ], | ||
243 | "default": 20, | 263 | "default": 20, |
264 | "minimum": 0, | ||
265 | "exclusiveMinimum": true, | ||
244 | "description": "Maximum length for inlay hints" | 266 | "description": "Maximum length for inlay hints" |
245 | }, | 267 | }, |
246 | "rust-analyzer.cargoFeatures.noDefaultFeatures": { | 268 | "rust-analyzer.cargoFeatures.noDefaultFeatures": { |
@@ -255,6 +277,9 @@ | |||
255 | }, | 277 | }, |
256 | "rust-analyzer.cargoFeatures.features": { | 278 | "rust-analyzer.cargoFeatures.features": { |
257 | "type": "array", | 279 | "type": "array", |
280 | "items": { | ||
281 | "type": "string" | ||
282 | }, | ||
258 | "default": [], | 283 | "default": [], |
259 | "description": "List of features to activate" | 284 | "description": "List of features to activate" |
260 | } | 285 | } |
diff --git a/editors/code/rollup.config.js b/editors/code/rollup.config.js index f8d320f46..337385a24 100644 --- a/editors/code/rollup.config.js +++ b/editors/code/rollup.config.js | |||
@@ -18,6 +18,7 @@ export default { | |||
18 | external: [...nodeBuiltins, 'vscode'], | 18 | external: [...nodeBuiltins, 'vscode'], |
19 | output: { | 19 | output: { |
20 | file: './out/main.js', | 20 | file: './out/main.js', |
21 | format: 'cjs' | 21 | format: 'cjs', |
22 | exports: 'named' | ||
22 | } | 23 | } |
23 | }; | 24 | }; |
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 2e3d4aba2..11894973c 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -1,44 +1,48 @@ | |||
1 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
2 | import * as vscode from 'vscode'; | ||
2 | 3 | ||
3 | import { window, workspace } from 'vscode'; | ||
4 | import { Config } from './config'; | 4 | import { Config } from './config'; |
5 | import { ensureLanguageServerBinary } from './installation/language_server'; | 5 | import { ensureServerBinary } from './installation/server'; |
6 | import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; | ||
6 | 7 | ||
7 | export async function createClient(config: Config): Promise<null | lc.LanguageClient> { | 8 | export async function createClient(config: Config): Promise<null | lc.LanguageClient> { |
8 | // '.' Is the fallback if no folder is open | 9 | // '.' Is the fallback if no folder is open |
9 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). | 10 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). |
10 | // It might be a good idea to test if the uri points to a file. | 11 | // It might be a good idea to test if the uri points to a file. |
11 | const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; | 12 | const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; |
12 | 13 | ||
13 | const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource); | 14 | const serverPath = await ensureServerBinary(config.serverSource); |
14 | if (!raLspServerPath) return null; | 15 | if (!serverPath) return null; |
15 | 16 | ||
16 | const run: lc.Executable = { | 17 | const run: lc.Executable = { |
17 | command: raLspServerPath, | 18 | command: serverPath, |
18 | options: { cwd: workspaceFolderPath }, | 19 | options: { cwd: workspaceFolderPath }, |
19 | }; | 20 | }; |
20 | const serverOptions: lc.ServerOptions = { | 21 | const serverOptions: lc.ServerOptions = { |
21 | run, | 22 | run, |
22 | debug: run, | 23 | debug: run, |
23 | }; | 24 | }; |
24 | const traceOutputChannel = window.createOutputChannel( | 25 | const traceOutputChannel = vscode.window.createOutputChannel( |
25 | 'Rust Analyzer Language Server Trace', | 26 | 'Rust Analyzer Language Server Trace', |
26 | ); | 27 | ); |
28 | const cargoWatchOpts = config.cargoWatchOptions; | ||
29 | |||
27 | const clientOptions: lc.LanguageClientOptions = { | 30 | const clientOptions: lc.LanguageClientOptions = { |
28 | documentSelector: [{ scheme: 'file', language: 'rust' }], | 31 | documentSelector: [{ scheme: 'file', language: 'rust' }], |
29 | initializationOptions: { | 32 | initializationOptions: { |
30 | publishDecorations: true, | 33 | publishDecorations: true, |
31 | lruCapacity: config.lruCapacity, | 34 | lruCapacity: config.lruCapacity, |
32 | maxInlayHintLength: config.maxInlayHintLength, | 35 | maxInlayHintLength: config.maxInlayHintLength, |
33 | cargoWatchEnable: config.cargoWatchOptions.enable, | 36 | cargoWatchEnable: cargoWatchOpts.enable, |
34 | cargoWatchArgs: config.cargoWatchOptions.arguments, | 37 | cargoWatchArgs: cargoWatchOpts.arguments, |
35 | cargoWatchCommand: config.cargoWatchOptions.command, | 38 | cargoWatchCommand: cargoWatchOpts.command, |
36 | cargoWatchAllTargets: config.cargoWatchOptions.allTargets, | 39 | cargoWatchAllTargets: cargoWatchOpts.allTargets, |
37 | excludeGlobs: config.excludeGlobs, | 40 | excludeGlobs: config.excludeGlobs, |
38 | useClientWatching: config.useClientWatching, | 41 | useClientWatching: config.useClientWatching, |
39 | featureFlags: config.featureFlags, | 42 | featureFlags: config.featureFlags, |
40 | withSysroot: config.withSysroot, | 43 | withSysroot: config.withSysroot, |
41 | cargoFeatures: config.cargoFeatures, | 44 | cargoFeatures: config.cargoFeatures, |
45 | rustfmtArgs: config.rustfmtArgs, | ||
42 | }, | 46 | }, |
43 | traceOutputChannel, | 47 | traceOutputChannel, |
44 | }; | 48 | }; |
@@ -78,6 +82,10 @@ export async function createClient(config: Config): Promise<null | lc.LanguageCl | |||
78 | } | 82 | } |
79 | }, | 83 | }, |
80 | }; | 84 | }; |
81 | res.registerProposedFeatures(); | 85 | |
86 | // To turn on all proposed features use: res.registerProposedFeatures(); | ||
87 | // Here we want to just enable CallHierarchyFeature since it is available on stable. | ||
88 | // Note that while the CallHierarchyFeature is stable the LSP protocol is not. | ||
89 | res.registerFeature(new CallHierarchyFeature(res)); | ||
82 | return res; | 90 | return res; |
83 | } | 91 | } |
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index d5f3da2ed..c3fa788c7 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -16,45 +16,61 @@ export interface CargoFeatures { | |||
16 | allFeatures: boolean; | 16 | allFeatures: boolean; |
17 | features: string[]; | 17 | features: string[]; |
18 | } | 18 | } |
19 | |||
20 | export class Config { | 19 | export class Config { |
21 | langServerSource!: null | BinarySource; | 20 | private static readonly rootSection = "rust-analyzer"; |
21 | private static readonly requiresReloadOpts = [ | ||
22 | "cargoFeatures", | ||
23 | "cargo-watch", | ||
24 | ] | ||
25 | .map(opt => `${Config.rootSection}.${opt}`); | ||
26 | |||
27 | private static readonly extensionVersion: string = (() => { | ||
28 | const packageJsonVersion = vscode | ||
29 | .extensions | ||
30 | .getExtension("matklad.rust-analyzer")! | ||
31 | .packageJSON | ||
32 | .version as string; // n.n.YYYYMMDD | ||
33 | |||
34 | const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; | ||
35 | const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; | ||
36 | |||
37 | return `${yyyy}-${mm}-${dd}`; | ||
38 | })(); | ||
39 | |||
40 | private cfg!: vscode.WorkspaceConfiguration; | ||
41 | |||
42 | constructor(private readonly ctx: vscode.ExtensionContext) { | ||
43 | vscode.workspace.onDidChangeConfiguration(this.onConfigChange, this, ctx.subscriptions); | ||
44 | this.refreshConfig(); | ||
45 | } | ||
22 | 46 | ||
23 | highlightingOn = true; | 47 | private refreshConfig() { |
24 | rainbowHighlightingOn = false; | 48 | this.cfg = vscode.workspace.getConfiguration(Config.rootSection); |
25 | enableEnhancedTyping = true; | 49 | console.log("Using configuration:", this.cfg); |
26 | lruCapacity: null | number = null; | 50 | } |
27 | displayInlayHints = true; | ||
28 | maxInlayHintLength: null | number = null; | ||
29 | excludeGlobs: string[] = []; | ||
30 | useClientWatching = true; | ||
31 | featureFlags: Record<string, boolean> = {}; | ||
32 | // for internal use | ||
33 | withSysroot: null | boolean = null; | ||
34 | cargoWatchOptions: CargoWatchOptions = { | ||
35 | enable: true, | ||
36 | arguments: [], | ||
37 | command: '', | ||
38 | allTargets: true, | ||
39 | }; | ||
40 | cargoFeatures: CargoFeatures = { | ||
41 | noDefaultFeatures: false, | ||
42 | allFeatures: true, | ||
43 | features: [], | ||
44 | }; | ||
45 | 51 | ||
46 | private prevEnhancedTyping: null | boolean = null; | 52 | private async onConfigChange(event: vscode.ConfigurationChangeEvent) { |
47 | private prevCargoFeatures: null | CargoFeatures = null; | 53 | this.refreshConfig(); |
48 | private prevCargoWatchOptions: null | CargoWatchOptions = null; | ||
49 | 54 | ||
50 | constructor(ctx: vscode.ExtensionContext) { | 55 | const requiresReloadOpt = Config.requiresReloadOpts.find( |
51 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions); | 56 | opt => event.affectsConfiguration(opt) |
52 | this.refresh(ctx); | 57 | ); |
58 | |||
59 | if (!requiresReloadOpt) return; | ||
60 | |||
61 | const userResponse = await vscode.window.showInformationMessage( | ||
62 | `Changing "${requiresReloadOpt}" requires a reload`, | ||
63 | "Reload now" | ||
64 | ); | ||
65 | |||
66 | if (userResponse === "Reload now") { | ||
67 | vscode.commands.executeCommand("workbench.action.reloadWindow"); | ||
68 | } | ||
53 | } | 69 | } |
54 | 70 | ||
55 | private static expandPathResolving(path: string) { | 71 | private static replaceTildeWithHomeDir(path: string) { |
56 | if (path.startsWith('~/')) { | 72 | if (path.startsWith("~/")) { |
57 | return path.replace('~', os.homedir()); | 73 | return os.homedir() + path.slice("~".length); |
58 | } | 74 | } |
59 | return path; | 75 | return path; |
60 | } | 76 | } |
@@ -64,9 +80,21 @@ export class Config { | |||
64 | * `platform` on GitHub releases. (It is also stored under the same name when | 80 | * `platform` on GitHub releases. (It is also stored under the same name when |
65 | * downloaded by the extension). | 81 | * downloaded by the extension). |
66 | */ | 82 | */ |
67 | private static prebuiltLangServerFileName(platform: NodeJS.Platform): null | string { | 83 | get prebuiltServerFileName(): null | string { |
68 | switch (platform) { | 84 | // See possible `arch` values here: |
69 | case "linux": return "ra_lsp_server-linux"; | 85 | // https://nodejs.org/api/process.html#process_process_arch |
86 | |||
87 | switch (process.platform) { | ||
88 | |||
89 | case "linux": { | ||
90 | switch (process.arch) { | ||
91 | case "arm": | ||
92 | case "arm64": return null; | ||
93 | |||
94 | default: return "ra_lsp_server-linux"; | ||
95 | } | ||
96 | } | ||
97 | |||
70 | case "darwin": return "ra_lsp_server-mac"; | 98 | case "darwin": return "ra_lsp_server-mac"; |
71 | case "win32": return "ra_lsp_server-windows.exe"; | 99 | case "win32": return "ra_lsp_server-windows.exe"; |
72 | 100 | ||
@@ -82,27 +110,26 @@ export class Config { | |||
82 | } | 110 | } |
83 | } | 111 | } |
84 | 112 | ||
85 | private static langServerBinarySource( | 113 | get serverSource(): null | BinarySource { |
86 | ctx: vscode.ExtensionContext, | 114 | const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("raLspServerPath"); |
87 | config: vscode.WorkspaceConfiguration | ||
88 | ): null | BinarySource { | ||
89 | const langServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath"); | ||
90 | 115 | ||
91 | if (langServerPath) { | 116 | if (serverPath) { |
92 | return { | 117 | return { |
93 | type: BinarySource.Type.ExplicitPath, | 118 | type: BinarySource.Type.ExplicitPath, |
94 | path: Config.expandPathResolving(langServerPath) | 119 | path: Config.replaceTildeWithHomeDir(serverPath) |
95 | }; | 120 | }; |
96 | } | 121 | } |
97 | 122 | ||
98 | const prebuiltBinaryName = Config.prebuiltLangServerFileName(process.platform); | 123 | const prebuiltBinaryName = this.prebuiltServerFileName; |
99 | 124 | ||
100 | if (!prebuiltBinaryName) return null; | 125 | if (!prebuiltBinaryName) return null; |
101 | 126 | ||
102 | return { | 127 | return { |
103 | type: BinarySource.Type.GithubRelease, | 128 | type: BinarySource.Type.GithubRelease, |
104 | dir: ctx.globalStoragePath, | 129 | dir: this.ctx.globalStoragePath, |
105 | file: prebuiltBinaryName, | 130 | file: prebuiltBinaryName, |
131 | storage: this.ctx.globalState, | ||
132 | version: Config.extensionVersion, | ||
106 | repo: { | 133 | repo: { |
107 | name: "rust-analyzer", | 134 | name: "rust-analyzer", |
108 | owner: "rust-analyzer", | 135 | owner: "rust-analyzer", |
@@ -110,158 +137,36 @@ export class Config { | |||
110 | }; | 137 | }; |
111 | } | 138 | } |
112 | 139 | ||
140 | // We don't do runtime config validation here for simplicity. More on stackoverflow: | ||
141 | // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension | ||
142 | |||
143 | get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } | ||
144 | get rainbowHighlightingOn() { return this.cfg.get("rainbowHighlightingOn") as boolean; } | ||
145 | get lruCapacity() { return this.cfg.get("lruCapacity") as null | number; } | ||
146 | get displayInlayHints() { return this.cfg.get("displayInlayHints") as boolean; } | ||
147 | get maxInlayHintLength() { return this.cfg.get("maxInlayHintLength") as number; } | ||
148 | get excludeGlobs() { return this.cfg.get("excludeGlobs") as string[]; } | ||
149 | get useClientWatching() { return this.cfg.get("useClientWatching") as boolean; } | ||
150 | get featureFlags() { return this.cfg.get("featureFlags") as Record<string, boolean>; } | ||
151 | get rustfmtArgs() { return this.cfg.get("rustfmtArgs") as string[]; } | ||
152 | |||
153 | get cargoWatchOptions(): CargoWatchOptions { | ||
154 | return { | ||
155 | enable: this.cfg.get("cargo-watch.enable") as boolean, | ||
156 | arguments: this.cfg.get("cargo-watch.arguments") as string[], | ||
157 | allTargets: this.cfg.get("cargo-watch.allTargets") as boolean, | ||
158 | command: this.cfg.get("cargo-watch.command") as string, | ||
159 | }; | ||
160 | } | ||
113 | 161 | ||
114 | // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default | 162 | get cargoFeatures(): CargoFeatures { |
115 | // values only in one place (i.e. remove default values from non-readonly members declarations) | 163 | return { |
116 | private refresh(ctx: vscode.ExtensionContext) { | 164 | noDefaultFeatures: this.cfg.get("cargoFeatures.noDefaultFeatures") as boolean, |
117 | const config = vscode.workspace.getConfiguration('rust-analyzer'); | 165 | allFeatures: this.cfg.get("cargoFeatures.allFeatures") as boolean, |
118 | 166 | features: this.cfg.get("cargoFeatures.features") as string[], | |
119 | let requireReloadMessage = null; | 167 | }; |
120 | |||
121 | if (config.has('highlightingOn')) { | ||
122 | this.highlightingOn = config.get('highlightingOn') as boolean; | ||
123 | } | ||
124 | |||
125 | if (config.has('rainbowHighlightingOn')) { | ||
126 | this.rainbowHighlightingOn = config.get( | ||
127 | 'rainbowHighlightingOn', | ||
128 | ) as boolean; | ||
129 | } | ||
130 | |||
131 | if (config.has('enableEnhancedTyping')) { | ||
132 | this.enableEnhancedTyping = config.get( | ||
133 | 'enableEnhancedTyping', | ||
134 | ) as boolean; | ||
135 | |||
136 | if (this.prevEnhancedTyping === null) { | ||
137 | this.prevEnhancedTyping = this.enableEnhancedTyping; | ||
138 | } | ||
139 | } else if (this.prevEnhancedTyping === null) { | ||
140 | this.prevEnhancedTyping = this.enableEnhancedTyping; | ||
141 | } | ||
142 | |||
143 | if (this.prevEnhancedTyping !== this.enableEnhancedTyping) { | ||
144 | requireReloadMessage = | ||
145 | 'Changing enhanced typing setting requires a reload'; | ||
146 | this.prevEnhancedTyping = this.enableEnhancedTyping; | ||
147 | } | ||
148 | |||
149 | this.langServerSource = Config.langServerBinarySource(ctx, config); | ||
150 | |||
151 | if (config.has('cargo-watch.enable')) { | ||
152 | this.cargoWatchOptions.enable = config.get<boolean>( | ||
153 | 'cargo-watch.enable', | ||
154 | true, | ||
155 | ); | ||
156 | } | ||
157 | |||
158 | if (config.has('cargo-watch.arguments')) { | ||
159 | this.cargoWatchOptions.arguments = config.get<string[]>( | ||
160 | 'cargo-watch.arguments', | ||
161 | [], | ||
162 | ); | ||
163 | } | ||
164 | |||
165 | if (config.has('cargo-watch.command')) { | ||
166 | this.cargoWatchOptions.command = config.get<string>( | ||
167 | 'cargo-watch.command', | ||
168 | '', | ||
169 | ); | ||
170 | } | ||
171 | |||
172 | if (config.has('cargo-watch.allTargets')) { | ||
173 | this.cargoWatchOptions.allTargets = config.get<boolean>( | ||
174 | 'cargo-watch.allTargets', | ||
175 | true, | ||
176 | ); | ||
177 | } | ||
178 | |||
179 | if (config.has('lruCapacity')) { | ||
180 | this.lruCapacity = config.get('lruCapacity') as number; | ||
181 | } | ||
182 | |||
183 | if (config.has('displayInlayHints')) { | ||
184 | this.displayInlayHints = config.get('displayInlayHints') as boolean; | ||
185 | } | ||
186 | if (config.has('maxInlayHintLength')) { | ||
187 | this.maxInlayHintLength = config.get( | ||
188 | 'maxInlayHintLength', | ||
189 | ) as number; | ||
190 | } | ||
191 | if (config.has('excludeGlobs')) { | ||
192 | this.excludeGlobs = config.get('excludeGlobs') || []; | ||
193 | } | ||
194 | if (config.has('useClientWatching')) { | ||
195 | this.useClientWatching = config.get('useClientWatching') || true; | ||
196 | } | ||
197 | if (config.has('featureFlags')) { | ||
198 | this.featureFlags = config.get('featureFlags') || {}; | ||
199 | } | ||
200 | if (config.has('withSysroot')) { | ||
201 | this.withSysroot = config.get('withSysroot') || false; | ||
202 | } | ||
203 | |||
204 | if (config.has('cargoFeatures.noDefaultFeatures')) { | ||
205 | this.cargoFeatures.noDefaultFeatures = config.get( | ||
206 | 'cargoFeatures.noDefaultFeatures', | ||
207 | false, | ||
208 | ); | ||
209 | } | ||
210 | if (config.has('cargoFeatures.allFeatures')) { | ||
211 | this.cargoFeatures.allFeatures = config.get( | ||
212 | 'cargoFeatures.allFeatures', | ||
213 | true, | ||
214 | ); | ||
215 | } | ||
216 | if (config.has('cargoFeatures.features')) { | ||
217 | this.cargoFeatures.features = config.get( | ||
218 | 'cargoFeatures.features', | ||
219 | [], | ||
220 | ); | ||
221 | } | ||
222 | |||
223 | if ( | ||
224 | this.prevCargoFeatures !== null && | ||
225 | (this.cargoFeatures.allFeatures !== | ||
226 | this.prevCargoFeatures.allFeatures || | ||
227 | this.cargoFeatures.noDefaultFeatures !== | ||
228 | this.prevCargoFeatures.noDefaultFeatures || | ||
229 | this.cargoFeatures.features.length !== | ||
230 | this.prevCargoFeatures.features.length || | ||
231 | this.cargoFeatures.features.some( | ||
232 | (v, i) => v !== this.prevCargoFeatures!.features[i], | ||
233 | )) | ||
234 | ) { | ||
235 | requireReloadMessage = 'Changing cargo features requires a reload'; | ||
236 | } | ||
237 | this.prevCargoFeatures = { ...this.cargoFeatures }; | ||
238 | |||
239 | if (this.prevCargoWatchOptions !== null) { | ||
240 | const changed = | ||
241 | this.cargoWatchOptions.enable !== this.prevCargoWatchOptions.enable || | ||
242 | this.cargoWatchOptions.command !== this.prevCargoWatchOptions.command || | ||
243 | this.cargoWatchOptions.allTargets !== this.prevCargoWatchOptions.allTargets || | ||
244 | this.cargoWatchOptions.arguments.length !== this.prevCargoWatchOptions.arguments.length || | ||
245 | this.cargoWatchOptions.arguments.some( | ||
246 | (v, i) => v !== this.prevCargoWatchOptions!.arguments[i], | ||
247 | ); | ||
248 | if (changed) { | ||
249 | requireReloadMessage = 'Changing cargo-watch options requires a reload'; | ||
250 | } | ||
251 | } | ||
252 | this.prevCargoWatchOptions = { ...this.cargoWatchOptions }; | ||
253 | |||
254 | if (requireReloadMessage !== null) { | ||
255 | const reloadAction = 'Reload now'; | ||
256 | vscode.window | ||
257 | .showInformationMessage(requireReloadMessage, reloadAction) | ||
258 | .then(selectedAction => { | ||
259 | if (selectedAction === reloadAction) { | ||
260 | vscode.commands.executeCommand( | ||
261 | 'workbench.action.reloadWindow', | ||
262 | ); | ||
263 | } | ||
264 | }); | ||
265 | } | ||
266 | } | 168 | } |
169 | |||
170 | // for internal use | ||
171 | get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } | ||
267 | } | 172 | } |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 70042a479..ff6245f78 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts | |||
@@ -60,6 +60,10 @@ export class Ctx { | |||
60 | this.pushCleanup(d); | 60 | this.pushCleanup(d); |
61 | } | 61 | } |
62 | 62 | ||
63 | get globalState(): vscode.Memento { | ||
64 | return this.extCtx.globalState; | ||
65 | } | ||
66 | |||
63 | get subscriptions(): Disposable[] { | 67 | get subscriptions(): Disposable[] { |
64 | return this.extCtx.subscriptions; | 68 | return this.extCtx.subscriptions; |
65 | } | 69 | } |
@@ -87,15 +91,11 @@ export async function sendRequestWithRetry<R>( | |||
87 | for (const delay of [2, 4, 6, 8, 10, null]) { | 91 | for (const delay of [2, 4, 6, 8, 10, null]) { |
88 | try { | 92 | try { |
89 | return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param)); | 93 | return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param)); |
90 | } catch (e) { | 94 | } catch (err) { |
91 | if ( | 95 | if (delay === null || err.code !== lc.ErrorCodes.ContentModified) { |
92 | e.code === lc.ErrorCodes.ContentModified && | 96 | throw err; |
93 | delay !== null | ||
94 | ) { | ||
95 | await sleep(10 * (1 << delay)); | ||
96 | continue; | ||
97 | } | 97 | } |
98 | throw e; | 98 | await sleep(10 * (1 << delay)); |
99 | } | 99 | } |
100 | } | 100 | } |
101 | throw 'unreachable'; | 101 | throw 'unreachable'; |
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 1c019a51b..3896878cd 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts | |||
@@ -13,7 +13,7 @@ export function activateInlayHints(ctx: Ctx) { | |||
13 | 13 | ||
14 | vscode.workspace.onDidChangeTextDocument( | 14 | vscode.workspace.onDidChangeTextDocument( |
15 | async event => { | 15 | async event => { |
16 | if (event.contentChanges.length !== 0) return; | 16 | if (event.contentChanges.length === 0) return; |
17 | if (event.document.languageId !== 'rust') return; | 17 | if (event.document.languageId !== 'rust') return; |
18 | await hintsUpdater.refresh(); | 18 | await hintsUpdater.refresh(); |
19 | }, | 19 | }, |
@@ -27,7 +27,9 @@ export function activateInlayHints(ctx: Ctx) { | |||
27 | ctx.subscriptions | 27 | ctx.subscriptions |
28 | ); | 28 | ); |
29 | 29 | ||
30 | ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints)); | 30 | // We pass async function though it will not be awaited when called, |
31 | // thus Promise rejections won't be handled, but this should never throw in fact... | ||
32 | ctx.onDidRestart(async _ => hintsUpdater.setEnabled(ctx.config.displayInlayHints)); | ||
31 | } | 33 | } |
32 | 34 | ||
33 | interface InlayHintsParams { | 35 | interface InlayHintsParams { |
@@ -36,7 +38,7 @@ interface InlayHintsParams { | |||
36 | 38 | ||
37 | interface InlayHint { | 39 | interface InlayHint { |
38 | range: vscode.Range; | 40 | range: vscode.Range; |
39 | kind: string; | 41 | kind: "TypeHint" | "ParameterHint"; |
40 | label: string; | 42 | label: string; |
41 | } | 43 | } |
42 | 44 | ||
@@ -53,7 +55,7 @@ const parameterHintDecorationType = vscode.window.createTextEditorDecorationType | |||
53 | }); | 55 | }); |
54 | 56 | ||
55 | class HintsUpdater { | 57 | class HintsUpdater { |
56 | private pending: Map<string, vscode.CancellationTokenSource> = new Map(); | 58 | private pending = new Map<string, vscode.CancellationTokenSource>(); |
57 | private ctx: Ctx; | 59 | private ctx: Ctx; |
58 | private enabled: boolean; | 60 | private enabled: boolean; |
59 | 61 | ||
@@ -62,30 +64,36 @@ class HintsUpdater { | |||
62 | this.enabled = ctx.config.displayInlayHints; | 64 | this.enabled = ctx.config.displayInlayHints; |
63 | } | 65 | } |
64 | 66 | ||
65 | async setEnabled(enabled: boolean) { | 67 | async setEnabled(enabled: boolean): Promise<void> { |
66 | if (this.enabled == enabled) return; | 68 | if (this.enabled == enabled) return; |
67 | this.enabled = enabled; | 69 | this.enabled = enabled; |
68 | 70 | ||
69 | if (this.enabled) { | 71 | if (this.enabled) { |
70 | await this.refresh(); | 72 | return await this.refresh(); |
71 | } else { | ||
72 | this.allEditors.forEach(it => { | ||
73 | this.setTypeDecorations(it, []); | ||
74 | this.setParameterDecorations(it, []); | ||
75 | }); | ||
76 | } | 73 | } |
74 | this.allEditors.forEach(it => { | ||
75 | this.setTypeDecorations(it, []); | ||
76 | this.setParameterDecorations(it, []); | ||
77 | }); | ||
77 | } | 78 | } |
78 | 79 | ||
79 | async refresh() { | 80 | async refresh() { |
80 | if (!this.enabled) return; | 81 | if (!this.enabled) return; |
81 | const promises = this.allEditors.map(it => this.refreshEditor(it)); | 82 | await Promise.all(this.allEditors.map(it => this.refreshEditor(it))); |
82 | await Promise.all(promises); | 83 | } |
84 | |||
85 | private get allEditors(): vscode.TextEditor[] { | ||
86 | return vscode.window.visibleTextEditors.filter( | ||
87 | editor => editor.document.languageId === 'rust', | ||
88 | ); | ||
83 | } | 89 | } |
84 | 90 | ||
85 | private async refreshEditor(editor: vscode.TextEditor): Promise<void> { | 91 | private async refreshEditor(editor: vscode.TextEditor): Promise<void> { |
86 | const newHints = await this.queryHints(editor.document.uri.toString()); | 92 | const newHints = await this.queryHints(editor.document.uri.toString()); |
87 | if (newHints == null) return; | 93 | if (newHints == null) return; |
88 | const newTypeDecorations = newHints.filter(hint => hint.kind === 'TypeHint') | 94 | |
95 | const newTypeDecorations = newHints | ||
96 | .filter(hint => hint.kind === 'TypeHint') | ||
89 | .map(hint => ({ | 97 | .map(hint => ({ |
90 | range: hint.range, | 98 | range: hint.range, |
91 | renderOptions: { | 99 | renderOptions: { |
@@ -96,7 +104,8 @@ class HintsUpdater { | |||
96 | })); | 104 | })); |
97 | this.setTypeDecorations(editor, newTypeDecorations); | 105 | this.setTypeDecorations(editor, newTypeDecorations); |
98 | 106 | ||
99 | const newParameterDecorations = newHints.filter(hint => hint.kind === 'ParameterHint') | 107 | const newParameterDecorations = newHints |
108 | .filter(hint => hint.kind === 'ParameterHint') | ||
100 | .map(hint => ({ | 109 | .map(hint => ({ |
101 | range: hint.range, | 110 | range: hint.range, |
102 | renderOptions: { | 111 | renderOptions: { |
@@ -108,12 +117,6 @@ class HintsUpdater { | |||
108 | this.setParameterDecorations(editor, newParameterDecorations); | 117 | this.setParameterDecorations(editor, newParameterDecorations); |
109 | } | 118 | } |
110 | 119 | ||
111 | private get allEditors(): vscode.TextEditor[] { | ||
112 | return vscode.window.visibleTextEditors.filter( | ||
113 | editor => editor.document.languageId === 'rust', | ||
114 | ); | ||
115 | } | ||
116 | |||
117 | private setTypeDecorations( | 120 | private setTypeDecorations( |
118 | editor: vscode.TextEditor, | 121 | editor: vscode.TextEditor, |
119 | decorations: vscode.DecorationOptions[], | 122 | decorations: vscode.DecorationOptions[], |
@@ -137,12 +140,14 @@ class HintsUpdater { | |||
137 | private async queryHints(documentUri: string): Promise<InlayHint[] | null> { | 140 | private async queryHints(documentUri: string): Promise<InlayHint[] | null> { |
138 | const client = this.ctx.client; | 141 | const client = this.ctx.client; |
139 | if (!client) return null; | 142 | if (!client) return null; |
143 | |||
140 | const request: InlayHintsParams = { | 144 | const request: InlayHintsParams = { |
141 | textDocument: { uri: documentUri }, | 145 | textDocument: { uri: documentUri }, |
142 | }; | 146 | }; |
143 | const tokenSource = new vscode.CancellationTokenSource(); | 147 | const tokenSource = new vscode.CancellationTokenSource(); |
144 | const prev = this.pending.get(documentUri); | 148 | const prevHintsRequest = this.pending.get(documentUri); |
145 | if (prev) prev.cancel(); | 149 | prevHintsRequest?.cancel(); |
150 | |||
146 | this.pending.set(documentUri, tokenSource); | 151 | this.pending.set(documentUri, tokenSource); |
147 | try { | 152 | try { |
148 | return await sendRequestWithRetry<InlayHint[] | null>( | 153 | return await sendRequestWithRetry<InlayHint[] | null>( |
diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts new file mode 100644 index 000000000..de655f8f4 --- /dev/null +++ b/editors/code/src/installation/download_artifact.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { promises as fs } from "fs"; | ||
4 | import { strict as assert } from "assert"; | ||
5 | |||
6 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
7 | import { downloadFile } from "./download_file"; | ||
8 | import { throttle } from "throttle-debounce"; | ||
9 | |||
10 | /** | ||
11 | * Downloads artifact from given `downloadUrl`. | ||
12 | * Creates `installationDir` if it is not yet created and put the artifact under | ||
13 | * `artifactFileName`. | ||
14 | * Displays info about the download progress in an info message printing the name | ||
15 | * of the artifact as `displayName`. | ||
16 | */ | ||
17 | export async function downloadArtifact( | ||
18 | {downloadUrl, releaseName}: ArtifactReleaseInfo, | ||
19 | artifactFileName: string, | ||
20 | installationDir: string, | ||
21 | displayName: string, | ||
22 | ) { | ||
23 | await fs.mkdir(installationDir).catch(err => assert.strictEqual( | ||
24 | err?.code, | ||
25 | "EEXIST", | ||
26 | `Couldn't create directory "${installationDir}" to download `+ | ||
27 | `${artifactFileName} artifact: ${err.message}` | ||
28 | )); | ||
29 | |||
30 | const installationPath = path.join(installationDir, artifactFileName); | ||
31 | |||
32 | console.time(`Downloading ${artifactFileName}`); | ||
33 | await vscode.window.withProgress( | ||
34 | { | ||
35 | location: vscode.ProgressLocation.Notification, | ||
36 | cancellable: false, // FIXME: add support for canceling download? | ||
37 | title: `Downloading ${displayName} (${releaseName})` | ||
38 | }, | ||
39 | async (progress, _cancellationToken) => { | ||
40 | let lastPrecentage = 0; | ||
41 | const filePermissions = 0o755; // (rwx, r_x, r_x) | ||
42 | await downloadFile(downloadUrl, installationPath, filePermissions, throttle( | ||
43 | 200, | ||
44 | /* noTrailing: */ true, | ||
45 | (readBytes, totalBytes) => { | ||
46 | const newPercentage = (readBytes / totalBytes) * 100; | ||
47 | progress.report({ | ||
48 | message: newPercentage.toFixed(0) + "%", | ||
49 | increment: newPercentage - lastPrecentage | ||
50 | }); | ||
51 | |||
52 | lastPrecentage = newPercentage; | ||
53 | }) | ||
54 | ); | ||
55 | } | ||
56 | ); | ||
57 | console.timeEnd(`Downloading ${artifactFileName}`); | ||
58 | } | ||
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts index b51602ef9..d154f4816 100644 --- a/editors/code/src/installation/download_file.ts +++ b/editors/code/src/installation/download_file.ts | |||
@@ -1,9 +1,13 @@ | |||
1 | import fetch from "node-fetch"; | 1 | import fetch from "node-fetch"; |
2 | import * as fs from "fs"; | 2 | import * as fs from "fs"; |
3 | import * as stream from "stream"; | ||
4 | import * as util from "util"; | ||
3 | import { strict as assert } from "assert"; | 5 | import { strict as assert } from "assert"; |
4 | 6 | ||
7 | const pipeline = util.promisify(stream.pipeline); | ||
8 | |||
5 | /** | 9 | /** |
6 | * Downloads file from `url` and stores it at `destFilePath`. | 10 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. |
7 | * `onProgress` callback is called on recieveing each chunk of bytes | 11 | * `onProgress` callback is called on recieveing each chunk of bytes |
8 | * to track the progress of downloading, it gets the already read and total | 12 | * to track the progress of downloading, it gets the already read and total |
9 | * amount of bytes to read as its parameters. | 13 | * amount of bytes to read as its parameters. |
@@ -11,24 +15,37 @@ import { strict as assert } from "assert"; | |||
11 | export async function downloadFile( | 15 | export async function downloadFile( |
12 | url: string, | 16 | url: string, |
13 | destFilePath: fs.PathLike, | 17 | destFilePath: fs.PathLike, |
18 | destFilePermissions: number, | ||
14 | onProgress: (readBytes: number, totalBytes: number) => void | 19 | onProgress: (readBytes: number, totalBytes: number) => void |
15 | ): Promise<void> { | 20 | ): Promise<void> { |
16 | const response = await fetch(url); | 21 | const res = await fetch(url); |
22 | |||
23 | if (!res.ok) { | ||
24 | console.log("Error", res.status, "while downloading file from", url); | ||
25 | console.dir({ body: await res.text(), headers: res.headers }, { depth: 3 }); | ||
17 | 26 | ||
18 | const totalBytes = Number(response.headers.get('content-length')); | 27 | throw new Error(`Got response ${res.status} when trying to download a file.`); |
28 | } | ||
29 | |||
30 | const totalBytes = Number(res.headers.get('content-length')); | ||
19 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); | 31 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); |
20 | 32 | ||
33 | console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | ||
34 | |||
21 | let readBytes = 0; | 35 | let readBytes = 0; |
36 | res.body.on("data", (chunk: Buffer) => { | ||
37 | readBytes += chunk.length; | ||
38 | onProgress(readBytes, totalBytes); | ||
39 | }); | ||
22 | 40 | ||
23 | console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | 41 | const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions }); |
42 | |||
43 | await pipeline(res.body, destFileStream); | ||
44 | return new Promise<void>(resolve => { | ||
45 | destFileStream.on("close", resolve); | ||
46 | destFileStream.destroy(); | ||
24 | 47 | ||
25 | return new Promise<void>((resolve, reject) => response.body | 48 | // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 |
26 | .on("data", (chunk: Buffer) => { | 49 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 |
27 | readBytes += chunk.length; | 50 | }); |
28 | onProgress(readBytes, totalBytes); | ||
29 | }) | ||
30 | .on("end", resolve) | ||
31 | .on("error", reject) | ||
32 | .pipe(fs.createWriteStream(destFilePath)) | ||
33 | ); | ||
34 | } | 51 | } |
diff --git a/editors/code/src/installation/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_artifact_release_info.ts index 7e3700603..7d497057a 100644 --- a/editors/code/src/installation/fetch_latest_artifact_metadata.ts +++ b/editors/code/src/installation/fetch_artifact_release_info.ts | |||
@@ -1,26 +1,32 @@ | |||
1 | import fetch from "node-fetch"; | 1 | import fetch from "node-fetch"; |
2 | import { GithubRepo, ArtifactMetadata } from "./interfaces"; | 2 | import { GithubRepo, ArtifactReleaseInfo } from "./interfaces"; |
3 | 3 | ||
4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | 4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; |
5 | 5 | ||
6 | |||
6 | /** | 7 | /** |
7 | * Fetches the latest release from GitHub `repo` and returns metadata about | 8 | * Fetches the release with `releaseTag` (or just latest release when not specified) |
8 | * `artifactFileName` shipped with this release or `null` if no such artifact was published. | 9 | * from GitHub `repo` and returns metadata about `artifactFileName` shipped with |
10 | * this release or `null` if no such artifact was published. | ||
9 | */ | 11 | */ |
10 | export async function fetchLatestArtifactMetadata( | 12 | export async function fetchArtifactReleaseInfo( |
11 | repo: GithubRepo, artifactFileName: string | 13 | repo: GithubRepo, artifactFileName: string, releaseTag?: string |
12 | ): Promise<null | ArtifactMetadata> { | 14 | ): Promise<null | ArtifactReleaseInfo> { |
13 | 15 | ||
14 | const repoOwner = encodeURIComponent(repo.owner); | 16 | const repoOwner = encodeURIComponent(repo.owner); |
15 | const repoName = encodeURIComponent(repo.name); | 17 | const repoName = encodeURIComponent(repo.name); |
16 | 18 | ||
17 | const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; | 19 | const apiEndpointPath = releaseTag |
20 | ? `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}` | ||
21 | : `/repos/${repoOwner}/${repoName}/releases/latest`; | ||
22 | |||
18 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | 23 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; |
19 | 24 | ||
20 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | 25 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) |
21 | 26 | ||
22 | console.log("Issuing request for released artifacts metadata to", requestUrl); | 27 | console.log("Issuing request for released artifacts metadata to", requestUrl); |
23 | 28 | ||
29 | // FIXME: handle non-ok response | ||
24 | const response: GithubRelease = await fetch(requestUrl, { | 30 | const response: GithubRelease = await fetch(requestUrl, { |
25 | headers: { Accept: "application/vnd.github.v3+json" } | 31 | headers: { Accept: "application/vnd.github.v3+json" } |
26 | }) | 32 | }) |
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts index 8039d0b90..e40839e4b 100644 --- a/editors/code/src/installation/interfaces.ts +++ b/editors/code/src/installation/interfaces.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | |||
1 | export interface GithubRepo { | 3 | export interface GithubRepo { |
2 | name: string; | 4 | name: string; |
3 | owner: string; | 5 | owner: string; |
@@ -6,7 +8,7 @@ export interface GithubRepo { | |||
6 | /** | 8 | /** |
7 | * Metadata about particular artifact retrieved from GitHub releases. | 9 | * Metadata about particular artifact retrieved from GitHub releases. |
8 | */ | 10 | */ |
9 | export interface ArtifactMetadata { | 11 | export interface ArtifactReleaseInfo { |
10 | releaseName: string; | 12 | releaseName: string; |
11 | downloadUrl: string; | 13 | downloadUrl: string; |
12 | } | 14 | } |
@@ -50,6 +52,17 @@ export namespace BinarySource { | |||
50 | * and in local `.dir`. | 52 | * and in local `.dir`. |
51 | */ | 53 | */ |
52 | file: string; | 54 | file: string; |
55 | |||
56 | /** | ||
57 | * Tag of github release that denotes a version required by this extension. | ||
58 | */ | ||
59 | version: string; | ||
60 | |||
61 | /** | ||
62 | * Object that provides `get()/update()` operations to store metadata | ||
63 | * about the actual binary, e.g. its actual version. | ||
64 | */ | ||
65 | storage: vscode.Memento; | ||
53 | } | 66 | } |
54 | 67 | ||
55 | } | 68 | } |
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts deleted file mode 100644 index 1ce67b8b2..000000000 --- a/editors/code/src/installation/language_server.ts +++ /dev/null | |||
@@ -1,141 +0,0 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { strict as assert } from "assert"; | ||
4 | import { promises as fs } from "fs"; | ||
5 | import { promises as dns } from "dns"; | ||
6 | import { spawnSync } from "child_process"; | ||
7 | import { throttle } from "throttle-debounce"; | ||
8 | |||
9 | import { BinarySource } from "./interfaces"; | ||
10 | import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata"; | ||
11 | import { downloadFile } from "./download_file"; | ||
12 | |||
13 | export 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 | await downloadFile(downloadUrl, installationPath, throttle( | ||
39 | 200, | ||
40 | /* noTrailing: */ true, | ||
41 | (readBytes, totalBytes) => { | ||
42 | const newPercentage = (readBytes / totalBytes) * 100; | ||
43 | progress.report({ | ||
44 | message: newPercentage.toFixed(0) + "%", | ||
45 | increment: newPercentage - lastPrecentage | ||
46 | }); | ||
47 | |||
48 | lastPrecentage = newPercentage; | ||
49 | }) | ||
50 | ); | ||
51 | } | ||
52 | ); | ||
53 | console.timeEnd("Downloading ra_lsp_server"); | ||
54 | |||
55 | await fs.chmod(installationPath, 0o755); // Set (rwx, r_x, r_x) permissions | ||
56 | } | ||
57 | export async function ensureLanguageServerBinary( | ||
58 | langServerSource: null | BinarySource | ||
59 | ): Promise<null | string> { | ||
60 | |||
61 | if (!langServerSource) { | ||
62 | vscode.window.showErrorMessage( | ||
63 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
64 | "You need to manually clone rust-analyzer repository and " + | ||
65 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
66 | "If you feel that your platform should be supported, please create an issue " + | ||
67 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
68 | "will consider it." | ||
69 | ); | ||
70 | return null; | ||
71 | } | ||
72 | |||
73 | switch (langServerSource.type) { | ||
74 | case BinarySource.Type.ExplicitPath: { | ||
75 | if (isBinaryAvailable(langServerSource.path)) { | ||
76 | return langServerSource.path; | ||
77 | } | ||
78 | |||
79 | vscode.window.showErrorMessage( | ||
80 | `Unable to run ${langServerSource.path} binary. ` + | ||
81 | `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + | ||
82 | "value to `null` or remove it from the settings to use it by default." | ||
83 | ); | ||
84 | return null; | ||
85 | } | ||
86 | case BinarySource.Type.GithubRelease: { | ||
87 | const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file); | ||
88 | |||
89 | if (isBinaryAvailable(prebuiltBinaryPath)) { | ||
90 | return prebuiltBinaryPath; | ||
91 | } | ||
92 | |||
93 | const userResponse = await vscode.window.showInformationMessage( | ||
94 | "Language server binary for rust-analyzer was not found. " + | ||
95 | "Do you want to download it now?", | ||
96 | "Download now", "Cancel" | ||
97 | ); | ||
98 | if (userResponse !== "Download now") return null; | ||
99 | |||
100 | try { | ||
101 | await downloadLatestLanguageServer(langServerSource); | ||
102 | } catch (err) { | ||
103 | await vscode.window.showErrorMessage( | ||
104 | `Failed to download language server from ${langServerSource.repo.name} ` + | ||
105 | `GitHub repository: ${err.message}` | ||
106 | ); | ||
107 | |||
108 | await dns.resolve('www.google.com').catch(err => { | ||
109 | console.error("DNS resolution failed, there might be an issue with Internet availability"); | ||
110 | console.error(err); | ||
111 | }); | ||
112 | |||
113 | return null; | ||
114 | } | ||
115 | |||
116 | if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, | ||
117 | `Downloaded language server binary is not functional.` + | ||
118 | `Downloaded from: ${JSON.stringify(langServerSource)}` | ||
119 | ); | ||
120 | |||
121 | |||
122 | vscode.window.showInformationMessage( | ||
123 | "Rust analyzer language server was successfully installed 🦀" | ||
124 | ); | ||
125 | |||
126 | return prebuiltBinaryPath; | ||
127 | } | ||
128 | } | ||
129 | |||
130 | function isBinaryAvailable(binaryPath: string) { | ||
131 | const res = spawnSync(binaryPath, ["--version"]); | ||
132 | |||
133 | // ACHTUNG! `res` type declaration is inherently wrong, see | ||
134 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | ||
135 | |||
136 | console.log("Checked binary availablity via --version", res); | ||
137 | console.log(binaryPath, "--version output:", res.output?.map(String)); | ||
138 | |||
139 | return res.status === 0; | ||
140 | } | ||
141 | } | ||
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts new file mode 100644 index 000000000..80cb719e3 --- /dev/null +++ b/editors/code/src/installation/server.ts | |||
@@ -0,0 +1,124 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { strict as assert } from "assert"; | ||
4 | import { promises as dns } from "dns"; | ||
5 | import { spawnSync } from "child_process"; | ||
6 | |||
7 | import { BinarySource } from "./interfaces"; | ||
8 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; | ||
9 | import { downloadArtifact } from "./download_artifact"; | ||
10 | |||
11 | export async function ensureServerBinary(source: null | BinarySource): Promise<null | string> { | ||
12 | if (!source) { | ||
13 | vscode.window.showErrorMessage( | ||
14 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
15 | "You need to manually clone rust-analyzer repository and " + | ||
16 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
17 | "If you feel that your platform should be supported, please create an issue " + | ||
18 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
19 | "will consider it." | ||
20 | ); | ||
21 | return null; | ||
22 | } | ||
23 | |||
24 | switch (source.type) { | ||
25 | case BinarySource.Type.ExplicitPath: { | ||
26 | if (isBinaryAvailable(source.path)) { | ||
27 | return source.path; | ||
28 | } | ||
29 | |||
30 | vscode.window.showErrorMessage( | ||
31 | `Unable to run ${source.path} binary. ` + | ||
32 | `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + | ||
33 | "value to `null` or remove it from the settings to use it by default." | ||
34 | ); | ||
35 | return null; | ||
36 | } | ||
37 | case BinarySource.Type.GithubRelease: { | ||
38 | const prebuiltBinaryPath = path.join(source.dir, source.file); | ||
39 | |||
40 | const installedVersion: null | string = getServerVersion(source.storage); | ||
41 | const requiredVersion: string = source.version; | ||
42 | |||
43 | console.log("Installed version:", installedVersion, "required:", requiredVersion); | ||
44 | |||
45 | if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion == requiredVersion) { | ||
46 | // FIXME: check for new releases and notify the user to update if possible | ||
47 | return prebuiltBinaryPath; | ||
48 | } | ||
49 | |||
50 | const userResponse = await vscode.window.showInformationMessage( | ||
51 | `Language server version ${source.version} for rust-analyzer is not installed. ` + | ||
52 | "Do you want to download it now?", | ||
53 | "Download now", "Cancel" | ||
54 | ); | ||
55 | if (userResponse !== "Download now") return null; | ||
56 | |||
57 | if (!await downloadServer(source)) return null; | ||
58 | |||
59 | return prebuiltBinaryPath; | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
64 | async function downloadServer(source: BinarySource.GithubRelease): Promise<boolean> { | ||
65 | try { | ||
66 | const releaseInfo = (await fetchArtifactReleaseInfo(source.repo, source.file, source.version))!; | ||
67 | |||
68 | await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); | ||
69 | await setServerVersion(source.storage, releaseInfo.releaseName); | ||
70 | } catch (err) { | ||
71 | vscode.window.showErrorMessage( | ||
72 | `Failed to download language server from ${source.repo.name} ` + | ||
73 | `GitHub repository: ${err.message}` | ||
74 | ); | ||
75 | |||
76 | console.error(err); | ||
77 | |||
78 | dns.resolve('example.com').then( | ||
79 | addrs => console.log("DNS resolution for example.com was successful", addrs), | ||
80 | err => { | ||
81 | console.error( | ||
82 | "DNS resolution for example.com failed, " + | ||
83 | "there might be an issue with Internet availability" | ||
84 | ); | ||
85 | console.error(err); | ||
86 | } | ||
87 | ); | ||
88 | return false; | ||
89 | } | ||
90 | |||
91 | if (!isBinaryAvailable(path.join(source.dir, source.file))) assert(false, | ||
92 | `Downloaded language server binary is not functional.` + | ||
93 | `Downloaded from: ${JSON.stringify(source, null, 4)}` | ||
94 | ); | ||
95 | |||
96 | vscode.window.showInformationMessage( | ||
97 | "Rust analyzer language server was successfully installed 🦀" | ||
98 | ); | ||
99 | |||
100 | return true; | ||
101 | } | ||
102 | |||
103 | function isBinaryAvailable(binaryPath: string): boolean { | ||
104 | const res = spawnSync(binaryPath, ["--version"]); | ||
105 | |||
106 | // ACHTUNG! `res` type declaration is inherently wrong, see | ||
107 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | ||
108 | |||
109 | console.log("Checked binary availablity via --version", res); | ||
110 | console.log(binaryPath, "--version output:", res.output?.map(String)); | ||
111 | |||
112 | return res.status === 0; | ||
113 | } | ||
114 | |||
115 | function getServerVersion(storage: vscode.Memento): null | string { | ||
116 | const version = storage.get<null | string>("server-version", null); | ||
117 | console.log("Get server-version:", version); | ||
118 | return version; | ||
119 | } | ||
120 | |||
121 | async function setServerVersion(storage: vscode.Memento, version: string): Promise<void> { | ||
122 | console.log("Set server-version:", version); | ||
123 | await storage.update("server-version", version.toString()); | ||
124 | } | ||
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts index 51dbf388b..993e79d70 100644 --- a/editors/code/src/status_display.ts +++ b/editors/code/src/status_display.ts | |||
@@ -66,9 +66,9 @@ class StatusDisplay implements Disposable { | |||
66 | 66 | ||
67 | refreshLabel() { | 67 | refreshLabel() { |
68 | if (this.packageName) { | 68 | if (this.packageName) { |
69 | this.statusBarItem!.text = `${spinnerFrames[this.i]} cargo ${this.command} [${this.packageName}]`; | 69 | this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command} [${this.packageName}]`; |
70 | } else { | 70 | } else { |
71 | this.statusBarItem!.text = `${spinnerFrames[this.i]} cargo ${this.command}`; | 71 | this.statusBarItem.text = `${spinnerFrames[this.i]} cargo ${this.command}`; |
72 | } | 72 | } |
73 | } | 73 | } |
74 | 74 | ||