aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts29
-rw-r--r--editors/code/src/commands/runnables.ts4
-rw-r--r--editors/code/src/commands/syntax_tree.ts242
-rw-r--r--editors/code/src/config.ts34
-rw-r--r--editors/code/src/ctx.ts9
-rw-r--r--editors/code/src/highlighting.ts255
-rw-r--r--editors/code/src/inlay_hints.ts8
-rw-r--r--editors/code/src/main.ts22
-rw-r--r--editors/code/src/status_display.ts2
-rw-r--r--editors/code/src/tasks.ts52
-rw-r--r--editors/code/src/util.ts4
11 files changed, 287 insertions, 374 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 98f2f232f..3b1d00bca 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -5,15 +5,14 @@ import { Config } from './config';
5import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed'; 5import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed';
6import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed'; 6import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed';
7 7
8export async function createClient(config: Config, serverPath: string): Promise<lc.LanguageClient> { 8export async function createClient(config: Config, serverPath: string, cwd: string): Promise<lc.LanguageClient> {
9 // '.' Is the fallback if no folder is open 9 // '.' Is the fallback if no folder is open
10 // TODO?: Workspace folders support Uri's (eg: file://test.txt). 10 // TODO?: Workspace folders support Uri's (eg: file://test.txt).
11 // It might be a good idea to test if the uri points to a file. 11 // It might be a good idea to test if the uri points to a file.
12 const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
13 12
14 const run: lc.Executable = { 13 const run: lc.Executable = {
15 command: serverPath, 14 command: serverPath,
16 options: { cwd: workspaceFolderPath }, 15 options: { cwd },
17 }; 16 };
18 const serverOptions: lc.ServerOptions = { 17 const serverOptions: lc.ServerOptions = {
19 run, 18 run,
@@ -22,32 +21,10 @@ export async function createClient(config: Config, serverPath: string): Promise<
22 const traceOutputChannel = vscode.window.createOutputChannel( 21 const traceOutputChannel = vscode.window.createOutputChannel(
23 'Rust Analyzer Language Server Trace', 22 'Rust Analyzer Language Server Trace',
24 ); 23 );
25 const cargoWatchOpts = config.cargoWatchOptions;
26 24
27 const clientOptions: lc.LanguageClientOptions = { 25 const clientOptions: lc.LanguageClientOptions = {
28 documentSelector: [{ scheme: 'file', language: 'rust' }], 26 documentSelector: [{ scheme: 'file', language: 'rust' }],
29 initializationOptions: { 27 initializationOptions: vscode.workspace.getConfiguration("rust-analyzer"),
30 publishDecorations: !config.highlightingSemanticTokens,
31 lruCapacity: config.lruCapacity,
32
33 inlayHintsType: config.inlayHints.typeHints,
34 inlayHintsParameter: config.inlayHints.parameterHints,
35 inlayHintsChaining: config.inlayHints.chainingHints,
36 inlayHintsMaxLength: config.inlayHints.maxLength,
37
38 cargoWatchEnable: cargoWatchOpts.enable,
39 cargoWatchArgs: cargoWatchOpts.arguments,
40 cargoWatchCommand: cargoWatchOpts.command,
41 cargoWatchAllTargets: cargoWatchOpts.allTargets,
42
43 excludeGlobs: config.excludeGlobs,
44 useClientWatching: config.useClientWatching,
45 featureFlags: config.featureFlags,
46 withSysroot: config.withSysroot,
47 cargoFeatures: config.cargoFeatures,
48 rustfmtArgs: config.rustfmtArgs,
49 vscodeLldb: vscode.extensions.getExtension("vadimcn.vscode-lldb") != null,
50 },
51 traceOutputChannel, 28 traceOutputChannel,
52 middleware: { 29 middleware: {
53 // Workaround for https://github.com/microsoft/vscode-languageserver-node/issues/576 30 // Workaround for https://github.com/microsoft/vscode-languageserver-node/issues/576
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
index 357155163..2635a1440 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/commands/runnables.ts
@@ -66,6 +66,10 @@ export function debugSingle(ctx: Ctx): Cmd {
66 return async (config: ra.Runnable) => { 66 return async (config: ra.Runnable) => {
67 const editor = ctx.activeRustEditor; 67 const editor = ctx.activeRustEditor;
68 if (!editor) return; 68 if (!editor) return;
69 if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) {
70 vscode.window.showErrorMessage("Install `vadimcn.vscode-lldb` extension for debugging");
71 return;
72 }
69 73
70 const debugConfig = { 74 const debugConfig = {
71 type: "lldb", 75 type: "lldb",
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
index 2e08e8f11..b7a397414 100644
--- a/editors/code/src/commands/syntax_tree.ts
+++ b/editors/code/src/commands/syntax_tree.ts
@@ -1,8 +1,10 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as ra from '../rust-analyzer-api'; 2import * as ra from '../rust-analyzer-api';
3 3
4import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd, Disposable } from '../ctx';
5import { isRustDocument } from '../util'; 5import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6
7const AST_FILE_SCHEME = "rust-analyzer";
6 8
7// Opens the virtual file that will show the syntax tree 9// Opens the virtual file that will show the syntax tree
8// 10//
@@ -10,35 +12,16 @@ import { isRustDocument } from '../util';
10export function syntaxTree(ctx: Ctx): Cmd { 12export function syntaxTree(ctx: Ctx): Cmd {
11 const tdcp = new TextDocumentContentProvider(ctx); 13 const tdcp = new TextDocumentContentProvider(ctx);
12 14
13 ctx.pushCleanup( 15 void new AstInspector(ctx);
14 vscode.workspace.registerTextDocumentContentProvider( 16
15 'rust-analyzer', 17 ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
16 tdcp, 18 ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
17 ), 19 brackets: [["[", ")"]],
18 ); 20 }));
19
20 vscode.workspace.onDidChangeTextDocument(
21 (event: vscode.TextDocumentChangeEvent) => {
22 const doc = event.document;
23 if (!isRustDocument(doc)) return;
24 afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
25 },
26 null,
27 ctx.subscriptions,
28 );
29
30 vscode.window.onDidChangeActiveTextEditor(
31 (editor: vscode.TextEditor | undefined) => {
32 if (!editor || !isRustDocument(editor.document)) return;
33 tdcp.eventEmitter.fire(tdcp.uri);
34 },
35 null,
36 ctx.subscriptions,
37 );
38 21
39 return async () => { 22 return async () => {
40 const editor = vscode.window.activeTextEditor; 23 const editor = vscode.window.activeTextEditor;
41 const rangeEnabled = !!(editor && !editor.selection.isEmpty); 24 const rangeEnabled = !!editor && !editor.selection.isEmpty;
42 25
43 const uri = rangeEnabled 26 const uri = rangeEnabled
44 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) 27 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
@@ -48,45 +31,200 @@ export function syntaxTree(ctx: Ctx): Cmd {
48 31
49 tdcp.eventEmitter.fire(uri); 32 tdcp.eventEmitter.fire(uri);
50 33
51 return vscode.window.showTextDocument( 34 void await vscode.window.showTextDocument(document, {
52 document, 35 viewColumn: vscode.ViewColumn.Two,
53 vscode.ViewColumn.Two, 36 preserveFocus: true
54 true, 37 });
55 );
56 }; 38 };
57} 39}
58 40
59// We need to order this after LS updates, but there's no API for that.
60// Hence, good old setTimeout.
61function afterLs(f: () => void) {
62 setTimeout(f, 10);
63}
64
65
66class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { 41class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
67 uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); 42 readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
68 eventEmitter = new vscode.EventEmitter<vscode.Uri>(); 43 readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
44
69 45
70 constructor(private readonly ctx: Ctx) { 46 constructor(private readonly ctx: Ctx) {
47 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
48 vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
71 } 49 }
72 50
73 provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> { 51 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
74 const editor = vscode.window.activeTextEditor; 52 if (isRustDocument(event.document)) {
75 const client = this.ctx.client; 53 // We need to order this after language server updates, but there's no API for that.
76 if (!editor || !client) return ''; 54 // Hence, good old sleep().
55 void sleep(10).then(() => this.eventEmitter.fire(this.uri));
56 }
57 }
58 private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
59 if (editor && isRustEditor(editor)) {
60 this.eventEmitter.fire(this.uri);
61 }
62 }
63
64 provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
65 const rustEditor = this.ctx.activeRustEditor;
66 if (!rustEditor) return '';
77 67
78 // When the range based query is enabled we take the range of the selection 68 // When the range based query is enabled we take the range of the selection
79 const range = uri.query === 'range=true' && !editor.selection.isEmpty 69 const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
80 ? client.code2ProtocolConverter.asRange(editor.selection) 70 ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
81 : null; 71 : null;
82 72
83 return client.sendRequest(ra.syntaxTree, { 73 const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
84 textDocument: { uri: editor.document.uri.toString() }, 74 return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
85 range,
86 });
87 } 75 }
88 76
89 get onDidChange(): vscode.Event<vscode.Uri> { 77 get onDidChange(): vscode.Event<vscode.Uri> {
90 return this.eventEmitter.event; 78 return this.eventEmitter.event;
91 } 79 }
92} 80}
81
82
83// FIXME: consider implementing this via the Tree View API?
84// https://code.visualstudio.com/api/extension-guides/tree-view
85class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
86 private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87 borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
88 borderStyle: "solid",
89 borderWidth: "2px",
90
91 });
92 private rustEditor: undefined | RustEditor;
93
94 // Lazy rust token range -> syntax tree file range.
95 private readonly rust2Ast = new Lazy(() => {
96 const astEditor = this.findAstTextEditor();
97 if (!this.rustEditor || !astEditor) return undefined;
98
99 const buf: [vscode.Range, vscode.Range][] = [];
100 for (let i = 0; i < astEditor.document.lineCount; ++i) {
101 const astLine = astEditor.document.lineAt(i);
102
103 // Heuristically look for nodes with quoted text (which are token nodes)
104 const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
105 if (!isTokenNode) continue;
106
107 const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
108 if (!rustRange) continue;
109
110 buf.push([rustRange, this.findAstNodeRange(astLine)]);
111 }
112 return buf;
113 });
114
115 constructor(ctx: Ctx) {
116 ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
117 ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
118 vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
119 vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
120 vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
121
122 ctx.pushCleanup(this);
123 }
124 dispose() {
125 this.setRustEditor(undefined);
126 }
127
128 private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
129 if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
130 this.rust2Ast.reset();
131 }
132 }
133
134 private onDidCloseTextDocument(doc: vscode.TextDocument) {
135 if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
136 this.setRustEditor(undefined);
137 }
138 }
139
140 private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
141 if (!this.findAstTextEditor()) {
142 this.setRustEditor(undefined);
143 return;
144 }
145 this.setRustEditor(editors.find(isRustEditor));
146 }
147
148 private findAstTextEditor(): undefined | vscode.TextEditor {
149 return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME);
150 }
151
152 private setRustEditor(newRustEditor: undefined | RustEditor) {
153 if (this.rustEditor && this.rustEditor !== newRustEditor) {
154 this.rustEditor.setDecorations(this.astDecorationType, []);
155 this.rust2Ast.reset();
156 }
157 this.rustEditor = newRustEditor;
158 }
159
160 // additional positional params are omitted
161 provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
162 if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
163
164 const astEditor = this.findAstTextEditor();
165 if (!astEditor) return;
166
167 const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
168 if (!rust2AstRanges) return;
169
170 const [rustFileRange, astFileRange] = rust2AstRanges;
171
172 astEditor.revealRange(astFileRange);
173 astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
174
175 return [{
176 targetRange: astFileRange,
177 targetUri: astEditor.document.uri,
178 originSelectionRange: rustFileRange,
179 targetSelectionRange: astFileRange,
180 }];
181 }
182
183 // additional positional params are omitted
184 provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
185 if (!this.rustEditor) return;
186
187 const astFileLine = doc.lineAt(hoverPosition.line);
188
189 const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
190 if (!rustFileRange) return;
191
192 this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
193 this.rustEditor.revealRange(rustFileRange);
194
195 const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
196 const astFileRange = this.findAstNodeRange(astFileLine);
197
198 return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
199 }
200
201 private findAstNodeRange(astLine: vscode.TextLine) {
202 const lineOffset = astLine.range.start;
203 const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
204 const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
205 return new vscode.Range(begin, end);
206 }
207
208 private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
209 const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine);
210 if (!parsedRange) return;
211
212 const [begin, end] = parsedRange.slice(1).map(off => doc.positionAt(+off));
213
214 return new vscode.Range(begin, end);
215 }
216}
217
218class Lazy<T> {
219 val: undefined | T;
220
221 constructor(private readonly compute: () => undefined | T) { }
222
223 get() {
224 return this.val ?? (this.val = this.compute());
225 }
226
227 reset() {
228 this.val = undefined;
229 }
230}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index e77462c1b..1f45f1de0 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -11,10 +11,9 @@ export class Config {
11 private readonly rootSection = "rust-analyzer"; 11 private readonly rootSection = "rust-analyzer";
12 private readonly requiresReloadOpts = [ 12 private readonly requiresReloadOpts = [
13 "serverPath", 13 "serverPath",
14 "cargoFeatures", 14 "cargo",
15 "cargo-watch", 15 "files",
16 "highlighting.semanticTokens", 16 "highlighting",
17 "inlayHints",
18 "updates.channel", 17 "updates.channel",
19 ] 18 ]
20 .map(opt => `${this.rootSection}.${opt}`); 19 .map(opt => `${this.rootSection}.${opt}`);
@@ -71,19 +70,8 @@ export class Config {
71 get channel() { return this.cfg.get<UpdatesChannel>("updates.channel")!; } 70 get channel() { return this.cfg.get<UpdatesChannel>("updates.channel")!; }
72 get askBeforeDownload() { return this.cfg.get<boolean>("updates.askBeforeDownload")!; } 71 get askBeforeDownload() { return this.cfg.get<boolean>("updates.askBeforeDownload")!; }
73 get highlightingSemanticTokens() { return this.cfg.get<boolean>("highlighting.semanticTokens")!; } 72 get highlightingSemanticTokens() { return this.cfg.get<boolean>("highlighting.semanticTokens")!; }
74 get highlightingOn() { return this.cfg.get<boolean>("highlightingOn")!; }
75 get rainbowHighlightingOn() { return this.cfg.get<boolean>("rainbowHighlightingOn")!; }
76 get lruCapacity() { return this.cfg.get<null | number>("lruCapacity")!; }
77 get excludeGlobs() { return this.cfg.get<string[]>("excludeGlobs")!; }
78 get useClientWatching() { return this.cfg.get<boolean>("useClientWatching")!; }
79 get featureFlags() { return this.cfg.get<Record<string, boolean>>("featureFlags")!; }
80 get rustfmtArgs() { return this.cfg.get<string[]>("rustfmtArgs")!; }
81 get loadOutDirsFromCheck() { return this.cfg.get<boolean>("loadOutDirsFromCheck")!; }
82 get traceExtension() { return this.cfg.get<boolean>("trace.extension")!; } 73 get traceExtension() { return this.cfg.get<boolean>("trace.extension")!; }
83 74
84 // for internal use
85 get withSysroot() { return this.cfg.get<boolean>("withSysroot", true)!; }
86
87 get inlayHints() { 75 get inlayHints() {
88 return { 76 return {
89 typeHints: this.cfg.get<boolean>("inlayHints.typeHints")!, 77 typeHints: this.cfg.get<boolean>("inlayHints.typeHints")!,
@@ -93,21 +81,9 @@ export class Config {
93 }; 81 };
94 } 82 }
95 83
96 get cargoWatchOptions() { 84 get checkOnSave() {
97 return {
98 enable: this.cfg.get<boolean>("cargo-watch.enable")!,
99 arguments: this.cfg.get<string[]>("cargo-watch.arguments")!,
100 allTargets: this.cfg.get<boolean>("cargo-watch.allTargets")!,
101 command: this.cfg.get<string>("cargo-watch.command")!,
102 };
103 }
104
105 get cargoFeatures() {
106 return { 85 return {
107 noDefaultFeatures: this.cfg.get<boolean>("cargoFeatures.noDefaultFeatures")!, 86 command: this.cfg.get<string>("checkOnSave.command")!,
108 allFeatures: this.cfg.get<boolean>("cargoFeatures.allFeatures")!,
109 features: this.cfg.get<string[]>("cargoFeatures.features")!,
110 loadOutDirsFromCheck: this.cfg.get<boolean>("cargoFeatures.loadOutDirsFromCheck")!,
111 }; 87 };
112 } 88 }
113} 89}
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 84c170ea8..bd1c3de07 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -15,8 +15,13 @@ export class Ctx {
15 15
16 } 16 }
17 17
18 static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { 18 static async create(
19 const client = await createClient(config, serverPath); 19 config: Config,
20 extCtx: vscode.ExtensionContext,
21 serverPath: string,
22 cwd: string,
23 ): Promise<Ctx> {
24 const client = await createClient(config, serverPath, cwd);
20 const res = new Ctx(config, extCtx, client, serverPath); 25 const res = new Ctx(config, extCtx, client, serverPath);
21 res.pushCleanup(client.start()); 26 res.pushCleanup(client.start());
22 await client.onReady(); 27 await client.onReady();
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
deleted file mode 100644
index ea2dfc0e3..000000000
--- a/editors/code/src/highlighting.ts
+++ /dev/null
@@ -1,255 +0,0 @@
1import * as vscode from 'vscode';
2import * as ra from './rust-analyzer-api';
3
4import { ColorTheme, TextMateRuleSettings } from './color_theme';
5
6import { Ctx } from './ctx';
7import { sendRequestWithRetry, isRustDocument } from './util';
8
9export function activateHighlighting(ctx: Ctx) {
10 const highlighter = new Highlighter(ctx);
11
12 ctx.client.onNotification(ra.publishDecorations, params => {
13 if (!ctx.config.highlightingOn) return;
14
15 const targetEditor = vscode.window.visibleTextEditors.find(
16 editor => {
17 const unescapedUri = unescape(
18 editor.document.uri.toString(),
19 );
20 // Unescaped URI looks like:
21 // file:///c:/Workspace/ra-test/src/main.rs
22 return unescapedUri === params.uri;
23 },
24 );
25 if (!targetEditor) return;
26
27 highlighter.setHighlights(targetEditor, params.decorations);
28 });
29
30
31 vscode.workspace.onDidChangeConfiguration(
32 _ => highlighter.removeHighlights(),
33 null,
34 ctx.subscriptions,
35 );
36
37 vscode.window.onDidChangeActiveTextEditor(
38 async (editor: vscode.TextEditor | undefined) => {
39 if (!editor || !isRustDocument(editor.document)) return;
40 if (!ctx.config.highlightingOn) return;
41 const client = ctx.client;
42 if (!client) return;
43
44 const decorations = await sendRequestWithRetry(
45 client,
46 ra.decorationsRequest,
47 { uri: editor.document.uri.toString() },
48 );
49 highlighter.setHighlights(editor, decorations);
50 },
51 null,
52 ctx.subscriptions,
53 );
54}
55
56// Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76
57function fancify(seed: string, shade: 'light' | 'dark') {
58 const random = randomU32Numbers(hashString(seed));
59 const randomInt = (min: number, max: number) => {
60 return Math.abs(random()) % (max - min + 1) + min;
61 };
62
63 const h = randomInt(0, 360);
64 const s = randomInt(42, 98);
65 const l = shade === 'light' ? randomInt(15, 40) : randomInt(40, 90);
66 return `hsl(${h},${s}%,${l}%)`;
67}
68
69class Highlighter {
70 private ctx: Ctx;
71 private decorations: Map<
72 string,
73 vscode.TextEditorDecorationType
74 > | null = null;
75
76 constructor(ctx: Ctx) {
77 this.ctx = ctx;
78 }
79
80 public removeHighlights() {
81 if (this.decorations == null) {
82 return;
83 }
84
85 // Decorations are removed when the object is disposed
86 for (const decoration of this.decorations.values()) {
87 decoration.dispose();
88 }
89
90 this.decorations = null;
91 }
92
93 public setHighlights(editor: vscode.TextEditor, highlights: ra.Decoration[]) {
94 const client = this.ctx.client;
95 if (!client) return;
96 // Initialize decorations if necessary
97 //
98 // Note: decoration objects need to be kept around so we can dispose them
99 // if the user disables syntax highlighting
100 if (this.decorations == null) {
101 this.decorations = initDecorations();
102 }
103
104 const byTag: Map<string, vscode.Range[]> = new Map();
105 const colorfulIdents: Map<
106 string,
107 [vscode.Range[], boolean]
108 > = new Map();
109 const rainbowTime = this.ctx.config.rainbowHighlightingOn;
110
111 for (const tag of this.decorations.keys()) {
112 byTag.set(tag, []);
113 }
114
115 for (const d of highlights) {
116 if (!byTag.get(d.tag)) {
117 continue;
118 }
119
120 if (rainbowTime && d.bindingHash) {
121 if (!colorfulIdents.has(d.bindingHash)) {
122 const mut = d.tag.endsWith('.mut');
123 colorfulIdents.set(d.bindingHash, [[], mut]);
124 }
125 colorfulIdents
126 .get(d.bindingHash)![0]
127 .push(
128 client.protocol2CodeConverter.asRange(d.range),
129 );
130 } else {
131 byTag
132 .get(d.tag)!
133 .push(
134 client.protocol2CodeConverter.asRange(d.range),
135 );
136 }
137 }
138
139 for (const tag of byTag.keys()) {
140 const dec = this.decorations.get(
141 tag,
142 ) as vscode.TextEditorDecorationType;
143 const ranges = byTag.get(tag)!;
144 editor.setDecorations(dec, ranges);
145 }
146
147 for (const [hash, [ranges, mut]] of colorfulIdents.entries()) {
148 const textDecoration = mut ? 'underline' : undefined;
149 const dec = vscode.window.createTextEditorDecorationType({
150 light: { color: fancify(hash, 'light'), textDecoration },
151 dark: { color: fancify(hash, 'dark'), textDecoration },
152 });
153 editor.setDecorations(dec, ranges);
154 }
155 }
156}
157
158function initDecorations(): Map<string, vscode.TextEditorDecorationType> {
159 const theme = ColorTheme.load();
160 const res = new Map();
161 TAG_TO_SCOPES.forEach((scopes, tag) => {
162 // We are going to axe this soon, so don't try to detect unknown tags.
163 // Users should switch to the new semantic tokens implementation.
164 if (!scopes) return;
165 const rule = theme.lookup(scopes);
166 const decor = createDecorationFromTextmate(rule);
167 res.set(tag, decor);
168 });
169 return res;
170}
171
172function createDecorationFromTextmate(
173 themeStyle: TextMateRuleSettings,
174): vscode.TextEditorDecorationType {
175 const decorationOptions: vscode.DecorationRenderOptions = {};
176 decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;
177
178 if (themeStyle.foreground) {
179 decorationOptions.color = themeStyle.foreground;
180 }
181
182 if (themeStyle.background) {
183 decorationOptions.backgroundColor = themeStyle.background;
184 }
185
186 if (themeStyle.fontStyle) {
187 const parts: string[] = themeStyle.fontStyle.split(' ');
188 parts.forEach(part => {
189 switch (part) {
190 case 'italic':
191 decorationOptions.fontStyle = 'italic';
192 break;
193 case 'bold':
194 decorationOptions.fontWeight = 'bold';
195 break;
196 case 'underline':
197 decorationOptions.textDecoration = 'underline';
198 break;
199 default:
200 break;
201 }
202 });
203 }
204 return vscode.window.createTextEditorDecorationType(decorationOptions);
205}
206
207// sync with tags from `syntax_highlighting.rs`.
208const TAG_TO_SCOPES = new Map<string, string[]>([
209 ["field", ["entity.name.field"]],
210 ["function", ["entity.name.function"]],
211 ["module", ["entity.name.module"]],
212 ["constant", ["entity.name.constant"]],
213 ["macro", ["entity.name.macro"]],
214
215 ["variable", ["variable"]],
216 ["variable.mutable", ["variable", "meta.mutable"]],
217
218 ["type", ["entity.name.type"]],
219 ["type.builtin", ["entity.name.type", "support.type.primitive"]],
220 ["type.self", ["entity.name.type.parameter.self"]],
221 ["type.param", ["entity.name.type.parameter", "entity.name.type.param.rust"]],
222 ["type.lifetime", ["entity.name.type.lifetime", "entity.name.lifetime.rust"]],
223
224 ["literal.byte", ["constant.character.byte"]],
225 ["literal.char", ["constant.character.rust"]],
226 ["numeric_literal", ["constant.numeric"]],
227
228 ["comment", ["comment"]],
229 ["string_literal", ["string.quoted"]],
230 ["attribute", ["meta.attribute.rust"]],
231
232 ["keyword", ["keyword"]],
233 ["keyword.unsafe", ["keyword.other.unsafe"]],
234 ["keyword.control", ["keyword.control"]],
235]);
236
237function randomU32Numbers(seed: number) {
238 let random = seed | 0;
239 return () => {
240 random ^= random << 13;
241 random ^= random >> 17;
242 random ^= random << 5;
243 random |= 0;
244 return random;
245 };
246}
247
248function hashString(str: string): number {
249 let res = 0;
250 for (let i = 0; i < str.length; ++i) {
251 const c = str.codePointAt(i)!;
252 res = (res * 31 + c) & ~0;
253 }
254 return res;
255}
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index 542d1f367..6a8bd942e 100644
--- a/editors/code/src/inlay_hints.ts
+++ b/editors/code/src/inlay_hints.ts
@@ -17,7 +17,11 @@ export function activateInlayHints(ctx: Ctx) {
17 ) { 17 ) {
18 return this.dispose(); 18 return this.dispose();
19 } 19 }
20 if (!this.updater) this.updater = new HintsUpdater(ctx); 20 if (this.updater) {
21 this.updater.syncCacheAndRenderHints();
22 } else {
23 this.updater = new HintsUpdater(ctx);
24 }
21 }, 25 },
22 dispose() { 26 dispose() {
23 this.updater?.dispose(); 27 this.updater?.dispose();
@@ -124,7 +128,7 @@ class HintsUpdater implements Disposable {
124 this.syncCacheAndRenderHints(); 128 this.syncCacheAndRenderHints();
125 } 129 }
126 130
127 private syncCacheAndRenderHints() { 131 syncCacheAndRenderHints() {
128 // FIXME: make inlayHints request pass an array of files? 132 // FIXME: make inlayHints request pass an array of files?
129 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => { 133 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
130 if (!hints) return; 134 if (!hints) return;
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 980ed925b..4f3b89f44 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -7,12 +7,12 @@ import * as commands from './commands';
7import { activateInlayHints } from './inlay_hints'; 7import { activateInlayHints } from './inlay_hints';
8import { activateStatusDisplay } from './status_display'; 8import { activateStatusDisplay } from './status_display';
9import { Ctx } from './ctx'; 9import { Ctx } from './ctx';
10import { activateHighlighting } from './highlighting';
11import { Config, NIGHTLY_TAG } from './config'; 10import { Config, NIGHTLY_TAG } from './config';
12import { log, assert } from './util'; 11import { log, assert } from './util';
13import { PersistentState } from './persistent_state'; 12import { PersistentState } from './persistent_state';
14import { fetchRelease, download } from './net'; 13import { fetchRelease, download } from './net';
15import { spawnSync } from 'child_process'; 14import { spawnSync } from 'child_process';
15import { activateTaskProvider } from './tasks';
16 16
17let ctx: Ctx | undefined; 17let ctx: Ctx | undefined;
18 18
@@ -41,11 +41,18 @@ export async function activate(context: vscode.ExtensionContext) {
41 const state = new PersistentState(context.globalState); 41 const state = new PersistentState(context.globalState);
42 const serverPath = await bootstrap(config, state); 42 const serverPath = await bootstrap(config, state);
43 43
44 const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
45 if (workspaceFolder === undefined) {
46 const err = "Cannot activate rust-analyzer when no folder is opened";
47 void vscode.window.showErrorMessage(err);
48 throw new Error(err);
49 }
50
44 // Note: we try to start the server before we activate type hints so that it 51 // Note: we try to start the server before we activate type hints so that it
45 // registers its `onDidChangeDocument` handler before us. 52 // registers its `onDidChangeDocument` handler before us.
46 // 53 //
47 // This a horribly, horribly wrong way to deal with this problem. 54 // This a horribly, horribly wrong way to deal with this problem.
48 ctx = await Ctx.create(config, context, serverPath); 55 ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
49 56
50 // Commands which invokes manually via command palette, shortcut, etc. 57 // Commands which invokes manually via command palette, shortcut, etc.
51 58
@@ -85,12 +92,17 @@ export async function activate(context: vscode.ExtensionContext) {
85 ctx.registerCommand('applySourceChange', commands.applySourceChange); 92 ctx.registerCommand('applySourceChange', commands.applySourceChange);
86 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); 93 ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
87 94
95 ctx.pushCleanup(activateTaskProvider(workspaceFolder));
96
88 activateStatusDisplay(ctx); 97 activateStatusDisplay(ctx);
89 98
90 if (!ctx.config.highlightingSemanticTokens) {
91 activateHighlighting(ctx);
92 }
93 activateInlayHints(ctx); 99 activateInlayHints(ctx);
100
101 vscode.workspace.onDidChangeConfiguration(
102 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
103 null,
104 ctx.subscriptions,
105 );
94} 106}
95 107
96export async function deactivate() { 108export async function deactivate() {
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts
index 0f5f6ef99..f9cadc8a2 100644
--- a/editors/code/src/status_display.ts
+++ b/editors/code/src/status_display.ts
@@ -7,7 +7,7 @@ import { Ctx } from './ctx';
7const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; 7const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8 8
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.checkOnSave.command);
11 ctx.pushCleanup(statusDisplay); 11 ctx.pushCleanup(statusDisplay);
12 const client = ctx.client; 12 const client = ctx.client;
13 if (client != null) { 13 if (client != null) {
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
new file mode 100644
index 000000000..fa1c4a951
--- /dev/null
+++ b/editors/code/src/tasks.ts
@@ -0,0 +1,52 @@
1import * as vscode from 'vscode';
2
3// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
4// our configuration should be compatible with it so use the same key.
5const TASK_TYPE = 'cargo';
6
7export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable {
8 const provider: vscode.TaskProvider = {
9 // Detect Rust tasks. Currently we do not do any actual detection
10 // of tasks (e.g. aliases in .cargo/config) and just return a fixed
11 // set of tasks that always exist. These tasks cannot be removed in
12 // tasks.json - only tweaked.
13 provideTasks: () => getStandardCargoTasks(target),
14
15 // We don't need to implement this.
16 resolveTask: () => undefined,
17 };
18
19 return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
20}
21
22function getStandardCargoTasks(target: vscode.WorkspaceFolder): vscode.Task[] {
23 return [
24 { command: 'build', group: vscode.TaskGroup.Build },
25 { command: 'check', group: vscode.TaskGroup.Build },
26 { command: 'test', group: vscode.TaskGroup.Test },
27 { command: 'clean', group: vscode.TaskGroup.Clean },
28 { command: 'run', group: undefined },
29 ]
30 .map(({ command, group }) => {
31 const vscodeTask = new vscode.Task(
32 // The contents of this object end up in the tasks.json entries.
33 {
34 type: TASK_TYPE,
35 command,
36 },
37 // The scope of the task - workspace or specific folder (global
38 // is not supported).
39 target,
40 // The task name, and task source. These are shown in the UI as
41 // `${source}: ${name}`, e.g. `rust: cargo build`.
42 `cargo ${command}`,
43 'rust',
44 // What to do when this command is executed.
45 new vscode.ShellExecution('cargo', [command]),
46 // Problem matchers.
47 ['$rustc'],
48 );
49 vscodeTask.group = group;
50 return vscodeTask;
51 });
52}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 978a31751..6f91f81d6 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -65,12 +65,12 @@ export async function sendRequestWithRetry<TParam, TRet>(
65 throw 'unreachable'; 65 throw 'unreachable';
66} 66}
67 67
68function sleep(ms: number) { 68export function sleep(ms: number) {
69 return new Promise(resolve => setTimeout(resolve, ms)); 69 return new Promise(resolve => setTimeout(resolve, ms));
70} 70}
71 71
72export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 72export type RustDocument = vscode.TextDocument & { languageId: "rust" };
73export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; 73export type RustEditor = vscode.TextEditor & { document: RustDocument };
74 74
75export function isRustDocument(document: vscode.TextDocument): document is RustDocument { 75export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
76 return document.languageId === 'rust' 76 return document.languageId === 'rust'