aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts35
-rw-r--r--editors/code/src/color_theme.ts28
-rw-r--r--editors/code/src/commands/index.ts49
-rw-r--r--editors/code/src/commands/on_enter.ts45
-rw-r--r--editors/code/src/commands/syntax_tree.ts4
-rw-r--r--editors/code/src/config.ts81
-rw-r--r--editors/code/src/ctx.ts48
-rw-r--r--editors/code/src/highlighting.ts34
-rw-r--r--editors/code/src/inlay_hints.ts40
-rw-r--r--editors/code/src/installation/download_file.ts34
-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.ts141
-rw-r--r--editors/code/src/main.ts8
-rw-r--r--editors/code/src/status_display.ts34
15 files changed, 514 insertions, 168 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 1ff64a930..2e3d4aba2 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -1,25 +1,21 @@
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). It might be a good idea to test if the uri points to a file. 9 // TODO?: Workspace folders support Uri's (eg: file://test.txt).
11 let folder: string = '.'; 10 // It might be a good idea to test if the uri points to a file.
12 if (workspace.workspaceFolders !== undefined) { 11 const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
13 folder = workspace.workspaceFolders[0].uri.fsPath.toString(); 12
14 } 13 const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource);
14 if (!raLspServerPath) return null;
15 15
16 const command = expandPathResolving(config.raLspServerPath);
17 if (spawnSync(command, ["--version"]).status !== 0) {
18 window.showErrorMessage(`Unable to execute '${command} --version'`);
19 }
20 const run: lc.Executable = { 16 const run: lc.Executable = {
21 command, 17 command: raLspServerPath,
22 options: { cwd: folder }, 18 options: { cwd: workspaceFolderPath },
23 }; 19 };
24 const serverOptions: lc.ServerOptions = { 20 const serverOptions: lc.ServerOptions = {
25 run, 21 run,
@@ -37,8 +33,7 @@ export function createClient(config: Config): lc.LanguageClient {
37 cargoWatchEnable: config.cargoWatchOptions.enable, 33 cargoWatchEnable: config.cargoWatchOptions.enable,
38 cargoWatchArgs: config.cargoWatchOptions.arguments, 34 cargoWatchArgs: config.cargoWatchOptions.arguments,
39 cargoWatchCommand: config.cargoWatchOptions.command, 35 cargoWatchCommand: config.cargoWatchOptions.command,
40 cargoWatchAllTargets: 36 cargoWatchAllTargets: config.cargoWatchOptions.allTargets,
41 config.cargoWatchOptions.allTargets,
42 excludeGlobs: config.excludeGlobs, 37 excludeGlobs: config.excludeGlobs,
43 useClientWatching: config.useClientWatching, 38 useClientWatching: config.useClientWatching,
44 featureFlags: config.featureFlags, 39 featureFlags: config.featureFlags,
@@ -62,7 +57,7 @@ export function createClient(config: Config): lc.LanguageClient {
62 // This also requires considering our settings strategy, which is work which needs doing 57 // This also requires considering our settings strategy, which is work which needs doing
63 // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests 58 // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests
64 res._tracer = { 59 res._tracer = {
65 log: (messageOrDataObject: string | any, data?: string) => { 60 log: (messageOrDataObject: string | unknown, data?: string) => {
66 if (typeof messageOrDataObject === 'string') { 61 if (typeof messageOrDataObject === 'string') {
67 if ( 62 if (
68 messageOrDataObject.includes( 63 messageOrDataObject.includes(
@@ -86,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
86 res.registerProposedFeatures(); 81 res.registerProposedFeatures();
87 return res; 82 return res;
88} 83}
89function expandPathResolving(path: string) {
90 if (path.startsWith('~/')) {
91 return path.replace('~', homedir());
92 }
93 return path;
94}
diff --git a/editors/code/src/color_theme.ts b/editors/code/src/color_theme.ts
index cbad47f35..a6957a76e 100644
--- a/editors/code/src/color_theme.ts
+++ b/editors/code/src/color_theme.ts
@@ -28,9 +28,12 @@ export class ColorTheme {
28 static fromRules(rules: TextMateRule[]): ColorTheme { 28 static fromRules(rules: TextMateRule[]): ColorTheme {
29 const res = new ColorTheme(); 29 const res = new ColorTheme();
30 for (const rule of rules) { 30 for (const rule of rules) {
31 const scopes = typeof rule.scope === 'string' 31 const scopes = typeof rule.scope === 'undefined'
32 ? [rule.scope] 32 ? []
33 : rule.scope; 33 : typeof rule.scope === 'string'
34 ? [rule.scope]
35 : rule.scope;
36
34 for (const scope of scopes) { 37 for (const scope of scopes) {
35 res.rules.set(scope, rule.settings); 38 res.rules.set(scope, rule.settings);
36 } 39 }
@@ -59,7 +62,7 @@ export class ColorTheme {
59} 62}
60 63
61function loadThemeNamed(themeName: string): ColorTheme { 64function loadThemeNamed(themeName: string): ColorTheme {
62 function isTheme(extension: vscode.Extension<any>): boolean { 65 function isTheme(extension: vscode.Extension<unknown>): boolean {
63 return ( 66 return (
64 extension.extensionKind === vscode.ExtensionKind.UI && 67 extension.extensionKind === vscode.ExtensionKind.UI &&
65 extension.packageJSON.contributes && 68 extension.packageJSON.contributes &&
@@ -67,13 +70,13 @@ function loadThemeNamed(themeName: string): ColorTheme {
67 ); 70 );
68 } 71 }
69 72
70 let themePaths = vscode.extensions.all 73 const themePaths: string[] = vscode.extensions.all
71 .filter(isTheme) 74 .filter(isTheme)
72 .flatMap(ext => { 75 .flatMap(
73 return ext.packageJSON.contributes.themes 76 ext => ext.packageJSON.contributes.themes
74 .filter((it: any) => (it.id || it.label) === themeName) 77 .filter((it: any) => (it.id || it.label) === themeName)
75 .map((it: any) => path.join(ext.extensionPath, it.path)); 78 .map((it: any) => path.join(ext.extensionPath, it.path))
76 }); 79 );
77 80
78 const res = new ColorTheme(); 81 const res = new ColorTheme();
79 for (const themePath of themePaths) { 82 for (const themePath of themePaths) {
@@ -94,13 +97,12 @@ function loadThemeFile(themePath: string): ColorTheme {
94 return new ColorTheme(); 97 return new ColorTheme();
95 } 98 }
96 const obj = jsonc.parse(text); 99 const obj = jsonc.parse(text);
97 const tokenColors = obj?.tokenColors ?? []; 100 const tokenColors: TextMateRule[] = obj?.tokenColors ?? [];
98 const res = ColorTheme.fromRules(tokenColors); 101 const res = ColorTheme.fromRules(tokenColors);
99 102
100 for (const include in obj?.include ?? []) { 103 for (const include of obj?.include ?? []) {
101 const includePath = path.join(path.dirname(themePath), include); 104 const includePath = path.join(path.dirname(themePath), include);
102 const tmp = loadThemeFile(includePath); 105 res.mergeFrom(loadThemeFile(includePath));
103 res.mergeFrom(tmp);
104 } 106 }
105 107
106 return res; 108 return res;
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index dc075aa82..aee969432 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -4,24 +4,24 @@ import * as lc from 'vscode-languageclient';
4import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd } from '../ctx';
5import * as sourceChange from '../source_change'; 5import * as sourceChange from '../source_change';
6 6
7import { analyzerStatus } from './analyzer_status'; 7export * from './analyzer_status';
8import { matchingBrace } from './matching_brace'; 8export * from './matching_brace';
9import { joinLines } from './join_lines'; 9export * from './join_lines';
10import { onEnter } from './on_enter'; 10export * from './on_enter';
11import { parentModule } from './parent_module'; 11export * from './parent_module';
12import { syntaxTree } from './syntax_tree'; 12export * from './syntax_tree';
13import { expandMacro } from './expand_macro'; 13export * from './expand_macro';
14import { run, runSingle } from './runnables'; 14export * from './runnables';
15 15
16function collectGarbage(ctx: Ctx): Cmd { 16export function collectGarbage(ctx: Ctx): Cmd {
17 return async () => { 17 return async () => {
18 ctx.client?.sendRequest<null>('rust-analyzer/collectGarbage', null); 18 ctx.client?.sendRequest<null>('rust-analyzer/collectGarbage', null);
19 }; 19 };
20} 20}
21 21
22function showReferences(ctx: Ctx): Cmd { 22export function showReferences(ctx: Ctx): Cmd {
23 return (uri: string, position: lc.Position, locations: lc.Location[]) => { 23 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
24 let client = ctx.client; 24 const client = ctx.client;
25 if (client) { 25 if (client) {
26 vscode.commands.executeCommand( 26 vscode.commands.executeCommand(
27 'editor.action.showReferences', 27 'editor.action.showReferences',
@@ -33,13 +33,13 @@ function showReferences(ctx: Ctx): Cmd {
33 }; 33 };
34} 34}
35 35
36function applySourceChange(ctx: Ctx): Cmd { 36export function applySourceChange(ctx: Ctx): Cmd {
37 return async (change: sourceChange.SourceChange) => { 37 return async (change: sourceChange.SourceChange) => {
38 sourceChange.applySourceChange(ctx, change); 38 await sourceChange.applySourceChange(ctx, change);
39 }; 39 };
40} 40}
41 41
42function selectAndApplySourceChange(ctx: Ctx): Cmd { 42export function selectAndApplySourceChange(ctx: Ctx): Cmd {
43 return async (changes: sourceChange.SourceChange[]) => { 43 return async (changes: sourceChange.SourceChange[]) => {
44 if (changes.length === 1) { 44 if (changes.length === 1) {
45 await sourceChange.applySourceChange(ctx, changes[0]); 45 await sourceChange.applySourceChange(ctx, changes[0]);
@@ -51,26 +51,9 @@ function selectAndApplySourceChange(ctx: Ctx): Cmd {
51 }; 51 };
52} 52}
53 53
54function reload(ctx: Ctx): Cmd { 54export function reload(ctx: Ctx): Cmd {
55 return async () => { 55 return async () => {
56 vscode.window.showInformationMessage('Reloading rust-analyzer...'); 56 vscode.window.showInformationMessage('Reloading rust-analyzer...');
57 await ctx.restartServer(); 57 await ctx.restartServer();
58 }; 58 };
59} 59}
60
61export {
62 analyzerStatus,
63 expandMacro,
64 joinLines,
65 matchingBrace,
66 parentModule,
67 syntaxTree,
68 onEnter,
69 collectGarbage,
70 run,
71 runSingle,
72 showReferences,
73 applySourceChange,
74 selectAndApplySourceChange,
75 reload
76};
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts
index 6f61883cd..25eaebcbe 100644
--- a/editors/code/src/commands/on_enter.ts
+++ b/editors/code/src/commands/on_enter.ts
@@ -1,28 +1,35 @@
1import * as vscode from 'vscode';
1import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
2 3
3import { applySourceChange, SourceChange } from '../source_change'; 4import { applySourceChange, SourceChange } from '../source_change';
4import { Cmd, Ctx } from '../ctx'; 5import { Cmd, Ctx } from '../ctx';
5 6
6export function onEnter(ctx: Ctx): Cmd { 7async function handleKeypress(ctx: Ctx) {
7 return async (event: { text: string }) => { 8 const editor = ctx.activeRustEditor;
8 const editor = ctx.activeRustEditor; 9 const client = ctx.client;
9 const client = ctx.client; 10
10 if (!editor || event.text !== '\n') return false; 11 if (!editor || !client) return false;
11 if (!client) return false; 12
13 const request: lc.TextDocumentPositionParams = {
14 textDocument: { uri: editor.document.uri.toString() },
15 position: client.code2ProtocolConverter.asPosition(
16 editor.selection.active,
17 ),
18 };
19 const change = await client.sendRequest<undefined | SourceChange>(
20 'rust-analyzer/onEnter',
21 request,
22 );
23 if (!change) return false;
12 24
13 const request: lc.TextDocumentPositionParams = { 25 await applySourceChange(ctx, change);
14 textDocument: { uri: editor.document.uri.toString() }, 26 return true;
15 position: client.code2ProtocolConverter.asPosition( 27}
16 editor.selection.active, 28
17 ), 29export function onEnter(ctx: Ctx): Cmd {
18 }; 30 return async () => {
19 const change = await client.sendRequest<undefined | SourceChange>( 31 if (await handleKeypress(ctx)) return;
20 'rust-analyzer/onEnter',
21 request,
22 );
23 if (!change) return false;
24 32
25 await applySourceChange(ctx, change); 33 await vscode.commands.executeCommand('default:type', { text: '\n' });
26 return true;
27 }; 34 };
28} 35}
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
index 02ea9f166..7dde66ad1 100644
--- a/editors/code/src/commands/syntax_tree.ts
+++ b/editors/code/src/commands/syntax_tree.ts
@@ -22,6 +22,7 @@ export function syntaxTree(ctx: Ctx): Cmd {
22 if (doc.languageId !== 'rust') return; 22 if (doc.languageId !== 'rust') return;
23 afterLs(() => tdcp.eventEmitter.fire(tdcp.uri)); 23 afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
24 }, 24 },
25 null,
25 ctx.subscriptions, 26 ctx.subscriptions,
26 ); 27 );
27 28
@@ -30,6 +31,7 @@ export function syntaxTree(ctx: Ctx): Cmd {
30 if (!editor || editor.document.languageId !== 'rust') return; 31 if (!editor || editor.document.languageId !== 'rust') return;
31 tdcp.eventEmitter.fire(tdcp.uri); 32 tdcp.eventEmitter.fire(tdcp.uri);
32 }, 33 },
34 null,
33 ctx.subscriptions, 35 ctx.subscriptions,
34 ); 36 );
35 37
@@ -55,7 +57,7 @@ export function syntaxTree(ctx: Ctx): Cmd {
55 57
56// We need to order this after LS updates, but there's no API for that. 58// We need to order this after LS updates, but there's no API for that.
57// Hence, good old setTimeout. 59// Hence, good old setTimeout.
58function afterLs(f: () => any) { 60function afterLs(f: () => void) {
59 setTimeout(f, 10); 61 setTimeout(f, 10);
60} 62}
61 63
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index fc21c8813..d5f3da2ed 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,16 +18,17 @@ 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;
26 excludeGlobs = []; 29 excludeGlobs: string[] = [];
27 useClientWatching = true; 30 useClientWatching = true;
28 featureFlags = {}; 31 featureFlags: Record<string, boolean> = {};
29 // for internal use 32 // for internal use
30 withSysroot: null | boolean = null; 33 withSysroot: null | boolean = null;
31 cargoWatchOptions: CargoWatchOptions = { 34 cargoWatchOptions: CargoWatchOptions = {
@@ -45,11 +48,72 @@ 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(), 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;
60 }
61
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(platform: NodeJS.Platform): null | string {
68 switch (platform) {
69 case "linux": return "ra_lsp_server-linux";
70 case "darwin": return "ra_lsp_server-mac";
71 case "win32": return "ra_lsp_server-windows.exe";
72
73 // Users on these platforms yet need to manually build from sources
74 case "aix":
75 case "android":
76 case "freebsd":
77 case "openbsd":
78 case "sunos":
79 case "cygwin":
80 case "netbsd": return null;
81 // The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
82 }
83 }
84
85 private static langServerBinarySource(
86 ctx: vscode.ExtensionContext,
87 config: vscode.WorkspaceConfiguration
88 ): null | BinarySource {
89 const langServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
90
91 if (langServerPath) {
92 return {
93 type: BinarySource.Type.ExplicitPath,
94 path: Config.expandPathResolving(langServerPath)
95 };
96 }
97
98 const prebuiltBinaryName = Config.prebuiltLangServerFileName(process.platform);
99
100 if (!prebuiltBinaryName) return null;
101
102 return {
103 type: BinarySource.Type.GithubRelease,
104 dir: ctx.globalStoragePath,
105 file: prebuiltBinaryName,
106 repo: {
107 name: "rust-analyzer",
108 owner: "rust-analyzer",
109 }
110 };
50 } 111 }
51 112
52 private refresh() { 113
114 // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
115 // values only in one place (i.e. remove default values from non-readonly members declarations)
116 private refresh(ctx: vscode.ExtensionContext) {
53 const config = vscode.workspace.getConfiguration('rust-analyzer'); 117 const config = vscode.workspace.getConfiguration('rust-analyzer');
54 118
55 let requireReloadMessage = null; 119 let requireReloadMessage = null;
@@ -82,10 +146,7 @@ export class Config {
82 this.prevEnhancedTyping = this.enableEnhancedTyping; 146 this.prevEnhancedTyping = this.enableEnhancedTyping;
83 } 147 }
84 148
85 if (config.has('raLspServerPath')) { 149 this.langServerSource = Config.langServerBinarySource(ctx, config);
86 this.raLspServerPath =
87 RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
88 }
89 150
90 if (config.has('cargo-watch.enable')) { 151 if (config.has('cargo-watch.enable')) {
91 this.cargoWatchOptions.enable = config.get<boolean>( 152 this.cargoWatchOptions.enable = config.get<boolean>(
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index a2a4e42a9..70042a479 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -1,5 +1,6 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3
3import { Config } from './config'; 4import { Config } from './config';
4import { createClient } from './client'; 5import { createClient } from './client';
5 6
@@ -10,6 +11,9 @@ export class Ctx {
10 // deal with it. 11 // deal with it.
11 // 12 //
12 // 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)
13 client: lc.LanguageClient | null = null; 17 client: lc.LanguageClient | null = null;
14 private extCtx: vscode.ExtensionContext; 18 private extCtx: vscode.ExtensionContext;
15 private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = []; 19 private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
@@ -20,12 +24,19 @@ export class Ctx {
20 } 24 }
21 25
22 async restartServer() { 26 async restartServer() {
23 let old = this.client; 27 const old = this.client;
24 if (old) { 28 if (old) {
25 await old.stop(); 29 await old.stop();
26 } 30 }
27 this.client = null; 31 this.client = null;
28 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
29 this.pushCleanup(client.start()); 40 this.pushCleanup(client.start());
30 await client.onReady(); 41 await client.onReady();
31 42
@@ -49,33 +60,11 @@ export class Ctx {
49 this.pushCleanup(d); 60 this.pushCleanup(d);
50 } 61 }
51 62
52 overrideCommand(name: string, factory: (ctx: Ctx) => Cmd) { 63 get subscriptions(): Disposable[] {
53 const defaultCmd = `default:${name}`;
54 const override = factory(this);
55 const original = (...args: any[]) =>
56 vscode.commands.executeCommand(defaultCmd, ...args);
57 try {
58 const d = vscode.commands.registerCommand(
59 name,
60 async (...args: any[]) => {
61 if (!(await override(...args))) {
62 return await original(...args);
63 }
64 },
65 );
66 this.pushCleanup(d);
67 } catch (_) {
68 vscode.window.showWarningMessage(
69 'Enhanced typing feature is disabled because of incompatibility with VIM extension, consider turning off rust-analyzer.enableEnhancedTyping: https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/README.md#settings',
70 );
71 }
72 }
73
74 get subscriptions(): { dispose(): any }[] {
75 return this.extCtx.subscriptions; 64 return this.extCtx.subscriptions;
76 } 65 }
77 66
78 pushCleanup(d: { dispose(): any }) { 67 pushCleanup(d: Disposable) {
79 this.extCtx.subscriptions.push(d); 68 this.extCtx.subscriptions.push(d);
80 } 69 }
81 70
@@ -84,12 +73,15 @@ export class Ctx {
84 } 73 }
85} 74}
86 75
87export type Cmd = (...args: any[]) => any; 76export interface Disposable {
77 dispose(): void;
78}
79export type Cmd = (...args: any[]) => unknown;
88 80
89export async function sendRequestWithRetry<R>( 81export async function sendRequestWithRetry<R>(
90 client: lc.LanguageClient, 82 client: lc.LanguageClient,
91 method: string, 83 method: string,
92 param: any, 84 param: unknown,
93 token?: vscode.CancellationToken, 85 token?: vscode.CancellationToken,
94): Promise<R> { 86): Promise<R> {
95 for (const delay of [2, 4, 6, 8, 10, null]) { 87 for (const delay of [2, 4, 6, 8, 10, null]) {
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
index 014e96f75..4fbbe3ddc 100644
--- a/editors/code/src/highlighting.ts
+++ b/editors/code/src/highlighting.ts
@@ -1,7 +1,5 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3import * as seedrandom_ from 'seedrandom';
4const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207
5 3
6import { ColorTheme, TextMateRuleSettings } from './color_theme'; 4import { ColorTheme, TextMateRuleSettings } from './color_theme';
7 5
@@ -34,6 +32,7 @@ export function activateHighlighting(ctx: Ctx) {
34 32
35 vscode.workspace.onDidChangeConfiguration( 33 vscode.workspace.onDidChangeConfiguration(
36 _ => highlighter.removeHighlights(), 34 _ => highlighter.removeHighlights(),
35 null,
37 ctx.subscriptions, 36 ctx.subscriptions,
38 ); 37 );
39 38
@@ -41,7 +40,7 @@ export function activateHighlighting(ctx: Ctx) {
41 async (editor: vscode.TextEditor | undefined) => { 40 async (editor: vscode.TextEditor | undefined) => {
42 if (!editor || editor.document.languageId !== 'rust') return; 41 if (!editor || editor.document.languageId !== 'rust') return;
43 if (!ctx.config.highlightingOn) return; 42 if (!ctx.config.highlightingOn) return;
44 let client = ctx.client; 43 const client = ctx.client;
45 if (!client) return; 44 if (!client) return;
46 45
47 const params: lc.TextDocumentIdentifier = { 46 const params: lc.TextDocumentIdentifier = {
@@ -54,6 +53,7 @@ export function activateHighlighting(ctx: Ctx) {
54 ); 53 );
55 highlighter.setHighlights(editor, decorations); 54 highlighter.setHighlights(editor, decorations);
56 }, 55 },
56 null,
57 ctx.subscriptions, 57 ctx.subscriptions,
58 ); 58 );
59} 59}
@@ -71,9 +71,9 @@ interface Decoration {
71 71
72// Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76 72// Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76
73function fancify(seed: string, shade: 'light' | 'dark') { 73function fancify(seed: string, shade: 'light' | 'dark') {
74 const random = seedrandom(seed); 74 const random = randomU32Numbers(hashString(seed));
75 const randomInt = (min: number, max: number) => { 75 const randomInt = (min: number, max: number) => {
76 return Math.floor(random() * (max - min + 1)) + min; 76 return Math.abs(random()) % (max - min + 1) + min;
77 }; 77 };
78 78
79 const h = randomInt(0, 360); 79 const h = randomInt(0, 360);
@@ -107,7 +107,7 @@ class Highlighter {
107 } 107 }
108 108
109 public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) { 109 public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) {
110 let client = this.ctx.client; 110 const client = this.ctx.client;
111 if (!client) return; 111 if (!client) return;
112 // Initialize decorations if necessary 112 // Initialize decorations if necessary
113 // 113 //
@@ -176,7 +176,7 @@ function initDecorations(): Map<string, vscode.TextEditorDecorationType> {
176 const res = new Map(); 176 const res = new Map();
177 TAG_TO_SCOPES.forEach((scopes, tag) => { 177 TAG_TO_SCOPES.forEach((scopes, tag) => {
178 if (!scopes) throw `unmapped tag: ${tag}`; 178 if (!scopes) throw `unmapped tag: ${tag}`;
179 let rule = theme.lookup(scopes); 179 const rule = theme.lookup(scopes);
180 const decor = createDecorationFromTextmate(rule); 180 const decor = createDecorationFromTextmate(rule);
181 res.set(tag, decor); 181 res.set(tag, decor);
182 }); 182 });
@@ -247,3 +247,23 @@ const TAG_TO_SCOPES = new Map<string, string[]>([
247 ["keyword.unsafe", ["keyword.other.unsafe"]], 247 ["keyword.unsafe", ["keyword.other.unsafe"]],
248 ["keyword.control", ["keyword.control"]], 248 ["keyword.control", ["keyword.control"]],
249]); 249]);
250
251function randomU32Numbers(seed: number) {
252 let random = seed | 0;
253 return () => {
254 random ^= random << 13;
255 random ^= random >> 17;
256 random ^= random << 5;
257 random |= 0;
258 return random;
259 };
260}
261
262function hashString(str: string): number {
263 let res = 0;
264 for (let i = 0; i < str.length; ++i) {
265 const c = str.codePointAt(i)!;
266 res = (res * 31 + c) & ~0;
267 }
268 return res;
269}
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index 6357e44f1..1c019a51b 100644
--- a/editors/code/src/inlay_hints.ts
+++ b/editors/code/src/inlay_hints.ts
@@ -5,19 +5,27 @@ import { Ctx, sendRequestWithRetry } from './ctx';
5 5
6export function activateInlayHints(ctx: Ctx) { 6export function activateInlayHints(ctx: Ctx) {
7 const hintsUpdater = new HintsUpdater(ctx); 7 const hintsUpdater = new HintsUpdater(ctx);
8 vscode.window.onDidChangeVisibleTextEditors(async _ => { 8 vscode.window.onDidChangeVisibleTextEditors(
9 await hintsUpdater.refresh(); 9 async _ => hintsUpdater.refresh(),
10 }, ctx.subscriptions); 10 null,
11 11 ctx.subscriptions
12 vscode.workspace.onDidChangeTextDocument(async e => { 12 );
13 if (e.contentChanges.length === 0) return; 13
14 if (e.document.languageId !== 'rust') return; 14 vscode.workspace.onDidChangeTextDocument(
15 await hintsUpdater.refresh(); 15 async event => {
16 }, ctx.subscriptions); 16 if (event.contentChanges.length !== 0) return;
17 17 if (event.document.languageId !== 'rust') return;
18 vscode.workspace.onDidChangeConfiguration(_ => { 18 await hintsUpdater.refresh();
19 hintsUpdater.setEnabled(ctx.config.displayInlayHints); 19 },
20 }, ctx.subscriptions); 20 null,
21 ctx.subscriptions
22 );
23
24 vscode.workspace.onDidChangeConfiguration(
25 async _ => hintsUpdater.setEnabled(ctx.config.displayInlayHints),
26 null,
27 ctx.subscriptions
28 );
21 29
22 ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints)); 30 ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints));
23} 31}
@@ -127,13 +135,13 @@ class HintsUpdater {
127 } 135 }
128 136
129 private async queryHints(documentUri: string): Promise<InlayHint[] | null> { 137 private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
130 let client = this.ctx.client; 138 const client = this.ctx.client;
131 if (!client) return null; 139 if (!client) return null;
132 const request: InlayHintsParams = { 140 const request: InlayHintsParams = {
133 textDocument: { uri: documentUri }, 141 textDocument: { uri: documentUri },
134 }; 142 };
135 let tokenSource = new vscode.CancellationTokenSource(); 143 const tokenSource = new vscode.CancellationTokenSource();
136 let prev = this.pending.get(documentUri); 144 const prev = this.pending.get(documentUri);
137 if (prev) prev.cancel(); 145 if (prev) prev.cancel();
138 this.pending.set(documentUri, tokenSource); 146 this.pending.set(documentUri, tokenSource);
139 try { 147 try {
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts
new file mode 100644
index 000000000..b51602ef9
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,34 @@
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`.
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 onProgress: (readBytes: number, totalBytes: number) => void
15): Promise<void> {
16 const response = await fetch(url);
17
18 const totalBytes = Number(response.headers.get('content-length'));
19 assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
20
21 let readBytes = 0;
22
23 console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
24
25 return new Promise<void>((resolve, reject) => response.body
26 .on("data", (chunk: Buffer) => {
27 readBytes += chunk.length;
28 onProgress(readBytes, totalBytes);
29 })
30 .on("end", resolve)
31 .on("error", reject)
32 .pipe(fs.createWriteStream(destFilePath))
33 );
34}
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..1ce67b8b2
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,141 @@
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 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}
57export 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/main.ts b/editors/code/src/main.ts
index 0494ccf63..5efce41f4 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -6,12 +6,12 @@ import { activateStatusDisplay } from './status_display';
6import { Ctx } from './ctx'; 6import { Ctx } from './ctx';
7import { activateHighlighting } from './highlighting'; 7import { activateHighlighting } from './highlighting';
8 8
9let ctx!: Ctx; 9let ctx: Ctx | undefined;
10 10
11export async function activate(context: vscode.ExtensionContext) { 11export async function activate(context: vscode.ExtensionContext) {
12 ctx = new Ctx(context); 12 ctx = new Ctx(context);
13 13
14 // Commands which invokes manually via command pallet, shortcut, etc. 14 // Commands which invokes manually via command palette, shortcut, etc.
15 ctx.registerCommand('analyzerStatus', commands.analyzerStatus); 15 ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
16 ctx.registerCommand('collectGarbage', commands.collectGarbage); 16 ctx.registerCommand('collectGarbage', commands.collectGarbage);
17 ctx.registerCommand('matchingBrace', commands.matchingBrace); 17 ctx.registerCommand('matchingBrace', commands.matchingBrace);
@@ -21,6 +21,7 @@ export async function activate(context: vscode.ExtensionContext) {
21 ctx.registerCommand('expandMacro', commands.expandMacro); 21 ctx.registerCommand('expandMacro', commands.expandMacro);
22 ctx.registerCommand('run', commands.run); 22 ctx.registerCommand('run', commands.run);
23 ctx.registerCommand('reload', commands.reload); 23 ctx.registerCommand('reload', commands.reload);
24 ctx.registerCommand('onEnter', commands.onEnter);
24 25
25 // Internal commands which are invoked by the server. 26 // Internal commands which are invoked by the server.
26 ctx.registerCommand('runSingle', commands.runSingle); 27 ctx.registerCommand('runSingle', commands.runSingle);
@@ -28,9 +29,6 @@ export async function activate(context: vscode.ExtensionContext) {
28 ctx.registerCommand('applySourceChange', commands.applySourceChange); 29 ctx.registerCommand('applySourceChange', commands.applySourceChange);
29 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 30 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
30 31
31 if (ctx.config.enableEnhancedTyping) {
32 ctx.overrideCommand('type', commands.onEnter);
33 }
34 activateStatusDisplay(ctx); 32 activateStatusDisplay(ctx);
35 33
36 activateHighlighting(ctx); 34 activateHighlighting(ctx);
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts
index c75fddf9d..51dbf388b 100644
--- a/editors/code/src/status_display.ts
+++ b/editors/code/src/status_display.ts
@@ -1,6 +1,6 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2 2
3import { WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd } from 'vscode-languageclient'; 3import { WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd, Disposable } from 'vscode-languageclient';
4 4
5import { Ctx } from './ctx'; 5import { Ctx } from './ctx';
6 6
@@ -9,15 +9,17 @@ const spinnerFrames = ['â ‹', 'â ™', 'â ¹', 'â ¸', 'â ¼', 'â ´', 'â ¦', 'â §', '
9export function activateStatusDisplay(ctx: Ctx) { 9export function activateStatusDisplay(ctx: Ctx) {
10 const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command); 10 const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command);
11 ctx.pushCleanup(statusDisplay); 11 ctx.pushCleanup(statusDisplay);
12 ctx.onDidRestart(client => { 12 ctx.onDidRestart(client => ctx.pushCleanup(client.onProgress(
13 client.onProgress(WorkDoneProgress.type, 'rustAnalyzer/cargoWatcher', params => statusDisplay.handleProgressNotification(params)); 13 WorkDoneProgress.type,
14 }); 14 'rustAnalyzer/cargoWatcher',
15 params => statusDisplay.handleProgressNotification(params)
16 )));
15} 17}
16 18
17class StatusDisplay implements vscode.Disposable { 19class StatusDisplay implements Disposable {
18 packageName?: string; 20 packageName?: string;
19 21
20 private i = 0; 22 private i: number = 0;
21 private statusBarItem: vscode.StatusBarItem; 23 private statusBarItem: vscode.StatusBarItem;
22 private command: string; 24 private command: string;
23 private timer?: NodeJS.Timeout; 25 private timer?: NodeJS.Timeout;
@@ -37,11 +39,8 @@ class StatusDisplay implements vscode.Disposable {
37 this.timer = 39 this.timer =
38 this.timer || 40 this.timer ||
39 setInterval(() => { 41 setInterval(() => {
40 if (this.packageName) { 42 this.tick();
41 this.statusBarItem!.text = `${this.frame()} cargo ${this.command} [${this.packageName}]`; 43 this.refreshLabel();
42 } else {
43 this.statusBarItem!.text = `${this.frame()} cargo ${this.command}`;
44 }
45 }, 300); 44 }, 300);
46 45
47 this.statusBarItem.show(); 46 this.statusBarItem.show();
@@ -65,6 +64,14 @@ class StatusDisplay implements vscode.Disposable {
65 this.statusBarItem.dispose(); 64 this.statusBarItem.dispose();
66 } 65 }
67 66
67 refreshLabel() {
68 if (this.packageName) {
69 this.statusBarItem!.text = `${spinnerFrames[this.i]} cargo ${this.command} [${this.packageName}]`;
70 } else {
71 this.statusBarItem!.text = `${spinnerFrames[this.i]} cargo ${this.command}`;
72 }
73 }
74
68 handleProgressNotification(params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd) { 75 handleProgressNotification(params: WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd) {
69 switch (params.kind) { 76 switch (params.kind) {
70 case 'begin': 77 case 'begin':
@@ -74,6 +81,7 @@ class StatusDisplay implements vscode.Disposable {
74 case 'report': 81 case 'report':
75 if (params.message) { 82 if (params.message) {
76 this.packageName = params.message; 83 this.packageName = params.message;
84 this.refreshLabel();
77 } 85 }
78 break; 86 break;
79 87
@@ -83,7 +91,7 @@ class StatusDisplay implements vscode.Disposable {
83 } 91 }
84 } 92 }
85 93
86 private frame() { 94 private tick() {
87 return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)]; 95 this.i = (this.i + 1) % spinnerFrames.length;
88 } 96 }
89} 97}