aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts90
-rw-r--r--editors/code/src/color_theme.ts123
-rw-r--r--editors/code/src/commands/analyzer_status.ts74
-rw-r--r--editors/code/src/commands/cargo_watch.ts264
-rw-r--r--editors/code/src/commands/expand_macro.ts103
-rw-r--r--editors/code/src/commands/index.ts66
-rw-r--r--editors/code/src/commands/inlay_hints.ts115
-rw-r--r--editors/code/src/commands/join_lines.ts46
-rw-r--r--editors/code/src/commands/line_buffer.ts16
-rw-r--r--editors/code/src/commands/matching_brace.ts58
-rw-r--r--editors/code/src/commands/on_enter.ts53
-rw-r--r--editors/code/src/commands/parent_module.ts55
-rw-r--r--editors/code/src/commands/runnables.ts214
-rw-r--r--editors/code/src/commands/syntaxTree.ts76
-rw-r--r--editors/code/src/commands/syntax_tree.ts104
-rw-r--r--editors/code/src/commands/watch_status.ts63
-rw-r--r--editors/code/src/config.ts87
-rw-r--r--editors/code/src/ctx.ts112
-rw-r--r--editors/code/src/events/change_active_text_editor.ts32
-rw-r--r--editors/code/src/events/change_text_document.ts24
-rw-r--r--editors/code/src/events/index.ts4
-rw-r--r--editors/code/src/extension.ts218
-rw-r--r--editors/code/src/highlighting.ts209
-rw-r--r--editors/code/src/inlay_hints.ts120
-rw-r--r--editors/code/src/main.ts51
-rw-r--r--editors/code/src/notifications/index.ts3
-rw-r--r--editors/code/src/notifications/publish_decorations.ts24
-rw-r--r--editors/code/src/server.ts109
-rw-r--r--editors/code/src/source_change.ts (renamed from editors/code/src/commands/apply_source_change.ts)13
-rw-r--r--editors/code/src/status_display.ts115
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json110
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json42
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json114
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json261
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json33
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json72
-rw-r--r--editors/code/src/test/runTest.ts22
-rw-r--r--editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts134
-rw-r--r--editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts127
-rw-r--r--editors/code/src/test/utils/diagnotics/rust.test.ts236
-rw-r--r--editors/code/src/test/utils/diagnotics/vscode.test.ts98
-rw-r--r--editors/code/src/test/utils/index.ts49
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFix.ts67
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFixCollection.ts77
-rw-r--r--editors/code/src/utils/diagnostics/rust.ts299
-rw-r--r--editors/code/src/utils/diagnostics/vscode.ts14
-rw-r--r--editors/code/src/utils/processes.ts51
-rw-r--r--editors/code/src/utils/terminateProcess.sh12
48 files changed, 1208 insertions, 3251 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
new file mode 100644
index 000000000..743384bd7
--- /dev/null
+++ b/editors/code/src/client.ts
@@ -0,0 +1,90 @@
1import { homedir } from 'os';
2import * as lc from 'vscode-languageclient';
3
4import { window, workspace } from 'vscode';
5import { Config } from './config';
6
7export function createClient(config: Config): lc.LanguageClient {
8 // '.' Is the fallback if no folder is open
9 // 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.
10 let folder: string = '.';
11 if (workspace.workspaceFolders !== undefined) {
12 folder = workspace.workspaceFolders[0].uri.fsPath.toString();
13 }
14
15 const command = expandPathResolving(config.raLspServerPath);
16 const run: lc.Executable = {
17 command,
18 options: { cwd: folder },
19 };
20 const serverOptions: lc.ServerOptions = {
21 run,
22 debug: run,
23 };
24 const traceOutputChannel = window.createOutputChannel(
25 'Rust Analyzer Language Server Trace',
26 );
27 const clientOptions: lc.LanguageClientOptions = {
28 documentSelector: [{ scheme: 'file', language: 'rust' }],
29 initializationOptions: {
30 publishDecorations: true,
31 lruCapacity: config.lruCapacity,
32 maxInlayHintLength: config.maxInlayHintLength,
33 cargoWatchEnable: config.cargoWatchOptions.enable,
34 cargoWatchArgs: config.cargoWatchOptions.arguments,
35 cargoWatchCommand: config.cargoWatchOptions.command,
36 cargoWatchAllTargets:
37 config.cargoWatchOptions.allTargets,
38 excludeGlobs: config.excludeGlobs,
39 useClientWatching: config.useClientWatching,
40 featureFlags: config.featureFlags,
41 withSysroot: config.withSysroot,
42 cargoFeatures: config.cargoFeatures,
43 },
44 traceOutputChannel,
45 };
46
47 const res = new lc.LanguageClient(
48 'rust-analyzer',
49 'Rust Analyzer Language Server',
50 serverOptions,
51 clientOptions,
52 );
53
54 // HACK: This is an awful way of filtering out the decorations notifications
55 // However, pending proper support, this is the most effecitve approach
56 // Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages
57 // Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting)
58 // This also requires considering our settings strategy, which is work which needs doing
59 // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests
60 res._tracer = {
61 log: (messageOrDataObject: string | any, data?: string) => {
62 if (typeof messageOrDataObject === 'string') {
63 if (
64 messageOrDataObject.includes(
65 'rust-analyzer/publishDecorations',
66 ) ||
67 messageOrDataObject.includes(
68 'rust-analyzer/decorationsRequest',
69 )
70 ) {
71 // Don't log publish decorations requests
72 } else {
73 // @ts-ignore This is just a utility function
74 res.logTrace(messageOrDataObject, data);
75 }
76 } else {
77 // @ts-ignore
78 res.logObjectTrace(messageOrDataObject);
79 }
80 },
81 };
82 res.registerProposedFeatures();
83 return res;
84}
85function expandPathResolving(path: string) {
86 if (path.startsWith('~/')) {
87 return path.replace('~', homedir());
88 }
89 return path;
90}
diff --git a/editors/code/src/color_theme.ts b/editors/code/src/color_theme.ts
new file mode 100644
index 000000000..cbad47f35
--- /dev/null
+++ b/editors/code/src/color_theme.ts
@@ -0,0 +1,123 @@
1import * as fs from 'fs';
2import * as jsonc from 'jsonc-parser';
3import * as path from 'path';
4import * as vscode from 'vscode';
5
6export interface TextMateRuleSettings {
7 foreground?: string;
8 background?: string;
9 fontStyle?: string;
10}
11
12export class ColorTheme {
13 private rules: Map<string, TextMateRuleSettings> = new Map();
14
15 static load(): ColorTheme {
16 // Find out current color theme
17 const themeName = vscode.workspace
18 .getConfiguration('workbench')
19 .get('colorTheme');
20
21 if (typeof themeName !== 'string') {
22 // console.warn('workbench.colorTheme is', themeName)
23 return new ColorTheme();
24 }
25 return loadThemeNamed(themeName);
26 }
27
28 static fromRules(rules: TextMateRule[]): ColorTheme {
29 const res = new ColorTheme();
30 for (const rule of rules) {
31 const scopes = typeof rule.scope === 'string'
32 ? [rule.scope]
33 : rule.scope;
34 for (const scope of scopes) {
35 res.rules.set(scope, rule.settings);
36 }
37 }
38 return res;
39 }
40
41 lookup(scopes: string[]): TextMateRuleSettings {
42 let res: TextMateRuleSettings = {};
43 for (const scope of scopes) {
44 this.rules.forEach((value, key) => {
45 if (scope.startsWith(key)) {
46 res = mergeRuleSettings(res, value);
47 }
48 });
49 }
50 return res;
51 }
52
53 mergeFrom(other: ColorTheme) {
54 other.rules.forEach((value, key) => {
55 const merged = mergeRuleSettings(this.rules.get(key), value);
56 this.rules.set(key, merged);
57 });
58 }
59}
60
61function loadThemeNamed(themeName: string): ColorTheme {
62 function isTheme(extension: vscode.Extension<any>): boolean {
63 return (
64 extension.extensionKind === vscode.ExtensionKind.UI &&
65 extension.packageJSON.contributes &&
66 extension.packageJSON.contributes.themes
67 );
68 }
69
70 let themePaths = vscode.extensions.all
71 .filter(isTheme)
72 .flatMap(ext => {
73 return ext.packageJSON.contributes.themes
74 .filter((it: any) => (it.id || it.label) === themeName)
75 .map((it: any) => path.join(ext.extensionPath, it.path));
76 });
77
78 const res = new ColorTheme();
79 for (const themePath of themePaths) {
80 res.mergeFrom(loadThemeFile(themePath));
81 }
82
83 const customizations: any = vscode.workspace.getConfiguration('editor').get('tokenColorCustomizations');
84 res.mergeFrom(ColorTheme.fromRules(customizations?.textMateRules ?? []));
85
86 return res;
87}
88
89function loadThemeFile(themePath: string): ColorTheme {
90 let text;
91 try {
92 text = fs.readFileSync(themePath, 'utf8');
93 } catch {
94 return new ColorTheme();
95 }
96 const obj = jsonc.parse(text);
97 const tokenColors = obj?.tokenColors ?? [];
98 const res = ColorTheme.fromRules(tokenColors);
99
100 for (const include in obj?.include ?? []) {
101 const includePath = path.join(path.dirname(themePath), include);
102 const tmp = loadThemeFile(includePath);
103 res.mergeFrom(tmp);
104 }
105
106 return res;
107}
108
109interface TextMateRule {
110 scope: string | string[];
111 settings: TextMateRuleSettings;
112}
113
114function mergeRuleSettings(
115 defaultSetting: TextMateRuleSettings | undefined,
116 override: TextMateRuleSettings,
117): TextMateRuleSettings {
118 return {
119 foreground: override.foreground ?? defaultSetting?.foreground,
120 background: override.background ?? defaultSetting?.background,
121 fontStyle: override.fontStyle ?? defaultSetting?.fontStyle,
122 };
123}
diff --git a/editors/code/src/commands/analyzer_status.ts b/editors/code/src/commands/analyzer_status.ts
index 2777ced24..cfe7d1af0 100644
--- a/editors/code/src/commands/analyzer_status.ts
+++ b/editors/code/src/commands/analyzer_status.ts
@@ -1,45 +1,20 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import { Server } from '../server';
3 2
4const statusUri = vscode.Uri.parse('rust-analyzer-status://status'); 3import { Ctx, Cmd } from '../ctx';
5
6export class TextDocumentContentProvider
7 implements vscode.TextDocumentContentProvider {
8 public eventEmitter = new vscode.EventEmitter<vscode.Uri>();
9 public syntaxTree: string = 'Not available';
10
11 public provideTextDocumentContent(
12 _uri: vscode.Uri,
13 ): vscode.ProviderResult<string> {
14 const editor = vscode.window.activeTextEditor;
15 if (editor == null) {
16 return '';
17 }
18 return Server.client.sendRequest<string>(
19 'rust-analyzer/analyzerStatus',
20 null,
21 );
22 }
23
24 get onDidChange(): vscode.Event<vscode.Uri> {
25 return this.eventEmitter.event;
26 }
27}
28
29let poller: NodeJS.Timer | null = null;
30 4
31// Shows status of rust-analyzer (for debugging) 5// Shows status of rust-analyzer (for debugging)
6export function analyzerStatus(ctx: Ctx): Cmd {
7 let poller: NodeJS.Timer | null = null;
8 const tdcp = new TextDocumentContentProvider(ctx);
32 9
33export function makeCommand(context: vscode.ExtensionContext) { 10 ctx.pushCleanup(
34 const textDocumentContentProvider = new TextDocumentContentProvider();
35 context.subscriptions.push(
36 vscode.workspace.registerTextDocumentContentProvider( 11 vscode.workspace.registerTextDocumentContentProvider(
37 'rust-analyzer-status', 12 'rust-analyzer-status',
38 textDocumentContentProvider, 13 tdcp,
39 ), 14 ),
40 ); 15 );
41 16
42 context.subscriptions.push({ 17 ctx.pushCleanup({
43 dispose() { 18 dispose() {
44 if (poller != null) { 19 if (poller != null) {
45 clearInterval(poller); 20 clearInterval(poller);
@@ -49,12 +24,9 @@ export function makeCommand(context: vscode.ExtensionContext) {
49 24
50 return async function handle() { 25 return async function handle() {
51 if (poller == null) { 26 if (poller == null) {
52 poller = setInterval( 27 poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
53 () => textDocumentContentProvider.eventEmitter.fire(statusUri),
54 1000,
55 );
56 } 28 }
57 const document = await vscode.workspace.openTextDocument(statusUri); 29 const document = await vscode.workspace.openTextDocument(tdcp.uri);
58 return vscode.window.showTextDocument( 30 return vscode.window.showTextDocument(
59 document, 31 document,
60 vscode.ViewColumn.Two, 32 vscode.ViewColumn.Two,
@@ -62,3 +34,31 @@ export function makeCommand(context: vscode.ExtensionContext) {
62 ); 34 );
63 }; 35 };
64} 36}
37
38class TextDocumentContentProvider
39 implements vscode.TextDocumentContentProvider {
40 private ctx: Ctx;
41 uri = vscode.Uri.parse('rust-analyzer-status://status');
42 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
43
44 constructor(ctx: Ctx) {
45 this.ctx = ctx;
46 }
47
48 provideTextDocumentContent(
49 _uri: vscode.Uri,
50 ): vscode.ProviderResult<string> {
51 const editor = vscode.window.activeTextEditor;
52 const client = this.ctx.client;
53 if (!editor || !client) return '';
54
55 return client.sendRequest<string>(
56 'rust-analyzer/analyzerStatus',
57 null,
58 );
59 }
60
61 get onDidChange(): vscode.Event<vscode.Uri> {
62 return this.eventEmitter.event;
63 }
64}
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
deleted file mode 100644
index ac62bdd48..000000000
--- a/editors/code/src/commands/cargo_watch.ts
+++ /dev/null
@@ -1,264 +0,0 @@
1import * as child_process from 'child_process';
2import * as path from 'path';
3import * as vscode from 'vscode';
4
5import { Server } from '../server';
6import { terminate } from '../utils/processes';
7import { LineBuffer } from './line_buffer';
8import { StatusDisplay } from './watch_status';
9
10import {
11 mapRustDiagnosticToVsCode,
12 RustDiagnostic,
13} from '../utils/diagnostics/rust';
14import SuggestedFixCollection from '../utils/diagnostics/SuggestedFixCollection';
15import { areDiagnosticsEqual } from '../utils/diagnostics/vscode';
16
17export async function registerCargoWatchProvider(
18 subscriptions: vscode.Disposable[],
19): Promise<CargoWatchProvider | undefined> {
20 let cargoExists = false;
21
22 // Check if the working directory is valid cargo root path
23 const cargoTomlPath = path.join(vscode.workspace.rootPath!, 'Cargo.toml');
24 const cargoTomlUri = vscode.Uri.file(cargoTomlPath);
25 const cargoTomlFileInfo = await vscode.workspace.fs.stat(cargoTomlUri);
26
27 if (cargoTomlFileInfo) {
28 cargoExists = true;
29 }
30
31 if (!cargoExists) {
32 vscode.window.showErrorMessage(
33 `Couldn\'t find \'Cargo.toml\' at ${cargoTomlPath}`,
34 );
35 return;
36 }
37
38 const provider = new CargoWatchProvider();
39 subscriptions.push(provider);
40 return provider;
41}
42
43export class CargoWatchProvider implements vscode.Disposable {
44 private readonly diagnosticCollection: vscode.DiagnosticCollection;
45 private readonly statusDisplay: StatusDisplay;
46 private readonly outputChannel: vscode.OutputChannel;
47
48 private suggestedFixCollection: SuggestedFixCollection;
49 private codeActionDispose: vscode.Disposable;
50
51 private cargoProcess?: child_process.ChildProcess;
52
53 constructor() {
54 this.diagnosticCollection = vscode.languages.createDiagnosticCollection(
55 'rustc',
56 );
57 this.statusDisplay = new StatusDisplay(
58 Server.config.cargoWatchOptions.command,
59 );
60 this.outputChannel = vscode.window.createOutputChannel(
61 'Cargo Watch Trace',
62 );
63
64 // Track `rustc`'s suggested fixes so we can convert them to code actions
65 this.suggestedFixCollection = new SuggestedFixCollection();
66 this.codeActionDispose = vscode.languages.registerCodeActionsProvider(
67 [{ scheme: 'file', language: 'rust' }],
68 this.suggestedFixCollection,
69 {
70 providedCodeActionKinds:
71 SuggestedFixCollection.PROVIDED_CODE_ACTION_KINDS,
72 },
73 );
74 }
75
76 public start() {
77 if (this.cargoProcess) {
78 vscode.window.showInformationMessage(
79 'Cargo Watch is already running',
80 );
81 return;
82 }
83
84 let args =
85 Server.config.cargoWatchOptions.command + ' --message-format json';
86 if (Server.config.cargoWatchOptions.allTargets) {
87 args += ' --all-targets';
88 }
89 if (Server.config.cargoWatchOptions.command.length > 0) {
90 // Excape the double quote string:
91 args += ' ' + Server.config.cargoWatchOptions.arguments;
92 }
93 // Windows handles arguments differently than the unix-likes, so we need to wrap the args in double quotes
94 if (process.platform === 'win32') {
95 args = '"' + args + '"';
96 }
97
98 const ignoreFlags = Server.config.cargoWatchOptions.ignore.reduce(
99 (flags, pattern) => [...flags, '--ignore', pattern],
100 [] as string[],
101 );
102
103 // Start the cargo watch with json message
104 this.cargoProcess = child_process.spawn(
105 'cargo',
106 ['watch', '-x', args, ...ignoreFlags],
107 {
108 stdio: ['ignore', 'pipe', 'pipe'],
109 cwd: vscode.workspace.rootPath,
110 windowsVerbatimArguments: true,
111 },
112 );
113
114 if (!this.cargoProcess) {
115 vscode.window.showErrorMessage('Cargo Watch failed to start');
116 return;
117 }
118
119 const stdoutData = new LineBuffer();
120 this.cargoProcess.stdout?.on('data', (s: string) => {
121 stdoutData.processOutput(s, line => {
122 this.logInfo(line);
123 try {
124 this.parseLine(line);
125 } catch (err) {
126 this.logError(`Failed to parse: ${err}, content : ${line}`);
127 }
128 });
129 });
130
131 const stderrData = new LineBuffer();
132 this.cargoProcess.stderr?.on('data', (s: string) => {
133 stderrData.processOutput(s, line => {
134 this.logError('Error on cargo-watch : {\n' + line + '}\n');
135 });
136 });
137
138 this.cargoProcess.on('error', (err: Error) => {
139 this.logError(
140 'Error on cargo-watch process : {\n' + err.message + '}\n',
141 );
142 });
143
144 this.logInfo('cargo-watch started.');
145 }
146
147 public stop() {
148 if (this.cargoProcess) {
149 this.cargoProcess.kill();
150 terminate(this.cargoProcess);
151 this.cargoProcess = undefined;
152 } else {
153 vscode.window.showInformationMessage('Cargo Watch is not running');
154 }
155 }
156
157 public dispose(): void {
158 this.stop();
159
160 this.diagnosticCollection.clear();
161 this.diagnosticCollection.dispose();
162 this.outputChannel.dispose();
163 this.statusDisplay.dispose();
164 this.codeActionDispose.dispose();
165 }
166
167 private logInfo(line: string) {
168 if (Server.config.cargoWatchOptions.trace === 'verbose') {
169 this.outputChannel.append(line);
170 }
171 }
172
173 private logError(line: string) {
174 if (
175 Server.config.cargoWatchOptions.trace === 'error' ||
176 Server.config.cargoWatchOptions.trace === 'verbose'
177 ) {
178 this.outputChannel.append(line);
179 }
180 }
181
182 private parseLine(line: string) {
183 if (line.startsWith('[Running')) {
184 this.diagnosticCollection.clear();
185 this.suggestedFixCollection.clear();
186 this.statusDisplay.show();
187 }
188
189 if (line.startsWith('[Finished running')) {
190 this.statusDisplay.hide();
191 }
192
193 interface CargoArtifact {
194 reason: string;
195 package_id: string;
196 }
197
198 // https://github.com/rust-lang/cargo/blob/master/src/cargo/util/machine_message.rs
199 interface CargoMessage {
200 reason: string;
201 package_id: string;
202 message: RustDiagnostic;
203 }
204
205 // cargo-watch itself output non json format
206 // Ignore these lines
207 let data: CargoMessage;
208 try {
209 data = JSON.parse(line.trim());
210 } catch (error) {
211 this.logError(`Fail to parse to json : { ${error} }`);
212 return;
213 }
214
215 if (data.reason === 'compiler-artifact') {
216 const msg = data as CargoArtifact;
217
218 // The format of the package_id is "{name} {version} ({source_id})",
219 // https://github.com/rust-lang/cargo/blob/37ad03f86e895bb80b474c1c088322634f4725f5/src/cargo/core/package_id.rs#L53
220 this.statusDisplay.packageName = msg.package_id.split(' ')[0];
221 } else if (data.reason === 'compiler-message') {
222 const msg = data.message as RustDiagnostic;
223
224 const mapResult = mapRustDiagnosticToVsCode(msg);
225 if (!mapResult) {
226 return;
227 }
228
229 const { location, diagnostic, suggestedFixes } = mapResult;
230 const fileUri = location.uri;
231
232 const diagnostics: vscode.Diagnostic[] = [
233 ...(this.diagnosticCollection!.get(fileUri) || []),
234 ];
235
236 // If we're building multiple targets it's possible we've already seen this diagnostic
237 const isDuplicate = diagnostics.some(d =>
238 areDiagnosticsEqual(d, diagnostic),
239 );
240 if (isDuplicate) {
241 return;
242 }
243
244 diagnostics.push(diagnostic);
245 this.diagnosticCollection!.set(fileUri, diagnostics);
246
247 if (suggestedFixes.length) {
248 for (const suggestedFix of suggestedFixes) {
249 this.suggestedFixCollection.addSuggestedFixForDiagnostic(
250 suggestedFix,
251 diagnostic,
252 );
253 }
254
255 // Have VsCode query us for the code actions
256 vscode.commands.executeCommand(
257 'vscode.executeCodeActionProvider',
258 fileUri,
259 diagnostic.range,
260 );
261 }
262 }
263 }
264}
diff --git a/editors/code/src/commands/expand_macro.ts b/editors/code/src/commands/expand_macro.ts
index 17c78280a..dcdde78af 100644
--- a/editors/code/src/commands/expand_macro.ts
+++ b/editors/code/src/commands/expand_macro.ts
@@ -1,60 +1,23 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3import { Server } from '../server';
4 3
5export const expandMacroUri = vscode.Uri.parse( 4import { Ctx, Cmd } from '../ctx';
6 'rust-analyzer://expandMacro/[EXPANSION].rs',
7);
8
9export class ExpandMacroContentProvider
10 implements vscode.TextDocumentContentProvider {
11 public eventEmitter = new vscode.EventEmitter<vscode.Uri>();
12
13 public provideTextDocumentContent(
14 _uri: vscode.Uri,
15 ): vscode.ProviderResult<string> {
16 async function handle() {
17 const editor = vscode.window.activeTextEditor;
18 if (editor == null) {
19 return '';
20 }
21
22 const position = editor.selection.active;
23 const request: MacroExpandParams = {
24 textDocument: { uri: editor.document.uri.toString() },
25 position,
26 };
27 const expanded = await Server.client.sendRequest<ExpandedMacro>(
28 'rust-analyzer/expandMacro',
29 request,
30 );
31
32 if (expanded == null) {
33 return 'Not available';
34 }
35
36 return code_format(expanded);
37 }
38
39 return handle();
40 }
41
42 get onDidChange(): vscode.Event<vscode.Uri> {
43 return this.eventEmitter.event;
44 }
45}
46 5
47// Opens the virtual file that will show the syntax tree 6// Opens the virtual file that will show the syntax tree
48// 7//
49// The contents of the file come from the `TextDocumentContentProvider` 8// The contents of the file come from the `TextDocumentContentProvider`
50export function createHandle(provider: ExpandMacroContentProvider) { 9export function expandMacro(ctx: Ctx): Cmd {
51 return async () => { 10 const tdcp = new TextDocumentContentProvider(ctx);
52 const uri = expandMacroUri; 11 ctx.pushCleanup(
53 12 vscode.workspace.registerTextDocumentContentProvider(
54 const document = await vscode.workspace.openTextDocument(uri); 13 'rust-analyzer',
55 14 tdcp,
56 provider.eventEmitter.fire(uri); 15 ),
16 );
57 17
18 return async () => {
19 const document = await vscode.workspace.openTextDocument(tdcp.uri);
20 tdcp.eventEmitter.fire(tdcp.uri);
58 return vscode.window.showTextDocument( 21 return vscode.window.showTextDocument(
59 document, 22 document,
60 vscode.ViewColumn.Two, 23 vscode.ViewColumn.Two,
@@ -63,11 +26,6 @@ export function createHandle(provider: ExpandMacroContentProvider) {
63 }; 26 };
64} 27}
65 28
66interface MacroExpandParams {
67 textDocument: TextDocumentIdentifier;
68 position: Position;
69}
70
71interface ExpandedMacro { 29interface ExpandedMacro {
72 name: string; 30 name: string;
73 expansion: string; 31 expansion: string;
@@ -81,3 +39,38 @@ function code_format(expanded: ExpandedMacro): string {
81 39
82 return result; 40 return result;
83} 41}
42
43class TextDocumentContentProvider
44 implements vscode.TextDocumentContentProvider {
45 private ctx: Ctx;
46 uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
47 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
48
49 constructor(ctx: Ctx) {
50 this.ctx = ctx;
51 }
52
53 async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
54 const editor = vscode.window.activeTextEditor;
55 const client = this.ctx.client;
56 if (!editor || !client) return '';
57
58 const position = editor.selection.active;
59 const request: lc.TextDocumentPositionParams = {
60 textDocument: { uri: editor.document.uri.toString() },
61 position,
62 };
63 const expanded = await client.sendRequest<ExpandedMacro>(
64 'rust-analyzer/expandMacro',
65 request,
66 );
67
68 if (expanded == null) return 'Not available';
69
70 return code_format(expanded);
71 }
72
73 get onDidChange(): vscode.Event<vscode.Uri> {
74 return this.eventEmitter.event;
75 }
76}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index 13a696758..9a1697dcb 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -1,23 +1,63 @@
1import * as analyzerStatus from './analyzer_status'; 1import * as vscode from 'vscode';
2import * as applySourceChange from './apply_source_change'; 2import * as lc from 'vscode-languageclient';
3import * as expandMacro from './expand_macro'; 3
4import * as inlayHints from './inlay_hints'; 4import { Ctx, Cmd } from '../ctx';
5import * as joinLines from './join_lines'; 5import * as sourceChange from '../source_change';
6import * as matchingBrace from './matching_brace'; 6
7import * as onEnter from './on_enter'; 7import { analyzerStatus } from './analyzer_status';
8import * as parentModule from './parent_module'; 8import { matchingBrace } from './matching_brace';
9import * as runnables from './runnables'; 9import { joinLines } from './join_lines';
10import * as syntaxTree from './syntaxTree'; 10import { onEnter } from './on_enter';
11import { parentModule } from './parent_module';
12import { syntaxTree } from './syntax_tree';
13import { expandMacro } from './expand_macro';
14import { run, runSingle } from './runnables';
15
16function collectGarbage(ctx: Ctx): Cmd {
17 return async () => {
18 ctx.client?.sendRequest<null>('rust-analyzer/collectGarbage', null);
19 };
20}
21
22function showReferences(ctx: Ctx): Cmd {
23 return (uri: string, position: lc.Position, locations: lc.Location[]) => {
24 let client = ctx.client;
25 if (client) {
26 vscode.commands.executeCommand(
27 'editor.action.showReferences',
28 vscode.Uri.parse(uri),
29 client.protocol2CodeConverter.asPosition(position),
30 locations.map(client.protocol2CodeConverter.asLocation),
31 );
32 }
33 };
34}
35
36function applySourceChange(ctx: Ctx): Cmd {
37 return async (change: sourceChange.SourceChange) => {
38 sourceChange.applySourceChange(ctx, change);
39 };
40}
41
42function reload(ctx: Ctx): Cmd {
43 return async () => {
44 vscode.window.showInformationMessage('Reloading rust-analyzer...');
45 await ctx.restartServer();
46 };
47}
11 48
12export { 49export {
13 analyzerStatus, 50 analyzerStatus,
14 applySourceChange,
15 expandMacro, 51 expandMacro,
16 joinLines, 52 joinLines,
17 matchingBrace, 53 matchingBrace,
18 parentModule, 54 parentModule,
19 runnables,
20 syntaxTree, 55 syntaxTree,
21 onEnter, 56 onEnter,
22 inlayHints, 57 collectGarbage,
58 run,
59 runSingle,
60 showReferences,
61 applySourceChange,
62 reload
23}; 63};
diff --git a/editors/code/src/commands/inlay_hints.ts b/editors/code/src/commands/inlay_hints.ts
deleted file mode 100644
index ac7dcce60..000000000
--- a/editors/code/src/commands/inlay_hints.ts
+++ /dev/null
@@ -1,115 +0,0 @@
1import * as vscode from 'vscode';
2import { Range, TextDocumentChangeEvent, TextEditor } from 'vscode';
3import { TextDocumentIdentifier } from 'vscode-languageclient';
4import { Server } from '../server';
5
6interface InlayHintsParams {
7 textDocument: TextDocumentIdentifier;
8}
9
10interface InlayHint {
11 range: Range;
12 kind: string;
13 label: string;
14}
15
16const typeHintDecorationType = vscode.window.createTextEditorDecorationType({
17 after: {
18 color: new vscode.ThemeColor('ralsp.inlayHint'),
19 },
20});
21
22export class HintsUpdater {
23 private displayHints = true;
24
25 public async toggleHintsDisplay(displayHints: boolean): Promise<void> {
26 if (this.displayHints !== displayHints) {
27 this.displayHints = displayHints;
28 return this.refreshVisibleEditorsHints(
29 displayHints ? undefined : [],
30 );
31 }
32 }
33
34 public async refreshHintsForVisibleEditors(
35 cause?: TextDocumentChangeEvent,
36 ): Promise<void> {
37 if (!this.displayHints) {
38 return;
39 }
40 if (
41 cause !== undefined &&
42 (cause.contentChanges.length === 0 ||
43 !this.isRustDocument(cause.document))
44 ) {
45 return;
46 }
47 return this.refreshVisibleEditorsHints();
48 }
49
50 private async refreshVisibleEditorsHints(
51 newDecorations?: vscode.DecorationOptions[],
52 ) {
53 const promises: Array<Promise<void>> = [];
54
55 for (const rustEditor of vscode.window.visibleTextEditors.filter(
56 editor => this.isRustDocument(editor.document),
57 )) {
58 if (newDecorations !== undefined) {
59 promises.push(
60 Promise.resolve(
61 rustEditor.setDecorations(
62 typeHintDecorationType,
63 newDecorations,
64 ),
65 ),
66 );
67 } else {
68 promises.push(this.updateDecorationsFromServer(rustEditor));
69 }
70 }
71
72 for (const promise of promises) {
73 await promise;
74 }
75 }
76
77 private isRustDocument(document: vscode.TextDocument): boolean {
78 return document && document.languageId === 'rust';
79 }
80
81 private async updateDecorationsFromServer(
82 editor: TextEditor,
83 ): Promise<void> {
84 const newHints = await this.queryHints(editor.document.uri.toString());
85 if (newHints !== null) {
86 const newDecorations = newHints.map(hint => ({
87 range: hint.range,
88 renderOptions: {
89 after: {
90 contentText: `: ${hint.label}`,
91 },
92 },
93 }));
94 return editor.setDecorations(
95 typeHintDecorationType,
96 newDecorations,
97 );
98 }
99 }
100
101 private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
102 const request: InlayHintsParams = {
103 textDocument: { uri: documentUri },
104 };
105 const client = Server.client;
106 return client
107 .onReady()
108 .then(() =>
109 client.sendRequest<InlayHint[] | null>(
110 'rust-analyzer/inlayHints',
111 request,
112 ),
113 );
114 }
115}
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
index 134ddc801..7b08c3255 100644
--- a/editors/code/src/commands/join_lines.ts
+++ b/editors/code/src/commands/join_lines.ts
@@ -1,29 +1,27 @@
1import * as vscode from 'vscode'; 1import * as lc from 'vscode-languageclient';
2 2
3import { Range, TextDocumentIdentifier } from 'vscode-languageclient'; 3import { Ctx, Cmd } from '../ctx';
4import { Server } from '../server'; 4import { applySourceChange, SourceChange } from '../source_change';
5import {
6 handle as applySourceChange,
7 SourceChange,
8} from './apply_source_change';
9 5
10interface JoinLinesParams { 6export function joinLines(ctx: Ctx): Cmd {
11 textDocument: TextDocumentIdentifier; 7 return async () => {
12 range: Range; 8 const editor = ctx.activeRustEditor;
13} 9 const client = ctx.client;
10 if (!editor || !client) return;
14 11
15export async function handle() { 12 const request: JoinLinesParams = {
16 const editor = vscode.window.activeTextEditor; 13 range: client.code2ProtocolConverter.asRange(editor.selection),
17 if (editor == null || editor.document.languageId !== 'rust') { 14 textDocument: { uri: editor.document.uri.toString() },
18 return; 15 };
19 } 16 const change = await client.sendRequest<SourceChange>(
20 const request: JoinLinesParams = { 17 'rust-analyzer/joinLines',
21 range: Server.client.code2ProtocolConverter.asRange(editor.selection), 18 request,
22 textDocument: { uri: editor.document.uri.toString() }, 19 );
20 await applySourceChange(ctx, change);
23 }; 21 };
24 const change = await Server.client.sendRequest<SourceChange>( 22}
25 'rust-analyzer/joinLines', 23
26 request, 24interface JoinLinesParams {
27 ); 25 textDocument: lc.TextDocumentIdentifier;
28 await applySourceChange(change); 26 range: lc.Range;
29} 27}
diff --git a/editors/code/src/commands/line_buffer.ts b/editors/code/src/commands/line_buffer.ts
deleted file mode 100644
index fb5b9f7f2..000000000
--- a/editors/code/src/commands/line_buffer.ts
+++ /dev/null
@@ -1,16 +0,0 @@
1export class LineBuffer {
2 private outBuffer: string = '';
3
4 public processOutput(chunk: string, cb: (line: string) => void) {
5 this.outBuffer += chunk;
6 let eolIndex = this.outBuffer.indexOf('\n');
7 while (eolIndex >= 0) {
8 // line includes the EOL
9 const line = this.outBuffer.slice(0, eolIndex + 1);
10 cb(line);
11 this.outBuffer = this.outBuffer.slice(eolIndex + 1);
12
13 eolIndex = this.outBuffer.indexOf('\n');
14 }
15 }
16}
diff --git a/editors/code/src/commands/matching_brace.ts b/editors/code/src/commands/matching_brace.ts
index 364208cc7..7c58bb7e7 100644
--- a/editors/code/src/commands/matching_brace.ts
+++ b/editors/code/src/commands/matching_brace.ts
@@ -1,34 +1,36 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
2 3
3import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; 4import { Ctx, Cmd } from '../ctx';
4import { Server } from '../server';
5 5
6interface FindMatchingBraceParams { 6export function matchingBrace(ctx: Ctx): Cmd {
7 textDocument: TextDocumentIdentifier; 7 return async () => {
8 offsets: Position[]; 8 const editor = ctx.activeRustEditor;
9} 9 const client = ctx.client;
10 if (!editor || !client) return;
10 11
11export async function handle() { 12 const request: FindMatchingBraceParams = {
12 const editor = vscode.window.activeTextEditor; 13 textDocument: { uri: editor.document.uri.toString() },
13 if (editor == null || editor.document.languageId !== 'rust') { 14 offsets: editor.selections.map(s =>
14 return; 15 client.code2ProtocolConverter.asPosition(s.active),
15 } 16 ),
16 const request: FindMatchingBraceParams = { 17 };
17 textDocument: { uri: editor.document.uri.toString() }, 18 const response = await client.sendRequest<lc.Position[]>(
18 offsets: editor.selections.map(s => { 19 'rust-analyzer/findMatchingBrace',
19 return Server.client.code2ProtocolConverter.asPosition(s.active); 20 request,
20 }),
21 };
22 const response = await Server.client.sendRequest<Position[]>(
23 'rust-analyzer/findMatchingBrace',
24 request,
25 );
26 editor.selections = editor.selections.map((sel, idx) => {
27 const active = Server.client.protocol2CodeConverter.asPosition(
28 response[idx],
29 ); 21 );
30 const anchor = sel.isEmpty ? active : sel.anchor; 22 editor.selections = editor.selections.map((sel, idx) => {
31 return new vscode.Selection(anchor, active); 23 const active = client.protocol2CodeConverter.asPosition(
32 }); 24 response[idx],
33 editor.revealRange(editor.selection); 25 );
26 const anchor = sel.isEmpty ? active : sel.anchor;
27 return new vscode.Selection(anchor, active);
28 });
29 editor.revealRange(editor.selection);
30 };
31}
32
33interface FindMatchingBraceParams {
34 textDocument: lc.TextDocumentIdentifier;
35 offsets: lc.Position[];
34} 36}
diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts
index 772c64b3c..6f61883cd 100644
--- a/editors/code/src/commands/on_enter.ts
+++ b/editors/code/src/commands/on_enter.ts
@@ -1,33 +1,28 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 1import * as lc from 'vscode-languageclient';
3import { Server } from '../server';
4import {
5 handle as applySourceChange,
6 SourceChange,
7} from './apply_source_change';
8 2
9export async function handle(event: { text: string }): Promise<boolean> { 3import { applySourceChange, SourceChange } from '../source_change';
10 const editor = vscode.window.activeTextEditor; 4import { Cmd, Ctx } from '../ctx';
11 if ( 5
12 editor == null || 6export function onEnter(ctx: Ctx): Cmd {
13 editor.document.languageId !== 'rust' || 7 return async (event: { text: string }) => {
14 event.text !== '\n' 8 const editor = ctx.activeRustEditor;
15 ) { 9 const client = ctx.client;
16 return false; 10 if (!editor || event.text !== '\n') return false;
17 } 11 if (!client) return false;
18 const request: lc.TextDocumentPositionParams = { 12
19 textDocument: { uri: editor.document.uri.toString() }, 13 const request: lc.TextDocumentPositionParams = {
20 position: Server.client.code2ProtocolConverter.asPosition( 14 textDocument: { uri: editor.document.uri.toString() },
21 editor.selection.active, 15 position: client.code2ProtocolConverter.asPosition(
22 ), 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;
24
25 await applySourceChange(ctx, change);
26 return true;
23 }; 27 };
24 const change = await Server.client.sendRequest<undefined | SourceChange>(
25 'rust-analyzer/onEnter',
26 request,
27 );
28 if (!change) {
29 return false;
30 }
31 await applySourceChange(change);
32 return true;
33} 28}
diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts
index ad49e1bdb..bf40b4021 100644
--- a/editors/code/src/commands/parent_module.ts
+++ b/editors/code/src/commands/parent_module.ts
@@ -1,32 +1,33 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2
3import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
4import { Server } from '../server';
5 3
6export async function handle() { 4import { Ctx, Cmd } from '../ctx';
7 const editor = vscode.window.activeTextEditor; 5
8 if (editor == null || editor.document.languageId !== 'rust') { 6export function parentModule(ctx: Ctx): Cmd {
9 return; 7 return async () => {
10 } 8 const editor = ctx.activeRustEditor;
11 const request: lc.TextDocumentPositionParams = { 9 const client = ctx.client;
12 textDocument: { uri: editor.document.uri.toString() }, 10 if (!editor || !client) return;
13 position: Server.client.code2ProtocolConverter.asPosition(
14 editor.selection.active,
15 ),
16 };
17 const response = await Server.client.sendRequest<lc.Location[]>(
18 'rust-analyzer/parentModule',
19 request,
20 );
21 const loc = response[0];
22 if (loc == null) {
23 return;
24 }
25 const uri = Server.client.protocol2CodeConverter.asUri(loc.uri);
26 const range = Server.client.protocol2CodeConverter.asRange(loc.range);
27 11
28 const doc = await vscode.workspace.openTextDocument(uri); 12 const request: lc.TextDocumentPositionParams = {
29 const e = await vscode.window.showTextDocument(doc); 13 textDocument: { uri: editor.document.uri.toString() },
30 e.selection = new vscode.Selection(range.start, range.start); 14 position: client.code2ProtocolConverter.asPosition(
31 e.revealRange(range, vscode.TextEditorRevealType.InCenter); 15 editor.selection.active,
16 ),
17 };
18 const response = await client.sendRequest<lc.Location[]>(
19 'rust-analyzer/parentModule',
20 request,
21 );
22 const loc = response[0];
23 if (loc == null) return;
24
25 const uri = client.protocol2CodeConverter.asUri(loc.uri);
26 const range = client.protocol2CodeConverter.asRange(loc.range);
27
28 const doc = await vscode.workspace.openTextDocument(uri);
29 const e = await vscode.window.showTextDocument(doc);
30 e.selection = new vscode.Selection(range.start, range.start);
31 e.revealRange(range, vscode.TextEditorRevealType.InCenter);
32 };
32} 33}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
index cf980e257..7919997ce 100644
--- a/editors/code/src/commands/runnables.ts
+++ b/editors/code/src/commands/runnables.ts
@@ -1,11 +1,68 @@
1import * as child_process from 'child_process';
2
3import * as util from 'util';
4import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
5import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
6 3
7import { Server } from '../server'; 4import { Ctx, Cmd } from '../ctx';
8import { CargoWatchProvider, registerCargoWatchProvider } from './cargo_watch'; 5
6export function run(ctx: Ctx): Cmd {
7 let prevRunnable: RunnableQuickPick | undefined;
8
9 return async () => {
10 const editor = ctx.activeRustEditor;
11 const client = ctx.client;
12 if (!editor || !client) return;
13
14 const textDocument: lc.TextDocumentIdentifier = {
15 uri: editor.document.uri.toString(),
16 };
17 const params: RunnablesParams = {
18 textDocument,
19 position: client.code2ProtocolConverter.asPosition(
20 editor.selection.active,
21 ),
22 };
23 const runnables = await client.sendRequest<Runnable[]>(
24 'rust-analyzer/runnables',
25 params,
26 );
27 const items: RunnableQuickPick[] = [];
28 if (prevRunnable) {
29 items.push(prevRunnable);
30 }
31 for (const r of runnables) {
32 if (
33 prevRunnable &&
34 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
35 ) {
36 continue;
37 }
38 items.push(new RunnableQuickPick(r));
39 }
40 const item = await vscode.window.showQuickPick(items);
41 if (!item) return;
42
43 item.detail = 'rerun';
44 prevRunnable = item;
45 const task = createTask(item.runnable);
46 return await vscode.tasks.executeTask(task);
47 };
48}
49
50export function runSingle(ctx: Ctx): Cmd {
51 return async (runnable: Runnable) => {
52 const editor = ctx.activeRustEditor;
53 if (!editor) return;
54
55 const task = createTask(runnable);
56 task.group = vscode.TaskGroup.Build;
57 task.presentationOptions = {
58 reveal: vscode.TaskRevealKind.Always,
59 panel: vscode.TaskPanelKind.Dedicated,
60 clear: true,
61 };
62
63 return vscode.tasks.executeTask(task);
64 };
65}
9 66
10interface RunnablesParams { 67interface RunnablesParams {
11 textDocument: lc.TextDocumentIdentifier; 68 textDocument: lc.TextDocumentIdentifier;
@@ -71,150 +128,3 @@ function createTask(spec: Runnable): vscode.Task {
71 t.presentationOptions.clear = true; 128 t.presentationOptions.clear = true;
72 return t; 129 return t;
73} 130}
74
75let prevRunnable: RunnableQuickPick | undefined;
76export async function handle(): Promise<vscode.TaskExecution | undefined> {
77 const editor = vscode.window.activeTextEditor;
78 if (editor == null || editor.document.languageId !== 'rust') {
79 return;
80 }
81 const textDocument: lc.TextDocumentIdentifier = {
82 uri: editor.document.uri.toString(),
83 };
84 const params: RunnablesParams = {
85 textDocument,
86 position: Server.client.code2ProtocolConverter.asPosition(
87 editor.selection.active,
88 ),
89 };
90 const runnables = await Server.client.sendRequest<Runnable[]>(
91 'rust-analyzer/runnables',
92 params,
93 );
94 const items: RunnableQuickPick[] = [];
95 if (prevRunnable) {
96 items.push(prevRunnable);
97 }
98 for (const r of runnables) {
99 if (
100 prevRunnable &&
101 JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
102 ) {
103 continue;
104 }
105 items.push(new RunnableQuickPick(r));
106 }
107 const item = await vscode.window.showQuickPick(items);
108 if (!item) {
109 return;
110 }
111
112 item.detail = 'rerun';
113 prevRunnable = item;
114 const task = createTask(item.runnable);
115 return await vscode.tasks.executeTask(task);
116}
117
118export async function handleSingle(runnable: Runnable) {
119 const editor = vscode.window.activeTextEditor;
120 if (editor == null || editor.document.languageId !== 'rust') {
121 return;
122 }
123
124 const task = createTask(runnable);
125 task.group = vscode.TaskGroup.Build;
126 task.presentationOptions = {
127 reveal: vscode.TaskRevealKind.Always,
128 panel: vscode.TaskPanelKind.Dedicated,
129 clear: true,
130 };
131
132 return vscode.tasks.executeTask(task);
133}
134
135/**
136 * Interactively asks the user whether we should run `cargo check` in order to
137 * provide inline diagnostics; the user is met with a series of dialog boxes
138 * that, when accepted, allow us to `cargo install cargo-watch` and then run it.
139 */
140export async function interactivelyStartCargoWatch(
141 context: vscode.ExtensionContext,
142): Promise<CargoWatchProvider | undefined> {
143 if (Server.config.cargoWatchOptions.enableOnStartup === 'disabled') {
144 return;
145 }
146
147 if (Server.config.cargoWatchOptions.enableOnStartup === 'ask') {
148 const watch = await vscode.window.showInformationMessage(
149 'Start watching changes with cargo? (Executes `cargo watch`, provides inline diagnostics)',
150 'yes',
151 'no',
152 );
153 if (watch !== 'yes') {
154 return;
155 }
156 }
157
158 return startCargoWatch(context);
159}
160
161export async function startCargoWatch(
162 context: vscode.ExtensionContext,
163): Promise<CargoWatchProvider | undefined> {
164 const execPromise = util.promisify(child_process.exec);
165
166 const { stderr, code = 0 } = await execPromise(
167 'cargo watch --version',
168 ).catch(e => e);
169
170 if (stderr.includes('no such subcommand: `watch`')) {
171 const msg =
172 'The `cargo-watch` subcommand is not installed. Install? (takes ~1-2 minutes)';
173 const install = await vscode.window.showInformationMessage(
174 msg,
175 'yes',
176 'no',
177 );
178 if (install !== 'yes') {
179 return;
180 }
181
182 const label = 'install-cargo-watch';
183 const taskFinished = new Promise((resolve, _reject) => {
184 const disposable = vscode.tasks.onDidEndTask(({ execution }) => {
185 if (execution.task.name === label) {
186 disposable.dispose();
187 resolve();
188 }
189 });
190 });
191
192 vscode.tasks.executeTask(
193 createTask({
194 label,
195 bin: 'cargo',
196 args: ['install', 'cargo-watch'],
197 env: {},
198 }),
199 );
200 await taskFinished;
201 const output = await execPromise('cargo watch --version').catch(e => e);
202 if (output.stderr !== '') {
203 vscode.window.showErrorMessage(
204 `Couldn't install \`cargo-\`watch: ${output.stderr}`,
205 );
206 return;
207 }
208 } else if (code !== 0) {
209 vscode.window.showErrorMessage(
210 `\`cargo watch\` failed with ${code}: ${stderr}`,
211 );
212 return;
213 }
214
215 const provider = await registerCargoWatchProvider(context.subscriptions);
216 if (provider) {
217 provider.start();
218 }
219 return provider;
220}
diff --git a/editors/code/src/commands/syntaxTree.ts b/editors/code/src/commands/syntaxTree.ts
deleted file mode 100644
index 89a80550c..000000000
--- a/editors/code/src/commands/syntaxTree.ts
+++ /dev/null
@@ -1,76 +0,0 @@
1import * as vscode from 'vscode';
2import { Range, TextDocumentIdentifier } from 'vscode-languageclient';
3
4import { Server } from '../server';
5
6export const syntaxTreeUri = vscode.Uri.parse('rust-analyzer://syntaxtree');
7
8export class SyntaxTreeContentProvider
9 implements vscode.TextDocumentContentProvider {
10 public eventEmitter = new vscode.EventEmitter<vscode.Uri>();
11 public syntaxTree: string = 'Not available';
12
13 public provideTextDocumentContent(
14 uri: vscode.Uri,
15 ): vscode.ProviderResult<string> {
16 const editor = vscode.window.activeTextEditor;
17 if (editor == null) {
18 return '';
19 }
20
21 let range: Range | undefined;
22
23 // When the range based query is enabled we take the range of the selection
24 if (uri.query === 'range=true') {
25 range = editor.selection.isEmpty
26 ? undefined
27 : Server.client.code2ProtocolConverter.asRange(
28 editor.selection,
29 );
30 }
31
32 const request: SyntaxTreeParams = {
33 textDocument: { uri: editor.document.uri.toString() },
34 range,
35 };
36 return Server.client.sendRequest<SyntaxTreeResult>(
37 'rust-analyzer/syntaxTree',
38 request,
39 );
40 }
41
42 get onDidChange(): vscode.Event<vscode.Uri> {
43 return this.eventEmitter.event;
44 }
45}
46
47interface SyntaxTreeParams {
48 textDocument: TextDocumentIdentifier;
49 range?: Range;
50}
51
52type SyntaxTreeResult = string;
53
54// Opens the virtual file that will show the syntax tree
55//
56// The contents of the file come from the `TextDocumentContentProvider`
57export function createHandle(provider: SyntaxTreeContentProvider) {
58 return async () => {
59 const editor = vscode.window.activeTextEditor;
60 const rangeEnabled = !!(editor && !editor.selection.isEmpty);
61
62 const uri = rangeEnabled
63 ? vscode.Uri.parse(`${syntaxTreeUri.toString()}?range=true`)
64 : syntaxTreeUri;
65
66 const document = await vscode.workspace.openTextDocument(uri);
67
68 provider.eventEmitter.fire(uri);
69
70 return vscode.window.showTextDocument(
71 document,
72 vscode.ViewColumn.Two,
73 true,
74 );
75 };
76}
diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts
new file mode 100644
index 000000000..02ea9f166
--- /dev/null
+++ b/editors/code/src/commands/syntax_tree.ts
@@ -0,0 +1,104 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3
4import { Ctx, Cmd } from '../ctx';
5
6// Opens the virtual file that will show the syntax tree
7//
8// The contents of the file come from the `TextDocumentContentProvider`
9export function syntaxTree(ctx: Ctx): Cmd {
10 const tdcp = new TextDocumentContentProvider(ctx);
11
12 ctx.pushCleanup(
13 vscode.workspace.registerTextDocumentContentProvider(
14 'rust-analyzer',
15 tdcp,
16 ),
17 );
18
19 vscode.workspace.onDidChangeTextDocument(
20 (event: vscode.TextDocumentChangeEvent) => {
21 const doc = event.document;
22 if (doc.languageId !== 'rust') return;
23 afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
24 },
25 ctx.subscriptions,
26 );
27
28 vscode.window.onDidChangeActiveTextEditor(
29 (editor: vscode.TextEditor | undefined) => {
30 if (!editor || editor.document.languageId !== 'rust') return;
31 tdcp.eventEmitter.fire(tdcp.uri);
32 },
33 ctx.subscriptions,
34 );
35
36 return async () => {
37 const editor = vscode.window.activeTextEditor;
38 const rangeEnabled = !!(editor && !editor.selection.isEmpty);
39
40 const uri = rangeEnabled
41 ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
42 : tdcp.uri;
43
44 const document = await vscode.workspace.openTextDocument(uri);
45
46 tdcp.eventEmitter.fire(uri);
47
48 return vscode.window.showTextDocument(
49 document,
50 vscode.ViewColumn.Two,
51 true,
52 );
53 };
54}
55
56// We need to order this after LS updates, but there's no API for that.
57// Hence, good old setTimeout.
58function afterLs(f: () => any) {
59 setTimeout(f, 10);
60}
61
62interface SyntaxTreeParams {
63 textDocument: lc.TextDocumentIdentifier;
64 range?: lc.Range;
65}
66
67class TextDocumentContentProvider
68 implements vscode.TextDocumentContentProvider {
69 private ctx: Ctx;
70 uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
71 eventEmitter = new vscode.EventEmitter<vscode.Uri>();
72
73 constructor(ctx: Ctx) {
74 this.ctx = ctx;
75 }
76
77 provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
78 const editor = vscode.window.activeTextEditor;
79 const client = this.ctx.client;
80 if (!editor || !client) return '';
81
82 let range: lc.Range | undefined;
83
84 // When the range based query is enabled we take the range of the selection
85 if (uri.query === 'range=true') {
86 range = editor.selection.isEmpty
87 ? undefined
88 : client.code2ProtocolConverter.asRange(editor.selection);
89 }
90
91 const request: SyntaxTreeParams = {
92 textDocument: { uri: editor.document.uri.toString() },
93 range,
94 };
95 return client.sendRequest<string>(
96 'rust-analyzer/syntaxTree',
97 request,
98 );
99 }
100
101 get onDidChange(): vscode.Event<vscode.Uri> {
102 return this.eventEmitter.event;
103 }
104}
diff --git a/editors/code/src/commands/watch_status.ts b/editors/code/src/commands/watch_status.ts
deleted file mode 100644
index 8d64394c7..000000000
--- a/editors/code/src/commands/watch_status.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import * as vscode from 'vscode';
2
3const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
5export class StatusDisplay implements vscode.Disposable {
6 public packageName?: string;
7
8 private i = 0;
9 private statusBarItem: vscode.StatusBarItem;
10 private command: string;
11 private timer?: NodeJS.Timeout;
12
13 constructor(command: string) {
14 this.statusBarItem = vscode.window.createStatusBarItem(
15 vscode.StatusBarAlignment.Left,
16 10,
17 );
18 this.command = command;
19 this.statusBarItem.hide();
20 }
21
22 public show() {
23 this.packageName = undefined;
24
25 this.timer =
26 this.timer ||
27 setInterval(() => {
28 if (this.packageName) {
29 this.statusBarItem!.text = `cargo ${this.command} [${
30 this.packageName
31 }] ${this.frame()}`;
32 } else {
33 this.statusBarItem!.text = `cargo ${
34 this.command
35 } ${this.frame()}`;
36 }
37 }, 300);
38
39 this.statusBarItem.show();
40 }
41
42 public hide() {
43 if (this.timer) {
44 clearInterval(this.timer);
45 this.timer = undefined;
46 }
47
48 this.statusBarItem.hide();
49 }
50
51 public dispose() {
52 if (this.timer) {
53 clearInterval(this.timer);
54 this.timer = undefined;
55 }
56
57 this.statusBarItem.dispose();
58 }
59
60 private frame() {
61 return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)];
62 }
63}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index e131f09df..ec2790b63 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,18 +1,11 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2 2
3import { Server } from './server';
4
5const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; 3const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
6 4
7export type CargoWatchStartupOptions = 'ask' | 'enabled' | 'disabled';
8export type CargoWatchTraceOptions = 'off' | 'error' | 'verbose';
9
10export interface CargoWatchOptions { 5export interface CargoWatchOptions {
11 enableOnStartup: CargoWatchStartupOptions; 6 enable: boolean;
12 arguments: string; 7 arguments: string[];
13 command: string; 8 command: string;
14 trace: CargoWatchTraceOptions;
15 ignore: string[];
16 allTargets: boolean; 9 allTargets: boolean;
17} 10}
18 11
@@ -23,27 +16,25 @@ export interface CargoFeatures {
23} 16}
24 17
25export class Config { 18export class Config {
26 public highlightingOn = true; 19 highlightingOn = true;
27 public rainbowHighlightingOn = false; 20 rainbowHighlightingOn = false;
28 public enableEnhancedTyping = true; 21 enableEnhancedTyping = true;
29 public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; 22 raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
30 public lruCapacity: null | number = null; 23 lruCapacity: null | number = null;
31 public displayInlayHints = true; 24 displayInlayHints = true;
32 public maxInlayHintLength: null | number = null; 25 maxInlayHintLength: null | number = null;
33 public excludeGlobs = []; 26 excludeGlobs = [];
34 public useClientWatching = true; 27 useClientWatching = true;
35 public featureFlags = {}; 28 featureFlags = {};
36 // for internal use 29 // for internal use
37 public withSysroot: null | boolean = null; 30 withSysroot: null | boolean = null;
38 public cargoWatchOptions: CargoWatchOptions = { 31 cargoWatchOptions: CargoWatchOptions = {
39 enableOnStartup: 'ask', 32 enable: true,
40 trace: 'off', 33 arguments: [],
41 arguments: '',
42 command: '', 34 command: '',
43 ignore: [],
44 allTargets: true, 35 allTargets: true,
45 }; 36 };
46 public cargoFeatures: CargoFeatures = { 37 cargoFeatures: CargoFeatures = {
47 noDefaultFeatures: false, 38 noDefaultFeatures: false,
48 allFeatures: true, 39 allFeatures: true,
49 features: [], 40 features: [],
@@ -52,15 +43,14 @@ export class Config {
52 private prevEnhancedTyping: null | boolean = null; 43 private prevEnhancedTyping: null | boolean = null;
53 private prevCargoFeatures: null | CargoFeatures = null; 44 private prevCargoFeatures: null | CargoFeatures = null;
54 45
55 constructor() { 46 constructor(ctx: vscode.ExtensionContext) {
56 vscode.workspace.onDidChangeConfiguration(_ => 47 vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), ctx.subscriptions);
57 this.userConfigChanged(), 48 this.refresh();
58 );
59 this.userConfigChanged();
60 } 49 }
61 50
62 public userConfigChanged() { 51 private refresh() {
63 const config = vscode.workspace.getConfiguration('rust-analyzer'); 52 const config = vscode.workspace.getConfiguration('rust-analyzer');
53
64 let requireReloadMessage = null; 54 let requireReloadMessage = null;
65 55
66 if (config.has('highlightingOn')) { 56 if (config.has('highlightingOn')) {
@@ -73,10 +63,6 @@ export class Config {
73 ) as boolean; 63 ) as boolean;
74 } 64 }
75 65
76 if (!this.highlightingOn && Server) {
77 Server.highlighter.removeHighlights();
78 }
79
80 if (config.has('enableEnhancedTyping')) { 66 if (config.has('enableEnhancedTyping')) {
81 this.enableEnhancedTyping = config.get( 67 this.enableEnhancedTyping = config.get(
82 'enableEnhancedTyping', 68 'enableEnhancedTyping',
@@ -100,23 +86,17 @@ export class Config {
100 RA_LSP_DEBUG || (config.get('raLspServerPath') as string); 86 RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
101 } 87 }
102 88
103 if (config.has('enableCargoWatchOnStartup')) { 89 if (config.has('cargo-watch.enable')) {
104 this.cargoWatchOptions.enableOnStartup = config.get< 90 this.cargoWatchOptions.enable = config.get<boolean>(
105 CargoWatchStartupOptions 91 'cargo-watch.enable',
106 >('enableCargoWatchOnStartup', 'ask'); 92 true,
107 }
108
109 if (config.has('trace.cargo-watch')) {
110 this.cargoWatchOptions.trace = config.get<CargoWatchTraceOptions>(
111 'trace.cargo-watch',
112 'off',
113 ); 93 );
114 } 94 }
115 95
116 if (config.has('cargo-watch.arguments')) { 96 if (config.has('cargo-watch.arguments')) {
117 this.cargoWatchOptions.arguments = config.get<string>( 97 this.cargoWatchOptions.arguments = config.get<string[]>(
118 'cargo-watch.arguments', 98 'cargo-watch.arguments',
119 '', 99 [],
120 ); 100 );
121 } 101 }
122 102
@@ -127,13 +107,6 @@ export class Config {
127 ); 107 );
128 } 108 }
129 109
130 if (config.has('cargo-watch.ignore')) {
131 this.cargoWatchOptions.ignore = config.get<string[]>(
132 'cargo-watch.ignore',
133 [],
134 );
135 }
136
137 if (config.has('cargo-watch.allTargets')) { 110 if (config.has('cargo-watch.allTargets')) {
138 this.cargoWatchOptions.allTargets = config.get<boolean>( 111 this.cargoWatchOptions.allTargets = config.get<boolean>(
139 'cargo-watch.allTargets', 112 'cargo-watch.allTargets',
@@ -190,9 +163,9 @@ export class Config {
190 (this.cargoFeatures.allFeatures !== 163 (this.cargoFeatures.allFeatures !==
191 this.prevCargoFeatures.allFeatures || 164 this.prevCargoFeatures.allFeatures ||
192 this.cargoFeatures.noDefaultFeatures !== 165 this.cargoFeatures.noDefaultFeatures !==
193 this.prevCargoFeatures.noDefaultFeatures || 166 this.prevCargoFeatures.noDefaultFeatures ||
194 this.cargoFeatures.features.length !== 167 this.cargoFeatures.features.length !==
195 this.prevCargoFeatures.features.length || 168 this.prevCargoFeatures.features.length ||
196 this.cargoFeatures.features.some( 169 this.cargoFeatures.features.some(
197 (v, i) => v !== this.prevCargoFeatures!.features[i], 170 (v, i) => v !== this.prevCargoFeatures!.features[i],
198 )) 171 ))
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
new file mode 100644
index 000000000..a2a4e42a9
--- /dev/null
+++ b/editors/code/src/ctx.ts
@@ -0,0 +1,112 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3import { Config } from './config';
4import { createClient } from './client';
5
6export class Ctx {
7 readonly config: Config;
8 // Because we have "reload server" action, various listeners **will** face a
9 // situation where the client is not ready yet, and should be prepared to
10 // deal with it.
11 //
12 // Ideally, this should be replaced with async getter though.
13 client: lc.LanguageClient | null = null;
14 private extCtx: vscode.ExtensionContext;
15 private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
16
17 constructor(extCtx: vscode.ExtensionContext) {
18 this.config = new Config(extCtx);
19 this.extCtx = extCtx;
20 }
21
22 async restartServer() {
23 let old = this.client;
24 if (old) {
25 await old.stop();
26 }
27 this.client = null;
28 const client = createClient(this.config);
29 this.pushCleanup(client.start());
30 await client.onReady();
31
32 this.client = client;
33 for (const hook of this.onDidRestartHooks) {
34 hook(client);
35 }
36 }
37
38 get activeRustEditor(): vscode.TextEditor | undefined {
39 const editor = vscode.window.activeTextEditor;
40 return editor && editor.document.languageId === 'rust'
41 ? editor
42 : undefined;
43 }
44
45 registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
46 const fullName = `rust-analyzer.${name}`;
47 const cmd = factory(this);
48 const d = vscode.commands.registerCommand(fullName, cmd);
49 this.pushCleanup(d);
50 }
51
52 overrideCommand(name: string, factory: (ctx: Ctx) => Cmd) {
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;
76 }
77
78 pushCleanup(d: { dispose(): any }) {
79 this.extCtx.subscriptions.push(d);
80 }
81
82 onDidRestart(hook: (client: lc.LanguageClient) => void) {
83 this.onDidRestartHooks.push(hook);
84 }
85}
86
87export type Cmd = (...args: any[]) => any;
88
89export async function sendRequestWithRetry<R>(
90 client: lc.LanguageClient,
91 method: string,
92 param: any,
93 token?: vscode.CancellationToken,
94): Promise<R> {
95 for (const delay of [2, 4, 6, 8, 10, null]) {
96 try {
97 return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param));
98 } catch (e) {
99 if (
100 e.code === lc.ErrorCodes.ContentModified &&
101 delay !== null
102 ) {
103 await sleep(10 * (1 << delay));
104 continue;
105 }
106 throw e;
107 }
108 }
109 throw 'unreachable';
110}
111
112const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts
deleted file mode 100644
index 74b91bd48..000000000
--- a/editors/code/src/events/change_active_text_editor.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import { TextEditor } from 'vscode';
2import { TextDocumentIdentifier } from 'vscode-languageclient';
3
4import {
5 SyntaxTreeContentProvider,
6 syntaxTreeUri,
7} from '../commands/syntaxTree';
8import { Decoration } from '../highlighting';
9import { Server } from '../server';
10
11export function makeHandler(syntaxTreeProvider: SyntaxTreeContentProvider) {
12 return async function handle(editor: TextEditor | undefined) {
13 if (!editor || editor.document.languageId !== 'rust') {
14 return;
15 }
16
17 syntaxTreeProvider.eventEmitter.fire(syntaxTreeUri);
18
19 if (!Server.config.highlightingOn) {
20 return;
21 }
22
23 const params: TextDocumentIdentifier = {
24 uri: editor.document.uri.toString(),
25 };
26 const decorations = await Server.client.sendRequest<Decoration[]>(
27 'rust-analyzer/decorationsRequest',
28 params,
29 );
30 Server.highlighter.setHighlights(editor, decorations);
31 };
32}
diff --git a/editors/code/src/events/change_text_document.ts b/editors/code/src/events/change_text_document.ts
deleted file mode 100644
index 2e998e889..000000000
--- a/editors/code/src/events/change_text_document.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as vscode from 'vscode';
2
3import {
4 SyntaxTreeContentProvider,
5 syntaxTreeUri,
6} from '../commands/syntaxTree';
7
8export function createHandler(syntaxTreeProvider: SyntaxTreeContentProvider) {
9 return (event: vscode.TextDocumentChangeEvent) => {
10 const doc = event.document;
11 if (doc.languageId !== 'rust') {
12 return;
13 }
14 afterLs(() => {
15 syntaxTreeProvider.eventEmitter.fire(syntaxTreeUri);
16 });
17 };
18}
19
20// We need to order this after LS updates, but there's no API for that.
21// Hence, good old setTimeout.
22function afterLs(f: () => any) {
23 setTimeout(f, 10);
24}
diff --git a/editors/code/src/events/index.ts b/editors/code/src/events/index.ts
deleted file mode 100644
index 4c154563f..000000000
--- a/editors/code/src/events/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1import * as changeActiveTextEditor from './change_active_text_editor';
2import * as changeTextDocument from './change_text_document';
3
4export { changeActiveTextEditor, changeTextDocument };
diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts
deleted file mode 100644
index 815f3692c..000000000
--- a/editors/code/src/extension.ts
+++ /dev/null
@@ -1,218 +0,0 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3
4import * as commands from './commands';
5import { CargoWatchProvider } from './commands/cargo_watch';
6import { ExpandMacroContentProvider } from './commands/expand_macro';
7import { HintsUpdater } from './commands/inlay_hints';
8import {
9 interactivelyStartCargoWatch,
10 startCargoWatch,
11} from './commands/runnables';
12import { SyntaxTreeContentProvider } from './commands/syntaxTree';
13import * as events from './events';
14import * as notifications from './notifications';
15import { Server } from './server';
16
17export async function activate(context: vscode.ExtensionContext) {
18 function disposeOnDeactivation(disposable: vscode.Disposable) {
19 context.subscriptions.push(disposable);
20 }
21
22 function registerCommand(name: string, f: any) {
23 disposeOnDeactivation(vscode.commands.registerCommand(name, f));
24 }
25 function overrideCommand(
26 name: string,
27 f: (...args: any[]) => Promise<boolean>,
28 ) {
29 const defaultCmd = `default:${name}`;
30 const original = (...args: any[]) =>
31 vscode.commands.executeCommand(defaultCmd, ...args);
32
33 try {
34 registerCommand(name, async (...args: any[]) => {
35 const editor = vscode.window.activeTextEditor;
36 if (
37 !editor ||
38 !editor.document ||
39 editor.document.languageId !== 'rust'
40 ) {
41 return await original(...args);
42 }
43 if (!(await f(...args))) {
44 return await original(...args);
45 }
46 });
47 } catch (_) {
48 vscode.window.showWarningMessage(
49 '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',
50 );
51 }
52 }
53
54 // Commands are requests from vscode to the language server
55 registerCommand(
56 'rust-analyzer.analyzerStatus',
57 commands.analyzerStatus.makeCommand(context),
58 );
59 registerCommand('rust-analyzer.collectGarbage', () =>
60 Server.client.sendRequest<null>('rust-analyzer/collectGarbage', null),
61 );
62 registerCommand(
63 'rust-analyzer.matchingBrace',
64 commands.matchingBrace.handle,
65 );
66 registerCommand('rust-analyzer.joinLines', commands.joinLines.handle);
67 registerCommand('rust-analyzer.parentModule', commands.parentModule.handle);
68 registerCommand('rust-analyzer.run', commands.runnables.handle);
69 // Unlike the above this does not send requests to the language server
70 registerCommand('rust-analyzer.runSingle', commands.runnables.handleSingle);
71 registerCommand(
72 'rust-analyzer.applySourceChange',
73 commands.applySourceChange.handle,
74 );
75 registerCommand(
76 'rust-analyzer.showReferences',
77 (uri: string, position: lc.Position, locations: lc.Location[]) => {
78 vscode.commands.executeCommand(
79 'editor.action.showReferences',
80 vscode.Uri.parse(uri),
81 Server.client.protocol2CodeConverter.asPosition(position),
82 locations.map(Server.client.protocol2CodeConverter.asLocation),
83 );
84 },
85 );
86
87 if (Server.config.enableEnhancedTyping) {
88 overrideCommand('type', commands.onEnter.handle);
89 }
90
91 // Notifications are events triggered by the language server
92 const allNotifications: Iterable<[
93 string,
94 lc.GenericNotificationHandler,
95 ]> = [
96 [
97 'rust-analyzer/publishDecorations',
98 notifications.publishDecorations.handle,
99 ],
100 ];
101 const syntaxTreeContentProvider = new SyntaxTreeContentProvider();
102 const expandMacroContentProvider = new ExpandMacroContentProvider();
103
104 // The events below are plain old javascript events, triggered and handled by vscode
105 vscode.window.onDidChangeActiveTextEditor(
106 events.changeActiveTextEditor.makeHandler(syntaxTreeContentProvider),
107 );
108
109 disposeOnDeactivation(
110 vscode.workspace.registerTextDocumentContentProvider(
111 'rust-analyzer',
112 syntaxTreeContentProvider,
113 ),
114 );
115 disposeOnDeactivation(
116 vscode.workspace.registerTextDocumentContentProvider(
117 'rust-analyzer',
118 expandMacroContentProvider,
119 ),
120 );
121
122 registerCommand(
123 'rust-analyzer.syntaxTree',
124 commands.syntaxTree.createHandle(syntaxTreeContentProvider),
125 );
126 registerCommand(
127 'rust-analyzer.expandMacro',
128 commands.expandMacro.createHandle(expandMacroContentProvider),
129 );
130
131 vscode.workspace.onDidChangeTextDocument(
132 events.changeTextDocument.createHandler(syntaxTreeContentProvider),
133 null,
134 context.subscriptions,
135 );
136
137 const startServer = () => Server.start(allNotifications);
138 const reloadCommand = () => reloadServer(startServer);
139
140 vscode.commands.registerCommand('rust-analyzer.reload', reloadCommand);
141
142 // Executing `cargo watch` provides us with inline diagnostics on save
143 let provider: CargoWatchProvider | undefined;
144 interactivelyStartCargoWatch(context).then(p => {
145 provider = p;
146 });
147 registerCommand('rust-analyzer.startCargoWatch', () => {
148 if (provider) {
149 provider.start();
150 } else {
151 startCargoWatch(context).then(p => {
152 provider = p;
153 });
154 }
155 });
156 registerCommand('rust-analyzer.stopCargoWatch', () => {
157 if (provider) {
158 provider.stop();
159 }
160 });
161
162 // Start the language server, finally!
163 try {
164 await startServer();
165 } catch (e) {
166 vscode.window.showErrorMessage(e.message);
167 }
168
169 if (Server.config.displayInlayHints) {
170 const hintsUpdater = new HintsUpdater();
171 hintsUpdater.refreshHintsForVisibleEditors().then(() => {
172 // vscode may ignore top level hintsUpdater.refreshHintsForVisibleEditors()
173 // so update the hints once when the focus changes to guarantee their presence
174 let editorChangeDisposable: vscode.Disposable | null = null;
175 editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(
176 _ => {
177 if (editorChangeDisposable !== null) {
178 editorChangeDisposable.dispose();
179 }
180 return hintsUpdater.refreshHintsForVisibleEditors();
181 },
182 );
183
184 disposeOnDeactivation(
185 vscode.window.onDidChangeVisibleTextEditors(_ =>
186 hintsUpdater.refreshHintsForVisibleEditors(),
187 ),
188 );
189 disposeOnDeactivation(
190 vscode.workspace.onDidChangeTextDocument(e =>
191 hintsUpdater.refreshHintsForVisibleEditors(e),
192 ),
193 );
194 disposeOnDeactivation(
195 vscode.workspace.onDidChangeConfiguration(_ =>
196 hintsUpdater.toggleHintsDisplay(
197 Server.config.displayInlayHints,
198 ),
199 ),
200 );
201 });
202 }
203}
204
205export function deactivate(): Thenable<void> {
206 if (!Server.client) {
207 return Promise.resolve();
208 }
209 return Server.client.stop();
210}
211
212async function reloadServer(startServer: () => Promise<void>) {
213 if (Server.client != null) {
214 vscode.window.showInformationMessage('Reloading rust-analyzer...');
215 await Server.client.stop();
216 await startServer();
217 }
218}
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
index e1b0d13e7..014e96f75 100644
--- a/editors/code/src/highlighting.ts
+++ b/editors/code/src/highlighting.ts
@@ -1,10 +1,69 @@
1import seedrandom = require('seedrandom');
2import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
3import * 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
6import { ColorTheme, TextMateRuleSettings } from './color_theme';
7
8import { Ctx, sendRequestWithRetry } from './ctx';
9
10export function activateHighlighting(ctx: Ctx) {
11 const highlighter = new Highlighter(ctx);
12 ctx.onDidRestart(client => {
13 client.onNotification(
14 'rust-analyzer/publishDecorations',
15 (params: PublishDecorationsParams) => {
16 if (!ctx.config.highlightingOn) return;
17
18 const targetEditor = vscode.window.visibleTextEditors.find(
19 editor => {
20 const unescapedUri = unescape(
21 editor.document.uri.toString(),
22 );
23 // Unescaped URI looks like:
24 // file:///c:/Workspace/ra-test/src/main.rs
25 return unescapedUri === params.uri;
26 },
27 );
28 if (!targetEditor) return;
29
30 highlighter.setHighlights(targetEditor, params.decorations);
31 },
32 );
33 });
34
35 vscode.workspace.onDidChangeConfiguration(
36 _ => highlighter.removeHighlights(),
37 ctx.subscriptions,
38 );
39
40 vscode.window.onDidChangeActiveTextEditor(
41 async (editor: vscode.TextEditor | undefined) => {
42 if (!editor || editor.document.languageId !== 'rust') return;
43 if (!ctx.config.highlightingOn) return;
44 let client = ctx.client;
45 if (!client) return;
46
47 const params: lc.TextDocumentIdentifier = {
48 uri: editor.document.uri.toString(),
49 };
50 const decorations = await sendRequestWithRetry<Decoration[]>(
51 client,
52 'rust-analyzer/decorationsRequest',
53 params,
54 );
55 highlighter.setHighlights(editor, decorations);
56 },
57 ctx.subscriptions,
58 );
59}
4 60
5import { Server } from './server'; 61interface PublishDecorationsParams {
62 uri: string;
63 decorations: Decoration[];
64}
6 65
7export interface Decoration { 66interface Decoration {
8 range: lc.Range; 67 range: lc.Range;
9 tag: string; 68 tag: string;
10 bindingHash?: string; 69 bindingHash?: string;
@@ -23,62 +82,17 @@ function fancify(seed: string, shade: 'light' | 'dark') {
23 return `hsl(${h},${s}%,${l}%)`; 82 return `hsl(${h},${s}%,${l}%)`;
24} 83}
25 84
26export class Highlighter { 85class Highlighter {
27 private static initDecorations(): Map< 86 private ctx: Ctx;
28 string,
29 vscode.TextEditorDecorationType
30 > {
31 const decoration = (
32 tag: string,
33 textDecoration?: string,
34 ): [string, vscode.TextEditorDecorationType] => {
35 const color = new vscode.ThemeColor('ralsp.' + tag);
36 const decor = vscode.window.createTextEditorDecorationType({
37 color,
38 textDecoration,
39 });
40 return [tag, decor];
41 };
42
43 const decorations: Iterable<[
44 string,
45 vscode.TextEditorDecorationType,
46 ]> = [
47 decoration('comment'),
48 decoration('string'),
49 decoration('keyword'),
50 decoration('keyword.control'),
51 decoration('keyword.unsafe'),
52 decoration('function'),
53 decoration('parameter'),
54 decoration('constant'),
55 decoration('type.builtin'),
56 decoration('type.generic'),
57 decoration('type.lifetime'),
58 decoration('type.param'),
59 decoration('type.self'),
60 decoration('type'),
61 decoration('text'),
62 decoration('attribute'),
63 decoration('literal'),
64 decoration('literal.numeric'),
65 decoration('literal.char'),
66 decoration('literal.byte'),
67 decoration('macro'),
68 decoration('variable'),
69 decoration('variable.mut', 'underline'),
70 decoration('field'),
71 decoration('module'),
72 ];
73
74 return new Map<string, vscode.TextEditorDecorationType>(decorations);
75 }
76
77 private decorations: Map< 87 private decorations: Map<
78 string, 88 string,
79 vscode.TextEditorDecorationType 89 vscode.TextEditorDecorationType
80 > | null = null; 90 > | null = null;
81 91
92 constructor(ctx: Ctx) {
93 this.ctx = ctx;
94 }
95
82 public removeHighlights() { 96 public removeHighlights() {
83 if (this.decorations == null) { 97 if (this.decorations == null) {
84 return; 98 return;
@@ -93,12 +107,14 @@ export class Highlighter {
93 } 107 }
94 108
95 public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) { 109 public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) {
110 let client = this.ctx.client;
111 if (!client) return;
96 // Initialize decorations if necessary 112 // Initialize decorations if necessary
97 // 113 //
98 // Note: decoration objects need to be kept around so we can dispose them 114 // Note: decoration objects need to be kept around so we can dispose them
99 // if the user disables syntax highlighting 115 // if the user disables syntax highlighting
100 if (this.decorations == null) { 116 if (this.decorations == null) {
101 this.decorations = Highlighter.initDecorations(); 117 this.decorations = initDecorations();
102 } 118 }
103 119
104 const byTag: Map<string, vscode.Range[]> = new Map(); 120 const byTag: Map<string, vscode.Range[]> = new Map();
@@ -106,7 +122,7 @@ export class Highlighter {
106 string, 122 string,
107 [vscode.Range[], boolean] 123 [vscode.Range[], boolean]
108 > = new Map(); 124 > = new Map();
109 const rainbowTime = Server.config.rainbowHighlightingOn; 125 const rainbowTime = this.ctx.config.rainbowHighlightingOn;
110 126
111 for (const tag of this.decorations.keys()) { 127 for (const tag of this.decorations.keys()) {
112 byTag.set(tag, []); 128 byTag.set(tag, []);
@@ -125,13 +141,13 @@ export class Highlighter {
125 colorfulIdents 141 colorfulIdents
126 .get(d.bindingHash)![0] 142 .get(d.bindingHash)![0]
127 .push( 143 .push(
128 Server.client.protocol2CodeConverter.asRange(d.range), 144 client.protocol2CodeConverter.asRange(d.range),
129 ); 145 );
130 } else { 146 } else {
131 byTag 147 byTag
132 .get(d.tag)! 148 .get(d.tag)!
133 .push( 149 .push(
134 Server.client.protocol2CodeConverter.asRange(d.range), 150 client.protocol2CodeConverter.asRange(d.range),
135 ); 151 );
136 } 152 }
137 } 153 }
@@ -154,3 +170,80 @@ export class Highlighter {
154 } 170 }
155 } 171 }
156} 172}
173
174function initDecorations(): Map<string, vscode.TextEditorDecorationType> {
175 const theme = ColorTheme.load();
176 const res = new Map();
177 TAG_TO_SCOPES.forEach((scopes, tag) => {
178 if (!scopes) throw `unmapped tag: ${tag}`;
179 let rule = theme.lookup(scopes);
180 const decor = createDecorationFromTextmate(rule);
181 res.set(tag, decor);
182 });
183 return res;
184}
185
186function createDecorationFromTextmate(
187 themeStyle: TextMateRuleSettings,
188): vscode.TextEditorDecorationType {
189 const decorationOptions: vscode.DecorationRenderOptions = {};
190 decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;
191
192 if (themeStyle.foreground) {
193 decorationOptions.color = themeStyle.foreground;
194 }
195
196 if (themeStyle.background) {
197 decorationOptions.backgroundColor = themeStyle.background;
198 }
199
200 if (themeStyle.fontStyle) {
201 const parts: string[] = themeStyle.fontStyle.split(' ');
202 parts.forEach(part => {
203 switch (part) {
204 case 'italic':
205 decorationOptions.fontStyle = 'italic';
206 break;
207 case 'bold':
208 decorationOptions.fontWeight = 'bold';
209 break;
210 case 'underline':
211 decorationOptions.textDecoration = 'underline';
212 break;
213 default:
214 break;
215 }
216 });
217 }
218 return vscode.window.createTextEditorDecorationType(decorationOptions);
219}
220
221// sync with tags from `syntax_highlighting.rs`.
222const TAG_TO_SCOPES = new Map<string, string[]>([
223 ["field", ["entity.name.field"]],
224 ["function", ["entity.name.function"]],
225 ["module", ["entity.name.module"]],
226 ["constant", ["entity.name.constant"]],
227 ["macro", ["entity.name.macro"]],
228
229 ["variable", ["variable"]],
230 ["variable.mut", ["variable", "meta.mutable"]],
231
232 ["type", ["entity.name.type"]],
233 ["type.builtin", ["entity.name.type", "support.type.primitive"]],
234 ["type.self", ["entity.name.type.parameter.self"]],
235 ["type.param", ["entity.name.type.parameter"]],
236 ["type.lifetime", ["entity.name.type.lifetime"]],
237
238 ["literal.byte", ["constant.character.byte"]],
239 ["literal.char", ["constant.character"]],
240 ["literal.numeric", ["constant.numeric"]],
241
242 ["comment", ["comment"]],
243 ["string", ["string.quoted"]],
244 ["attribute", ["meta.attribute"]],
245
246 ["keyword", ["keyword"]],
247 ["keyword.unsafe", ["keyword.other.unsafe"]],
248 ["keyword.control", ["keyword.control"]],
249]);
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
new file mode 100644
index 000000000..6dd767d72
--- /dev/null
+++ b/editors/code/src/inlay_hints.ts
@@ -0,0 +1,120 @@
1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient';
3
4import { Ctx, sendRequestWithRetry } from './ctx';
5
6export function activateInlayHints(ctx: Ctx) {
7 const hintsUpdater = new HintsUpdater(ctx);
8 vscode.window.onDidChangeVisibleTextEditors(async _ => {
9 await hintsUpdater.refresh();
10 }, ctx.subscriptions);
11
12 vscode.workspace.onDidChangeTextDocument(async e => {
13 if (e.contentChanges.length === 0) return;
14 if (e.document.languageId !== 'rust') return;
15 await hintsUpdater.refresh();
16 }, ctx.subscriptions);
17
18 vscode.workspace.onDidChangeConfiguration(_ => {
19 hintsUpdater.setEnabled(ctx.config.displayInlayHints);
20 }, ctx.subscriptions);
21
22 ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints));
23}
24
25interface InlayHintsParams {
26 textDocument: lc.TextDocumentIdentifier;
27}
28
29interface InlayHint {
30 range: vscode.Range;
31 kind: string;
32 label: string;
33}
34
35const typeHintDecorationType = vscode.window.createTextEditorDecorationType({
36 after: {
37 color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
38 },
39});
40
41class HintsUpdater {
42 private pending: Map<string, vscode.CancellationTokenSource> = new Map();
43 private ctx: Ctx;
44 private enabled = true;
45
46 constructor(ctx: Ctx) {
47 this.ctx = ctx;
48 }
49
50 async setEnabled(enabled: boolean) {
51 if (this.enabled == enabled) return;
52 this.enabled = enabled;
53
54 if (this.enabled) {
55 await this.refresh();
56 } else {
57 this.allEditors.forEach(it => this.setDecorations(it, []));
58 }
59 }
60
61 async refresh() {
62 if (!this.enabled) return;
63 const promises = this.allEditors.map(it => this.refreshEditor(it));
64 await Promise.all(promises);
65 }
66
67 private async refreshEditor(editor: vscode.TextEditor): Promise<void> {
68 const newHints = await this.queryHints(editor.document.uri.toString());
69 if (newHints == null) return;
70 const newDecorations = newHints.map(hint => ({
71 range: hint.range,
72 renderOptions: {
73 after: {
74 contentText: `: ${hint.label}`,
75 },
76 },
77 }));
78 this.setDecorations(editor, newDecorations);
79 }
80
81 private get allEditors(): vscode.TextEditor[] {
82 return vscode.window.visibleTextEditors.filter(
83 editor => editor.document.languageId === 'rust',
84 );
85 }
86
87 private setDecorations(
88 editor: vscode.TextEditor,
89 decorations: vscode.DecorationOptions[],
90 ) {
91 editor.setDecorations(
92 typeHintDecorationType,
93 this.enabled ? decorations : [],
94 );
95 }
96
97 private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
98 let client = this.ctx.client;
99 if (!client) return null;
100 const request: InlayHintsParams = {
101 textDocument: { uri: documentUri },
102 };
103 let tokenSource = new vscode.CancellationTokenSource();
104 let prev = this.pending.get(documentUri);
105 if (prev) prev.cancel();
106 this.pending.set(documentUri, tokenSource);
107 try {
108 return await sendRequestWithRetry<InlayHint[] | null>(
109 client,
110 'rust-analyzer/inlayHints',
111 request,
112 tokenSource.token,
113 );
114 } finally {
115 if (!tokenSource.token.isCancellationRequested) {
116 this.pending.delete(documentUri);
117 }
118 }
119 }
120}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
new file mode 100644
index 000000000..51dedd5ef
--- /dev/null
+++ b/editors/code/src/main.ts
@@ -0,0 +1,51 @@
1import * as vscode from 'vscode';
2
3import * as commands from './commands';
4import { activateInlayHints } from './inlay_hints';
5import { activateStatusDisplay } from './status_display';
6import { Ctx } from './ctx';
7import { activateHighlighting } from './highlighting';
8
9let ctx!: Ctx;
10
11export async function activate(context: vscode.ExtensionContext) {
12 ctx = new Ctx(context);
13
14 // Note: we try to start the server before we register various commands, so
15 // that it registers its `onDidChangeDocument` handler before us.
16 //
17 // This a horribly, horribly wrong way to deal with this problem.
18 try {
19 await ctx.restartServer();
20 } catch (e) {
21 vscode.window.showErrorMessage(e.message);
22 }
23
24
25 // Commands which invokes manually via command pallet, shortcut, etc.
26 ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
27 ctx.registerCommand('collectGarbage', commands.collectGarbage);
28 ctx.registerCommand('matchingBrace', commands.matchingBrace);
29 ctx.registerCommand('joinLines', commands.joinLines);
30 ctx.registerCommand('parentModule', commands.parentModule);
31 ctx.registerCommand('syntaxTree', commands.syntaxTree);
32 ctx.registerCommand('expandMacro', commands.expandMacro);
33 ctx.registerCommand('run', commands.run);
34 ctx.registerCommand('reload', commands.reload);
35
36 // Internal commands which are invoked by the server.
37 ctx.registerCommand('runSingle', commands.runSingle);
38 ctx.registerCommand('showReferences', commands.showReferences);
39 ctx.registerCommand('applySourceChange', commands.applySourceChange);
40
41 if (ctx.config.enableEnhancedTyping) {
42 ctx.overrideCommand('type', commands.onEnter);
43 }
44 activateStatusDisplay(ctx);
45 activateHighlighting(ctx);
46 activateInlayHints(ctx);
47}
48
49export async function deactivate() {
50 await ctx?.client?.stop();
51}
diff --git a/editors/code/src/notifications/index.ts b/editors/code/src/notifications/index.ts
deleted file mode 100644
index 74c4c3563..000000000
--- a/editors/code/src/notifications/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1import * as publishDecorations from './publish_decorations';
2
3export { publishDecorations };
diff --git a/editors/code/src/notifications/publish_decorations.ts b/editors/code/src/notifications/publish_decorations.ts
deleted file mode 100644
index f23e286ad..000000000
--- a/editors/code/src/notifications/publish_decorations.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as vscode from 'vscode';
2
3import { Decoration } from '../highlighting';
4import { Server } from '../server';
5
6export interface PublishDecorationsParams {
7 uri: string;
8 decorations: Decoration[];
9}
10
11export function handle(params: PublishDecorationsParams) {
12 const targetEditor = vscode.window.visibleTextEditors.find(editor => {
13 const unescapedUri = unescape(editor.document.uri.toString());
14 // Unescaped URI looks like:
15 // file:///c:/Workspace/ra-test/src/main.rs
16 return unescapedUri === params.uri;
17 });
18
19 if (!Server.config.highlightingOn || !targetEditor) {
20 return;
21 }
22
23 Server.highlighter.setHighlights(targetEditor, params.decorations);
24}
diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts
deleted file mode 100644
index 5ace1d0fa..000000000
--- a/editors/code/src/server.ts
+++ /dev/null
@@ -1,109 +0,0 @@
1import { lookpath } from 'lookpath';
2import { homedir, platform } from 'os';
3import * as lc from 'vscode-languageclient';
4
5import { window, workspace } from 'vscode';
6import { Config } from './config';
7import { Highlighter } from './highlighting';
8
9function expandPathResolving(path: string) {
10 if (path.startsWith('~/')) {
11 return path.replace('~', homedir());
12 }
13 return path;
14}
15
16export class Server {
17 public static highlighter = new Highlighter();
18 public static config = new Config();
19 public static client: lc.LanguageClient;
20
21 public static async start(
22 notificationHandlers: Iterable<[string, lc.GenericNotificationHandler]>,
23 ) {
24 // '.' Is the fallback if no folder is open
25 // 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.
26 let folder: string = '.';
27 if (workspace.workspaceFolders !== undefined) {
28 folder = workspace.workspaceFolders[0].uri.fsPath.toString();
29 }
30
31 const command = expandPathResolving(this.config.raLspServerPath);
32 // FIXME: remove check when the following issue is fixed:
33 // https://github.com/otiai10/lookpath/issues/4
34 if (platform() !== 'win32') {
35 if (!(await lookpath(command))) {
36 throw new Error(
37 `Cannot find rust-analyzer server \`${command}\` in PATH.`,
38 );
39 }
40 }
41 const run: lc.Executable = {
42 command,
43 options: { cwd: folder },
44 };
45 const serverOptions: lc.ServerOptions = {
46 run,
47 debug: run,
48 };
49 const traceOutputChannel = window.createOutputChannel(
50 'Rust Analyzer Language Server Trace',
51 );
52 const clientOptions: lc.LanguageClientOptions = {
53 documentSelector: [{ scheme: 'file', language: 'rust' }],
54 initializationOptions: {
55 publishDecorations: true,
56 lruCapacity: Server.config.lruCapacity,
57 maxInlayHintLength: Server.config.maxInlayHintLength,
58 excludeGlobs: Server.config.excludeGlobs,
59 useClientWatching: Server.config.useClientWatching,
60 featureFlags: Server.config.featureFlags,
61 withSysroot: Server.config.withSysroot,
62 cargoFeatures: Server.config.cargoFeatures,
63 },
64 traceOutputChannel,
65 };
66
67 Server.client = new lc.LanguageClient(
68 'rust-analyzer',
69 'Rust Analyzer Language Server',
70 serverOptions,
71 clientOptions,
72 );
73 // HACK: This is an awful way of filtering out the decorations notifications
74 // However, pending proper support, this is the most effecitve approach
75 // Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages
76 // Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting)
77 // This also requires considering our settings strategy, which is work which needs doing
78 // @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests
79 Server.client._tracer = {
80 log: (messageOrDataObject: string | any, data?: string) => {
81 if (typeof messageOrDataObject === 'string') {
82 if (
83 messageOrDataObject.includes(
84 'rust-analyzer/publishDecorations',
85 ) ||
86 messageOrDataObject.includes(
87 'rust-analyzer/decorationsRequest',
88 )
89 ) {
90 // Don't log publish decorations requests
91 } else {
92 // @ts-ignore This is just a utility function
93 Server.client.logTrace(messageOrDataObject, data);
94 }
95 } else {
96 // @ts-ignore
97 Server.client.logObjectTrace(messageOrDataObject);
98 }
99 },
100 };
101 Server.client.registerProposedFeatures();
102 Server.client.onReady().then(() => {
103 for (const [type, handler] of notificationHandlers) {
104 Server.client.onNotification(type, handler);
105 }
106 });
107 Server.client.start();
108 }
109}
diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/source_change.ts
index 8167398b1..a336269ba 100644
--- a/editors/code/src/commands/apply_source_change.ts
+++ b/editors/code/src/source_change.ts
@@ -1,7 +1,7 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as lc from 'vscode-languageclient'; 2import * as lc from 'vscode-languageclient';
3 3
4import { Server } from '../server'; 4import { Ctx } from './ctx';
5 5
6export interface SourceChange { 6export interface SourceChange {
7 label: string; 7 label: string;
@@ -9,8 +9,11 @@ export interface SourceChange {
9 cursorPosition?: lc.TextDocumentPositionParams; 9 cursorPosition?: lc.TextDocumentPositionParams;
10} 10}
11 11
12export async function handle(change: SourceChange) { 12export async function applySourceChange(ctx: Ctx, change: SourceChange) {
13 const wsEdit = Server.client.protocol2CodeConverter.asWorkspaceEdit( 13 const client = ctx.client;
14 if (!client) return;
15
16 const wsEdit = client.protocol2CodeConverter.asWorkspaceEdit(
14 change.workspaceEdit, 17 change.workspaceEdit,
15 ); 18 );
16 let created; 19 let created;
@@ -32,10 +35,10 @@ export async function handle(change: SourceChange) {
32 const doc = await vscode.workspace.openTextDocument(toOpenUri); 35 const doc = await vscode.workspace.openTextDocument(toOpenUri);
33 await vscode.window.showTextDocument(doc); 36 await vscode.window.showTextDocument(doc);
34 } else if (toReveal) { 37 } else if (toReveal) {
35 const uri = Server.client.protocol2CodeConverter.asUri( 38 const uri = client.protocol2CodeConverter.asUri(
36 toReveal.textDocument.uri, 39 toReveal.textDocument.uri,
37 ); 40 );
38 const position = Server.client.protocol2CodeConverter.asPosition( 41 const position = client.protocol2CodeConverter.asPosition(
39 toReveal.position, 42 toReveal.position,
40 ); 43 );
41 const editor = vscode.window.activeTextEditor; 44 const editor = vscode.window.activeTextEditor;
diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts
new file mode 100644
index 000000000..08cdc8bdf
--- /dev/null
+++ b/editors/code/src/status_display.ts
@@ -0,0 +1,115 @@
1import * as vscode from 'vscode';
2
3import { Ctx } from './ctx';
4
5const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
7export function activateStatusDisplay(ctx: Ctx) {
8 const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command);
9 ctx.pushCleanup(statusDisplay);
10 ctx.onDidRestart(client => {
11 client.onNotification('$/progress', params => statusDisplay.handleProgressNotification(params));
12 });
13}
14
15class StatusDisplay implements vscode.Disposable {
16 packageName?: string;
17
18 private i = 0;
19 private statusBarItem: vscode.StatusBarItem;
20 private command: string;
21 private timer?: NodeJS.Timeout;
22
23 constructor(command: string) {
24 this.statusBarItem = vscode.window.createStatusBarItem(
25 vscode.StatusBarAlignment.Left,
26 10,
27 );
28 this.command = command;
29 this.statusBarItem.hide();
30 }
31
32 show() {
33 this.packageName = undefined;
34
35 this.timer =
36 this.timer ||
37 setInterval(() => {
38 if (this.packageName) {
39 this.statusBarItem!.text = `cargo ${this.command} [${
40 this.packageName
41 }] ${this.frame()}`;
42 } else {
43 this.statusBarItem!.text = `cargo ${
44 this.command
45 } ${this.frame()}`;
46 }
47 }, 300);
48
49 this.statusBarItem.show();
50 }
51
52 hide() {
53 if (this.timer) {
54 clearInterval(this.timer);
55 this.timer = undefined;
56 }
57
58 this.statusBarItem.hide();
59 }
60
61 dispose() {
62 if (this.timer) {
63 clearInterval(this.timer);
64 this.timer = undefined;
65 }
66
67 this.statusBarItem.dispose();
68 }
69
70 handleProgressNotification(params: ProgressParams) {
71 const { token, value } = params;
72 if (token !== 'rustAnalyzer/cargoWatcher') {
73 return;
74 }
75
76 switch (value.kind) {
77 case 'begin':
78 this.show();
79 break;
80
81 case 'report':
82 if (value.message) {
83 this.packageName = value.message;
84 }
85 break;
86
87 case 'end':
88 this.hide();
89 break;
90 }
91 }
92
93 private frame() {
94 return spinnerFrames[(this.i = ++this.i % spinnerFrames.length)];
95 }
96}
97
98// FIXME: Replace this once vscode-languageclient is updated to LSP 3.15
99interface ProgressParams {
100 token: string;
101 value: WorkDoneProgress;
102}
103
104enum WorkDoneProgressKind {
105 Begin = 'begin',
106 Report = 'report',
107 End = 'end',
108}
109
110interface WorkDoneProgress {
111 kind: WorkDoneProgressKind;
112 message?: string;
113 cancelable?: boolean;
114 percentage?: string;
115}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json b/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json
deleted file mode 100644
index d874e99bc..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json
+++ /dev/null
@@ -1,110 +0,0 @@
1{
2 "message": "this argument is passed by reference, but would be more efficient if passed by value",
3 "code": {
4 "code": "clippy::trivially_copy_pass_by_ref",
5 "explanation": null
6 },
7 "level": "warning",
8 "spans": [
9 {
10 "file_name": "compiler/mir/tagset.rs",
11 "byte_start": 941,
12 "byte_end": 946,
13 "line_start": 42,
14 "line_end": 42,
15 "column_start": 24,
16 "column_end": 29,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
21 "highlight_start": 24,
22 "highlight_end": 29
23 }
24 ],
25 "label": null,
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "lint level defined here",
34 "code": null,
35 "level": "note",
36 "spans": [
37 {
38 "file_name": "compiler/lib.rs",
39 "byte_start": 8,
40 "byte_end": 19,
41 "line_start": 1,
42 "line_end": 1,
43 "column_start": 9,
44 "column_end": 20,
45 "is_primary": true,
46 "text": [
47 {
48 "text": "#![warn(clippy::all)]",
49 "highlight_start": 9,
50 "highlight_end": 20
51 }
52 ],
53 "label": null,
54 "suggested_replacement": null,
55 "suggestion_applicability": null,
56 "expansion": null
57 }
58 ],
59 "children": [],
60 "rendered": null
61 },
62 {
63 "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
64 "code": null,
65 "level": "note",
66 "spans": [],
67 "children": [],
68 "rendered": null
69 },
70 {
71 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
72 "code": null,
73 "level": "help",
74 "spans": [],
75 "children": [],
76 "rendered": null
77 },
78 {
79 "message": "consider passing by value instead",
80 "code": null,
81 "level": "help",
82 "spans": [
83 {
84 "file_name": "compiler/mir/tagset.rs",
85 "byte_start": 941,
86 "byte_end": 946,
87 "line_start": 42,
88 "line_end": 42,
89 "column_start": 24,
90 "column_end": 29,
91 "is_primary": true,
92 "text": [
93 {
94 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
95 "highlight_start": 24,
96 "highlight_end": 29
97 }
98 ],
99 "label": null,
100 "suggested_replacement": "self",
101 "suggestion_applicability": "Unspecified",
102 "expansion": null
103 }
104 ],
105 "children": [],
106 "rendered": null
107 }
108 ],
109 "rendered": "warning: this argument is passed by reference, but would be more efficient if passed by value\n --> compiler/mir/tagset.rs:42:24\n |\n42 | pub fn is_disjoint(&self, other: Self) -> bool {\n | ^^^^^ help: consider passing by value instead: `self`\n |\nnote: lint level defined here\n --> compiler/lib.rs:1:9\n |\n1 | #![warn(clippy::all)]\n | ^^^^^^^^^^^\n = note: #[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref\n\n"
110}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json
deleted file mode 100644
index ea5c976d1..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json
+++ /dev/null
@@ -1,42 +0,0 @@
1{
2 "message": "method `next` has an incompatible type for trait",
3 "code": {
4 "code": "E0053",
5 "explanation": "\nThe parameters of any trait method must match between a trait implementation\nand the trait definition.\n\nHere are a couple examples of this error:\n\n```compile_fail,E0053\ntrait Foo {\n fn foo(x: u16);\n fn bar(&self);\n}\n\nstruct Bar;\n\nimpl Foo for Bar {\n // error, expected u16, found i16\n fn foo(x: i16) { }\n\n // error, types differ in mutability\n fn bar(&mut self) { }\n}\n```\n"
6 },
7 "level": "error",
8 "spans": [
9 {
10 "file_name": "compiler/ty/list_iter.rs",
11 "byte_start": 1307,
12 "byte_end": 1350,
13 "line_start": 52,
14 "line_end": 52,
15 "column_start": 5,
16 "column_end": 48,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {",
21 "highlight_start": 5,
22 "highlight_end": 48
23 }
24 ],
25 "label": "types differ in mutability",
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`",
34 "code": null,
35 "level": "note",
36 "spans": [],
37 "children": [],
38 "rendered": null
39 }
40 ],
41 "rendered": "error[E0053]: method `next` has an incompatible type for trait\n --> compiler/ty/list_iter.rs:52:5\n |\n52 | fn next(&self) -> Option<&'list ty::Ref<M>> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ in mutability\n |\n = note: expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`\n\n"
42}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json
deleted file mode 100644
index 3154d1098..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json
+++ /dev/null
@@ -1,114 +0,0 @@
1{
2 "message": "this function takes 2 parameters but 3 parameters were supplied",
3 "code": {
4 "code": "E0061",
5 "explanation": "\nThe number of arguments passed to a function must match the number of arguments\nspecified in the function signature.\n\nFor example, a function like:\n\n```\nfn f(a: u16, b: &str) {}\n```\n\nMust always be called with exactly two arguments, e.g., `f(2, \"test\")`.\n\nNote that Rust does not have a notion of optional function arguments or\nvariadic functions (except for its C-FFI).\n"
6 },
7 "level": "error",
8 "spans": [
9 {
10 "file_name": "compiler/ty/select.rs",
11 "byte_start": 8787,
12 "byte_end": 9241,
13 "line_start": 219,
14 "line_end": 231,
15 "column_start": 5,
16 "column_end": 6,
17 "is_primary": false,
18 "text": [
19 {
20 "text": " pub fn add_evidence(",
21 "highlight_start": 5,
22 "highlight_end": 25
23 },
24 {
25 "text": " &mut self,",
26 "highlight_start": 1,
27 "highlight_end": 19
28 },
29 {
30 "text": " target_poly: &ty::Ref<ty::Poly>,",
31 "highlight_start": 1,
32 "highlight_end": 41
33 },
34 {
35 "text": " evidence_poly: &ty::Ref<ty::Poly>,",
36 "highlight_start": 1,
37 "highlight_end": 43
38 },
39 {
40 "text": " ) {",
41 "highlight_start": 1,
42 "highlight_end": 8
43 },
44 {
45 "text": " match target_poly {",
46 "highlight_start": 1,
47 "highlight_end": 28
48 },
49 {
50 "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
51 "highlight_start": 1,
52 "highlight_end": 81
53 },
54 {
55 "text": " ty::Ref::Fixed(target_ty) => {",
56 "highlight_start": 1,
57 "highlight_end": 43
58 },
59 {
60 "text": " let evidence_ty = evidence_poly.resolve_to_ty();",
61 "highlight_start": 1,
62 "highlight_end": 65
63 },
64 {
65 "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
66 "highlight_start": 1,
67 "highlight_end": 76
68 },
69 {
70 "text": " }",
71 "highlight_start": 1,
72 "highlight_end": 14
73 },
74 {
75 "text": " }",
76 "highlight_start": 1,
77 "highlight_end": 10
78 },
79 {
80 "text": " }",
81 "highlight_start": 1,
82 "highlight_end": 6
83 }
84 ],
85 "label": "defined here",
86 "suggested_replacement": null,
87 "suggestion_applicability": null,
88 "expansion": null
89 },
90 {
91 "file_name": "compiler/ty/select.rs",
92 "byte_start": 4045,
93 "byte_end": 4057,
94 "line_start": 104,
95 "line_end": 104,
96 "column_start": 18,
97 "column_end": 30,
98 "is_primary": true,
99 "text": [
100 {
101 "text": " self.add_evidence(target_fixed, evidence_fixed, false);",
102 "highlight_start": 18,
103 "highlight_end": 30
104 }
105 ],
106 "label": "expected 2 parameters",
107 "suggested_replacement": null,
108 "suggestion_applicability": null,
109 "expansion": null
110 }
111 ],
112 "children": [],
113 "rendered": "error[E0061]: this function takes 2 parameters but 3 parameters were supplied\n --> compiler/ty/select.rs:104:18\n |\n104 | self.add_evidence(target_fixed, evidence_fixed, false);\n | ^^^^^^^^^^^^ expected 2 parameters\n...\n219 | / pub fn add_evidence(\n220 | | &mut self,\n221 | | target_poly: &ty::Ref<ty::Poly>,\n222 | | evidence_poly: &ty::Ref<ty::Poly>,\n... |\n230 | | }\n231 | | }\n | |_____- defined here\n\n"
114}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json
deleted file mode 100644
index bfef33c7d..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0277.json
+++ /dev/null
@@ -1,261 +0,0 @@
1{
2 "rendered": "error[E0277]: can't compare `{integer}` with `&str`\n --> src/main.rs:2:5\n |\n2 | assert_eq!(1, \"love\");\n | ^^^^^^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &str`\n |\n = help: the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`\n = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)\n\n",
3 "children": [
4 {
5 "children": [],
6 "code": null,
7 "level": "help",
8 "message": "the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
9 "rendered": null,
10 "spans": []
11 }
12 ],
13 "code": {
14 "code": "E0277",
15 "explanation": "\nYou tried to use a type which doesn't implement some trait in a place which\nexpected that trait. Erroneous code example:\n\n```compile_fail,E0277\n// here we declare the Foo trait with a bar method\ntrait Foo {\n fn bar(&self);\n}\n\n// we now declare a function which takes an object implementing the Foo trait\nfn some_func<T: Foo>(foo: T) {\n foo.bar();\n}\n\nfn main() {\n // we now call the method with the i32 type, which doesn't implement\n // the Foo trait\n some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied\n}\n```\n\nIn order to fix this error, verify that the type you're using does implement\nthe trait. Example:\n\n```\ntrait Foo {\n fn bar(&self);\n}\n\nfn some_func<T: Foo>(foo: T) {\n foo.bar(); // we can now use this method since i32 implements the\n // Foo trait\n}\n\n// we implement the trait on the i32 type\nimpl Foo for i32 {\n fn bar(&self) {}\n}\n\nfn main() {\n some_func(5i32); // ok!\n}\n```\n\nOr in a generic context, an erroneous code example would look like:\n\n```compile_fail,E0277\nfn some_func<T>(foo: T) {\n println!(\"{:?}\", foo); // error: the trait `core::fmt::Debug` is not\n // implemented for the type `T`\n}\n\nfn main() {\n // We now call the method with the i32 type,\n // which *does* implement the Debug trait.\n some_func(5i32);\n}\n```\n\nNote that the error here is in the definition of the generic function: Although\nwe only call it with a parameter that does implement `Debug`, the compiler\nstill rejects the function: It must work with all possible input types. In\norder to make this example compile, we need to restrict the generic type we're\naccepting:\n\n```\nuse std::fmt;\n\n// Restrict the input type to types that implement Debug.\nfn some_func<T: fmt::Debug>(foo: T) {\n println!(\"{:?}\", foo);\n}\n\nfn main() {\n // Calling the method is still fine, as i32 implements Debug.\n some_func(5i32);\n\n // This would fail to compile now:\n // struct WithoutDebug;\n // some_func(WithoutDebug);\n}\n```\n\nRust only looks at the signature of the called function, as such it must\nalready specify all requirements that will be used for every type parameter.\n"
16 },
17 "level": "error",
18 "message": "can't compare `{integer}` with `&str`",
19 "spans": [
20 {
21 "byte_end": 155,
22 "byte_start": 153,
23 "column_end": 33,
24 "column_start": 31,
25 "expansion": {
26 "def_site_span": {
27 "byte_end": 940,
28 "byte_start": 0,
29 "column_end": 6,
30 "column_start": 1,
31 "expansion": null,
32 "file_name": "<::core::macros::assert_eq macros>",
33 "is_primary": false,
34 "label": null,
35 "line_end": 36,
36 "line_start": 1,
37 "suggested_replacement": null,
38 "suggestion_applicability": null,
39 "text": [
40 {
41 "highlight_end": 35,
42 "highlight_start": 1,
43 "text": "($ left : expr, $ right : expr) =>"
44 },
45 {
46 "highlight_end": 3,
47 "highlight_start": 1,
48 "text": "({"
49 },
50 {
51 "highlight_end": 33,
52 "highlight_start": 1,
53 "text": " match (& $ left, & $ right)"
54 },
55 {
56 "highlight_end": 7,
57 "highlight_start": 1,
58 "text": " {"
59 },
60 {
61 "highlight_end": 34,
62 "highlight_start": 1,
63 "text": " (left_val, right_val) =>"
64 },
65 {
66 "highlight_end": 11,
67 "highlight_start": 1,
68 "text": " {"
69 },
70 {
71 "highlight_end": 46,
72 "highlight_start": 1,
73 "text": " if ! (* left_val == * right_val)"
74 },
75 {
76 "highlight_end": 15,
77 "highlight_start": 1,
78 "text": " {"
79 },
80 {
81 "highlight_end": 25,
82 "highlight_start": 1,
83 "text": " panic !"
84 },
85 {
86 "highlight_end": 57,
87 "highlight_start": 1,
88 "text": " (r#\"assertion failed: `(left == right)`"
89 },
90 {
91 "highlight_end": 16,
92 "highlight_start": 1,
93 "text": " left: `{:?}`,"
94 },
95 {
96 "highlight_end": 18,
97 "highlight_start": 1,
98 "text": " right: `{:?}`\"#,"
99 },
100 {
101 "highlight_end": 47,
102 "highlight_start": 1,
103 "text": " & * left_val, & * right_val)"
104 },
105 {
106 "highlight_end": 15,
107 "highlight_start": 1,
108 "text": " }"
109 },
110 {
111 "highlight_end": 11,
112 "highlight_start": 1,
113 "text": " }"
114 },
115 {
116 "highlight_end": 7,
117 "highlight_start": 1,
118 "text": " }"
119 },
120 {
121 "highlight_end": 42,
122 "highlight_start": 1,
123 "text": " }) ; ($ left : expr, $ right : expr,) =>"
124 },
125 {
126 "highlight_end": 49,
127 "highlight_start": 1,
128 "text": "({ $ crate :: assert_eq ! ($ left, $ right) }) ;"
129 },
130 {
131 "highlight_end": 53,
132 "highlight_start": 1,
133 "text": "($ left : expr, $ right : expr, $ ($ arg : tt) +) =>"
134 },
135 {
136 "highlight_end": 3,
137 "highlight_start": 1,
138 "text": "({"
139 },
140 {
141 "highlight_end": 37,
142 "highlight_start": 1,
143 "text": " match (& ($ left), & ($ right))"
144 },
145 {
146 "highlight_end": 7,
147 "highlight_start": 1,
148 "text": " {"
149 },
150 {
151 "highlight_end": 34,
152 "highlight_start": 1,
153 "text": " (left_val, right_val) =>"
154 },
155 {
156 "highlight_end": 11,
157 "highlight_start": 1,
158 "text": " {"
159 },
160 {
161 "highlight_end": 46,
162 "highlight_start": 1,
163 "text": " if ! (* left_val == * right_val)"
164 },
165 {
166 "highlight_end": 15,
167 "highlight_start": 1,
168 "text": " {"
169 },
170 {
171 "highlight_end": 25,
172 "highlight_start": 1,
173 "text": " panic !"
174 },
175 {
176 "highlight_end": 57,
177 "highlight_start": 1,
178 "text": " (r#\"assertion failed: `(left == right)`"
179 },
180 {
181 "highlight_end": 16,
182 "highlight_start": 1,
183 "text": " left: `{:?}`,"
184 },
185 {
186 "highlight_end": 22,
187 "highlight_start": 1,
188 "text": " right: `{:?}`: {}\"#,"
189 },
190 {
191 "highlight_end": 72,
192 "highlight_start": 1,
193 "text": " & * left_val, & * right_val, $ crate :: format_args !"
194 },
195 {
196 "highlight_end": 33,
197 "highlight_start": 1,
198 "text": " ($ ($ arg) +))"
199 },
200 {
201 "highlight_end": 15,
202 "highlight_start": 1,
203 "text": " }"
204 },
205 {
206 "highlight_end": 11,
207 "highlight_start": 1,
208 "text": " }"
209 },
210 {
211 "highlight_end": 7,
212 "highlight_start": 1,
213 "text": " }"
214 },
215 {
216 "highlight_end": 6,
217 "highlight_start": 1,
218 "text": " }) ;"
219 }
220 ]
221 },
222 "macro_decl_name": "assert_eq!",
223 "span": {
224 "byte_end": 38,
225 "byte_start": 16,
226 "column_end": 27,
227 "column_start": 5,
228 "expansion": null,
229 "file_name": "src/main.rs",
230 "is_primary": false,
231 "label": null,
232 "line_end": 2,
233 "line_start": 2,
234 "suggested_replacement": null,
235 "suggestion_applicability": null,
236 "text": [
237 {
238 "highlight_end": 27,
239 "highlight_start": 5,
240 "text": " assert_eq!(1, \"love\");"
241 }
242 ]
243 }
244 },
245 "file_name": "<::core::macros::assert_eq macros>",
246 "is_primary": true,
247 "label": "no implementation for `{integer} == &str`",
248 "line_end": 7,
249 "line_start": 7,
250 "suggested_replacement": null,
251 "suggestion_applicability": null,
252 "text": [
253 {
254 "highlight_end": 33,
255 "highlight_start": 31,
256 "text": " if ! (* left_val == * right_val)"
257 }
258 ]
259 }
260 ]
261}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json
deleted file mode 100644
index fb23824a3..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/error/E0308.json
+++ /dev/null
@@ -1,33 +0,0 @@
1{
2 "message": "mismatched types",
3 "code": {
4 "code": "E0308",
5 "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n"
6 },
7 "level": "error",
8 "spans": [
9 {
10 "file_name": "runtime/compiler_support.rs",
11 "byte_start": 1589,
12 "byte_end": 1594,
13 "line_start": 48,
14 "line_end": 48,
15 "column_start": 65,
16 "column_end": 70,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " let layout = alloc::Layout::from_size_align_unchecked(size, align);",
21 "highlight_start": 65,
22 "highlight_end": 70
23 }
24 ],
25 "label": "expected usize, found u32",
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [],
32 "rendered": "error[E0308]: mismatched types\n --> runtime/compiler_support.rs:48:65\n |\n48 | let layout = alloc::Layout::from_size_align_unchecked(size, align);\n | ^^^^^ expected usize, found u32\n\n"
33}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json b/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json
deleted file mode 100644
index d1e2be722..000000000
--- a/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json
+++ /dev/null
@@ -1,72 +0,0 @@
1{
2 "message": "unused variable: `foo`",
3 "code": {
4 "code": "unused_variables",
5 "explanation": null
6 },
7 "level": "warning",
8 "spans": [
9 {
10 "file_name": "driver/subcommand/repl.rs",
11 "byte_start": 9228,
12 "byte_end": 9231,
13 "line_start": 291,
14 "line_end": 291,
15 "column_start": 9,
16 "column_end": 12,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " let foo = 42;",
21 "highlight_start": 9,
22 "highlight_end": 12
23 }
24 ],
25 "label": null,
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "#[warn(unused_variables)] on by default",
34 "code": null,
35 "level": "note",
36 "spans": [],
37 "children": [],
38 "rendered": null
39 },
40 {
41 "message": "consider prefixing with an underscore",
42 "code": null,
43 "level": "help",
44 "spans": [
45 {
46 "file_name": "driver/subcommand/repl.rs",
47 "byte_start": 9228,
48 "byte_end": 9231,
49 "line_start": 291,
50 "line_end": 291,
51 "column_start": 9,
52 "column_end": 12,
53 "is_primary": true,
54 "text": [
55 {
56 "text": " let foo = 42;",
57 "highlight_start": 9,
58 "highlight_end": 12
59 }
60 ],
61 "label": null,
62 "suggested_replacement": "_foo",
63 "suggestion_applicability": "MachineApplicable",
64 "expansion": null
65 }
66 ],
67 "children": [],
68 "rendered": null
69 }
70 ],
71 "rendered": "warning: unused variable: `foo`\n --> driver/subcommand/repl.rs:291:9\n |\n291 | let foo = 42;\n | ^^^ help: consider prefixing with an underscore: `_foo`\n |\n = note: #[warn(unused_variables)] on by default\n\n"
72}
diff --git a/editors/code/src/test/runTest.ts b/editors/code/src/test/runTest.ts
deleted file mode 100644
index d880d47df..000000000
--- a/editors/code/src/test/runTest.ts
+++ /dev/null
@@ -1,22 +0,0 @@
1import * as path from 'path';
2
3import { runTests } from 'vscode-test';
4
5async function main() {
6 try {
7 // The folder containing the Extension Manifest package.json
8 // Passed to `--extensionDevelopmentPath`
9 const extensionDevelopmentPath = path.resolve(__dirname, '../../');
10
11 // The path to the extension test runner script
12 // Passed to --extensionTestsPath
13 const extensionTestsPath = path.resolve(__dirname, './utils/index');
14
15 // Download VS Code, unzip it and run the integration test
16 await runTests({ extensionDevelopmentPath, extensionTestsPath });
17 } catch (err) {
18 process.exit(1);
19 }
20}
21
22main();
diff --git a/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts b/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts
deleted file mode 100644
index 2b25eb705..000000000
--- a/editors/code/src/test/utils/diagnotics/SuggestedFix.test.ts
+++ /dev/null
@@ -1,134 +0,0 @@
1import * as assert from 'assert';
2import * as vscode from 'vscode';
3
4import { SuggestionApplicability } from '../../../utils/diagnostics/rust';
5import SuggestedFix from '../../../utils/diagnostics/SuggestedFix';
6
7const location1 = new vscode.Location(
8 vscode.Uri.file('/file/1'),
9 new vscode.Range(new vscode.Position(1, 2), new vscode.Position(3, 4)),
10);
11
12const location2 = new vscode.Location(
13 vscode.Uri.file('/file/2'),
14 new vscode.Range(new vscode.Position(5, 6), new vscode.Position(7, 8)),
15);
16
17describe('SuggestedFix', () => {
18 describe('isEqual', () => {
19 it('should treat identical instances as equal', () => {
20 const suggestion1 = new SuggestedFix(
21 'Replace me!',
22 location1,
23 'With this!',
24 );
25
26 const suggestion2 = new SuggestedFix(
27 'Replace me!',
28 location1,
29 'With this!',
30 );
31
32 assert(suggestion1.isEqual(suggestion2));
33 });
34
35 it('should treat instances with different titles as inequal', () => {
36 const suggestion1 = new SuggestedFix(
37 'Replace me!',
38 location1,
39 'With this!',
40 );
41
42 const suggestion2 = new SuggestedFix(
43 'Not the same title!',
44 location1,
45 'With this!',
46 );
47
48 assert(!suggestion1.isEqual(suggestion2));
49 });
50
51 it('should treat instances with different replacements as inequal', () => {
52 const suggestion1 = new SuggestedFix(
53 'Replace me!',
54 location1,
55 'With this!',
56 );
57
58 const suggestion2 = new SuggestedFix(
59 'Replace me!',
60 location1,
61 'With something else!',
62 );
63
64 assert(!suggestion1.isEqual(suggestion2));
65 });
66
67 it('should treat instances with different locations as inequal', () => {
68 const suggestion1 = new SuggestedFix(
69 'Replace me!',
70 location1,
71 'With this!',
72 );
73
74 const suggestion2 = new SuggestedFix(
75 'Replace me!',
76 location2,
77 'With this!',
78 );
79
80 assert(!suggestion1.isEqual(suggestion2));
81 });
82
83 it('should treat instances with different applicability as inequal', () => {
84 const suggestion1 = new SuggestedFix(
85 'Replace me!',
86 location1,
87 'With this!',
88 SuggestionApplicability.MachineApplicable,
89 );
90
91 const suggestion2 = new SuggestedFix(
92 'Replace me!',
93 location2,
94 'With this!',
95 SuggestionApplicability.HasPlaceholders,
96 );
97
98 assert(!suggestion1.isEqual(suggestion2));
99 });
100 });
101
102 describe('toCodeAction', () => {
103 it('should map a simple suggestion', () => {
104 const suggestion = new SuggestedFix(
105 'Replace me!',
106 location1,
107 'With this!',
108 );
109
110 const codeAction = suggestion.toCodeAction();
111 assert.strictEqual(codeAction.kind, vscode.CodeActionKind.QuickFix);
112 assert.strictEqual(codeAction.title, 'Replace me!');
113 assert.strictEqual(codeAction.isPreferred, false);
114
115 const edit = codeAction.edit;
116 if (!edit) {
117 assert.fail('Code Action edit unexpectedly missing');
118 return;
119 }
120
121 const editEntries = edit.entries();
122 assert.strictEqual(editEntries.length, 1);
123
124 const [[editUri, textEdits]] = editEntries;
125 assert.strictEqual(editUri.toString(), location1.uri.toString());
126
127 assert.strictEqual(textEdits.length, 1);
128 const [textEdit] = textEdits;
129
130 assert(textEdit.range.isEqual(location1.range));
131 assert.strictEqual(textEdit.newText, 'With this!');
132 });
133 });
134});
diff --git a/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts b/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts
deleted file mode 100644
index ef09013f4..000000000
--- a/editors/code/src/test/utils/diagnotics/SuggestedFixCollection.test.ts
+++ /dev/null
@@ -1,127 +0,0 @@
1import * as assert from 'assert';
2import * as vscode from 'vscode';
3
4import SuggestedFix from '../../../utils/diagnostics/SuggestedFix';
5import SuggestedFixCollection from '../../../utils/diagnostics/SuggestedFixCollection';
6
7const uri1 = vscode.Uri.file('/file/1');
8const uri2 = vscode.Uri.file('/file/2');
9
10const mockDocument1 = ({
11 uri: uri1,
12} as unknown) as vscode.TextDocument;
13
14const mockDocument2 = ({
15 uri: uri2,
16} as unknown) as vscode.TextDocument;
17
18const range1 = new vscode.Range(
19 new vscode.Position(1, 2),
20 new vscode.Position(3, 4),
21);
22const range2 = new vscode.Range(
23 new vscode.Position(5, 6),
24 new vscode.Position(7, 8),
25);
26
27const diagnostic1 = new vscode.Diagnostic(range1, 'First diagnostic');
28const diagnostic2 = new vscode.Diagnostic(range2, 'Second diagnostic');
29
30// This is a mutable object so return a fresh instance every time
31function suggestion1(): SuggestedFix {
32 return new SuggestedFix(
33 'Replace me!',
34 new vscode.Location(uri1, range1),
35 'With this!',
36 );
37}
38
39describe('SuggestedFixCollection', () => {
40 it('should add a suggestion then return it as a code action', () => {
41 const suggestedFixes = new SuggestedFixCollection();
42 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
43
44 // Specify the document and range that exactly matches
45 const codeActions = suggestedFixes.provideCodeActions(
46 mockDocument1,
47 range1,
48 );
49
50 assert.strictEqual(codeActions.length, 1);
51 const [codeAction] = codeActions;
52 assert.strictEqual(codeAction.title, suggestion1().title);
53
54 const { diagnostics } = codeAction;
55 if (!diagnostics) {
56 assert.fail('Diagnostics unexpectedly missing');
57 return;
58 }
59
60 assert.strictEqual(diagnostics.length, 1);
61 assert.strictEqual(diagnostics[0], diagnostic1);
62 });
63
64 it('should not return code actions for different ranges', () => {
65 const suggestedFixes = new SuggestedFixCollection();
66 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
67
68 const codeActions = suggestedFixes.provideCodeActions(
69 mockDocument1,
70 range2,
71 );
72
73 assert(!codeActions || codeActions.length === 0);
74 });
75
76 it('should not return code actions for different documents', () => {
77 const suggestedFixes = new SuggestedFixCollection();
78 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
79
80 const codeActions = suggestedFixes.provideCodeActions(
81 mockDocument2,
82 range1,
83 );
84
85 assert(!codeActions || codeActions.length === 0);
86 });
87
88 it('should not return code actions that have been cleared', () => {
89 const suggestedFixes = new SuggestedFixCollection();
90 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
91 suggestedFixes.clear();
92
93 const codeActions = suggestedFixes.provideCodeActions(
94 mockDocument1,
95 range1,
96 );
97
98 assert(!codeActions || codeActions.length === 0);
99 });
100
101 it('should merge identical suggestions together', () => {
102 const suggestedFixes = new SuggestedFixCollection();
103
104 // Add the same suggestion for two diagnostics
105 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
106 suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic2);
107
108 const codeActions = suggestedFixes.provideCodeActions(
109 mockDocument1,
110 range1,
111 );
112
113 assert.strictEqual(codeActions.length, 1);
114 const [codeAction] = codeActions;
115 const { diagnostics } = codeAction;
116
117 if (!diagnostics) {
118 assert.fail('Diagnostics unexpectedly missing');
119 return;
120 }
121
122 // We should be associated with both diagnostics
123 assert.strictEqual(diagnostics.length, 2);
124 assert.strictEqual(diagnostics[0], diagnostic1);
125 assert.strictEqual(diagnostics[1], diagnostic2);
126 });
127});
diff --git a/editors/code/src/test/utils/diagnotics/rust.test.ts b/editors/code/src/test/utils/diagnotics/rust.test.ts
deleted file mode 100644
index 358325cc8..000000000
--- a/editors/code/src/test/utils/diagnotics/rust.test.ts
+++ /dev/null
@@ -1,236 +0,0 @@
1import * as assert from 'assert';
2import * as fs from 'fs';
3import * as vscode from 'vscode';
4
5import {
6 MappedRustDiagnostic,
7 mapRustDiagnosticToVsCode,
8 RustDiagnostic,
9 SuggestionApplicability,
10} from '../../../utils/diagnostics/rust';
11
12function loadDiagnosticFixture(name: string): RustDiagnostic {
13 const jsonText = fs
14 .readFileSync(
15 // We're actually in our JavaScript output directory, climb out
16 `${__dirname}/../../../../src/test/fixtures/rust-diagnostics/${name}.json`,
17 )
18 .toString();
19
20 return JSON.parse(jsonText);
21}
22
23function mapFixtureToVsCode(name: string): MappedRustDiagnostic {
24 const rd = loadDiagnosticFixture(name);
25 const mapResult = mapRustDiagnosticToVsCode(rd);
26
27 if (!mapResult) {
28 return assert.fail('Mapping unexpectedly failed');
29 }
30 return mapResult;
31}
32
33describe('mapRustDiagnosticToVsCode', () => {
34 it('should map an incompatible type for trait error', () => {
35 const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
36 'error/E0053',
37 );
38
39 assert.strictEqual(
40 diagnostic.severity,
41 vscode.DiagnosticSeverity.Error,
42 );
43 assert.strictEqual(diagnostic.source, 'rustc');
44 assert.strictEqual(
45 diagnostic.message,
46 [
47 `method \`next\` has an incompatible type for trait`,
48 `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``,
49 ` found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``,
50 ].join('\n'),
51 );
52 assert.strictEqual(diagnostic.code, 'E0053');
53 assert.deepStrictEqual(diagnostic.tags, []);
54
55 // No related information
56 assert.deepStrictEqual(diagnostic.relatedInformation, []);
57
58 // There are no suggested fixes
59 assert.strictEqual(suggestedFixes.length, 0);
60 });
61
62 it('should map an unused variable warning', () => {
63 const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
64 'warning/unused_variables',
65 );
66
67 assert.strictEqual(
68 diagnostic.severity,
69 vscode.DiagnosticSeverity.Warning,
70 );
71 assert.strictEqual(
72 diagnostic.message,
73 [
74 'unused variable: `foo`',
75 '#[warn(unused_variables)] on by default',
76 ].join('\n'),
77 );
78 assert.strictEqual(diagnostic.code, 'unused_variables');
79 assert.strictEqual(diagnostic.source, 'rustc');
80 assert.deepStrictEqual(diagnostic.tags, [
81 vscode.DiagnosticTag.Unnecessary,
82 ]);
83
84 // No related information
85 assert.deepStrictEqual(diagnostic.relatedInformation, []);
86
87 // One suggested fix available to prefix the variable
88 assert.strictEqual(suggestedFixes.length, 1);
89 const [suggestedFix] = suggestedFixes;
90 assert.strictEqual(
91 suggestedFix.title,
92 'consider prefixing with an underscore: `_foo`',
93 );
94 assert.strictEqual(
95 suggestedFix.applicability,
96 SuggestionApplicability.MachineApplicable,
97 );
98 });
99
100 it('should map a wrong number of parameters error', () => {
101 const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
102 'error/E0061',
103 );
104
105 assert.strictEqual(
106 diagnostic.severity,
107 vscode.DiagnosticSeverity.Error,
108 );
109 assert.strictEqual(
110 diagnostic.message,
111 [
112 'this function takes 2 parameters but 3 parameters were supplied',
113 'expected 2 parameters',
114 ].join('\n'),
115 );
116 assert.strictEqual(diagnostic.code, 'E0061');
117 assert.strictEqual(diagnostic.source, 'rustc');
118 assert.deepStrictEqual(diagnostic.tags, []);
119
120 // One related information for the original definition
121 const relatedInformation = diagnostic.relatedInformation;
122 if (!relatedInformation) {
123 assert.fail('Related information unexpectedly undefined');
124 return;
125 }
126 assert.strictEqual(relatedInformation.length, 1);
127 const [related] = relatedInformation;
128 assert.strictEqual(related.message, 'defined here');
129
130 // There are no suggested fixes
131 assert.strictEqual(suggestedFixes.length, 0);
132 });
133
134 it('should map a Clippy copy pass by ref warning', () => {
135 const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
136 'clippy/trivially_copy_pass_by_ref',
137 );
138
139 assert.strictEqual(
140 diagnostic.severity,
141 vscode.DiagnosticSeverity.Warning,
142 );
143 assert.strictEqual(diagnostic.source, 'clippy');
144 assert.strictEqual(
145 diagnostic.message,
146 [
147 'this argument is passed by reference, but would be more efficient if passed by value',
148 '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]',
149 'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref',
150 ].join('\n'),
151 );
152 assert.strictEqual(diagnostic.code, 'trivially_copy_pass_by_ref');
153 assert.deepStrictEqual(diagnostic.tags, []);
154
155 // One related information for the lint definition
156 const relatedInformation = diagnostic.relatedInformation;
157 if (!relatedInformation) {
158 assert.fail('Related information unexpectedly undefined');
159 return;
160 }
161 assert.strictEqual(relatedInformation.length, 1);
162 const [related] = relatedInformation;
163 assert.strictEqual(related.message, 'lint level defined here');
164
165 // One suggested fix to pass by value
166 assert.strictEqual(suggestedFixes.length, 1);
167 const [suggestedFix] = suggestedFixes;
168 assert.strictEqual(
169 suggestedFix.title,
170 'consider passing by value instead: `self`',
171 );
172 // Clippy does not mark this with any applicability
173 assert.strictEqual(
174 suggestedFix.applicability,
175 SuggestionApplicability.Unspecified,
176 );
177 });
178
179 it('should map a mismatched type error', () => {
180 const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
181 'error/E0308',
182 );
183
184 assert.strictEqual(
185 diagnostic.severity,
186 vscode.DiagnosticSeverity.Error,
187 );
188 assert.strictEqual(
189 diagnostic.message,
190 ['mismatched types', 'expected usize, found u32'].join('\n'),
191 );
192 assert.strictEqual(diagnostic.code, 'E0308');
193 assert.strictEqual(diagnostic.source, 'rustc');
194 assert.deepStrictEqual(diagnostic.tags, []);
195
196 // No related information
197 assert.deepStrictEqual(diagnostic.relatedInformation, []);
198
199 // There are no suggested fixes
200 assert.strictEqual(suggestedFixes.length, 0);
201 });
202
203 it('should map a macro invocation location to normal file path', () => {
204 const { location, diagnostic, suggestedFixes } = mapFixtureToVsCode(
205 'error/E0277',
206 );
207
208 assert.strictEqual(
209 diagnostic.severity,
210 vscode.DiagnosticSeverity.Error,
211 );
212 assert.strictEqual(
213 diagnostic.message,
214 [
215 "can't compare `{integer}` with `&str`",
216 'the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`',
217 ].join('\n'),
218 );
219 assert.strictEqual(diagnostic.code, 'E0277');
220 assert.strictEqual(diagnostic.source, 'rustc');
221 assert.deepStrictEqual(diagnostic.tags, []);
222
223 // No related information
224 assert.deepStrictEqual(diagnostic.relatedInformation, []);
225
226 // There are no suggested fixes
227 assert.strictEqual(suggestedFixes.length, 0);
228
229 // The file url should be normal file
230 // Ignore the first part because it depends on vs workspace location
231 assert.strictEqual(
232 location.uri.path.substr(-'src/main.rs'.length),
233 'src/main.rs',
234 );
235 });
236});
diff --git a/editors/code/src/test/utils/diagnotics/vscode.test.ts b/editors/code/src/test/utils/diagnotics/vscode.test.ts
deleted file mode 100644
index 4944dd032..000000000
--- a/editors/code/src/test/utils/diagnotics/vscode.test.ts
+++ /dev/null
@@ -1,98 +0,0 @@
1import * as assert from 'assert';
2import * as vscode from 'vscode';
3
4import { areDiagnosticsEqual } from '../../../utils/diagnostics/vscode';
5
6const range1 = new vscode.Range(
7 new vscode.Position(1, 2),
8 new vscode.Position(3, 4),
9);
10
11const range2 = new vscode.Range(
12 new vscode.Position(5, 6),
13 new vscode.Position(7, 8),
14);
15
16describe('areDiagnosticsEqual', () => {
17 it('should treat identical diagnostics as equal', () => {
18 const diagnostic1 = new vscode.Diagnostic(
19 range1,
20 'Hello, world!',
21 vscode.DiagnosticSeverity.Error,
22 );
23
24 const diagnostic2 = new vscode.Diagnostic(
25 range1,
26 'Hello, world!',
27 vscode.DiagnosticSeverity.Error,
28 );
29
30 assert(areDiagnosticsEqual(diagnostic1, diagnostic2));
31 });
32
33 it('should treat diagnostics with different sources as inequal', () => {
34 const diagnostic1 = new vscode.Diagnostic(
35 range1,
36 'Hello, world!',
37 vscode.DiagnosticSeverity.Error,
38 );
39 diagnostic1.source = 'rustc';
40
41 const diagnostic2 = new vscode.Diagnostic(
42 range1,
43 'Hello, world!',
44 vscode.DiagnosticSeverity.Error,
45 );
46 diagnostic2.source = 'clippy';
47
48 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
49 });
50
51 it('should treat diagnostics with different ranges as inequal', () => {
52 const diagnostic1 = new vscode.Diagnostic(
53 range1,
54 'Hello, world!',
55 vscode.DiagnosticSeverity.Error,
56 );
57
58 const diagnostic2 = new vscode.Diagnostic(
59 range2,
60 'Hello, world!',
61 vscode.DiagnosticSeverity.Error,
62 );
63
64 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
65 });
66
67 it('should treat diagnostics with different messages as inequal', () => {
68 const diagnostic1 = new vscode.Diagnostic(
69 range1,
70 'Hello, world!',
71 vscode.DiagnosticSeverity.Error,
72 );
73
74 const diagnostic2 = new vscode.Diagnostic(
75 range1,
76 'Goodbye!, world!',
77 vscode.DiagnosticSeverity.Error,
78 );
79
80 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
81 });
82
83 it('should treat diagnostics with different severities as inequal', () => {
84 const diagnostic1 = new vscode.Diagnostic(
85 range1,
86 'Hello, world!',
87 vscode.DiagnosticSeverity.Warning,
88 );
89
90 const diagnostic2 = new vscode.Diagnostic(
91 range1,
92 'Hello, world!',
93 vscode.DiagnosticSeverity.Error,
94 );
95
96 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
97 });
98});
diff --git a/editors/code/src/test/utils/index.ts b/editors/code/src/test/utils/index.ts
deleted file mode 100644
index 9927daaf6..000000000
--- a/editors/code/src/test/utils/index.ts
+++ /dev/null
@@ -1,49 +0,0 @@
1//
2// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
3//
4// This file is providing the test runner to use when running extension tests.
5// By default the test runner in use is Mocha based.
6//
7// You can provide your own test runner if you want to override it by exporting
8// a function run(testRoot: string, clb: (error:Error) => void) that the extension
9// host can call to run the tests. The test runner is expected to use console.log
10// to report the results back to the caller. When the tests are finished, return
11// a possible error to the callback or null if none.
12
13import * as glob from 'glob';
14import * as Mocha from 'mocha';
15import * as path from 'path';
16
17export function run(): Promise<void> {
18 // Create the mocha test
19 const mocha = new Mocha({
20 ui: 'bdd',
21 });
22 mocha.useColors(true);
23
24 const testsRoot = __dirname;
25
26 return new Promise((c, e) => {
27 glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
28 if (err) {
29 return e(err);
30 }
31
32 // Add files to the test suite
33 files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
34
35 try {
36 // Run the mocha test
37 mocha.run(failures => {
38 if (failures > 0) {
39 e(new Error(`${failures} tests failed.`));
40 } else {
41 c();
42 }
43 });
44 } catch (err) {
45 e(err);
46 }
47 });
48 });
49}
diff --git a/editors/code/src/utils/diagnostics/SuggestedFix.ts b/editors/code/src/utils/diagnostics/SuggestedFix.ts
deleted file mode 100644
index 6e660bb61..000000000
--- a/editors/code/src/utils/diagnostics/SuggestedFix.ts
+++ /dev/null
@@ -1,67 +0,0 @@
1import * as vscode from 'vscode';
2
3import { SuggestionApplicability } from './rust';
4
5/**
6 * Model object for text replacements suggested by the Rust compiler
7 *
8 * This is an intermediate form between the raw `rustc` JSON and a
9 * `vscode.CodeAction`. It's optimised for the use-cases of
10 * `SuggestedFixCollection`.
11 */
12export default class SuggestedFix {
13 public readonly title: string;
14 public readonly location: vscode.Location;
15 public readonly replacement: string;
16 public readonly applicability: SuggestionApplicability;
17
18 /**
19 * Diagnostics this suggested fix could resolve
20 */
21 public diagnostics: vscode.Diagnostic[];
22
23 constructor(
24 title: string,
25 location: vscode.Location,
26 replacement: string,
27 applicability: SuggestionApplicability = SuggestionApplicability.Unspecified,
28 ) {
29 this.title = title;
30 this.location = location;
31 this.replacement = replacement;
32 this.applicability = applicability;
33 this.diagnostics = [];
34 }
35
36 /**
37 * Determines if this suggested fix is equivalent to another instance
38 */
39 public isEqual(other: SuggestedFix): boolean {
40 return (
41 this.title === other.title &&
42 this.location.range.isEqual(other.location.range) &&
43 this.replacement === other.replacement &&
44 this.applicability === other.applicability
45 );
46 }
47
48 /**
49 * Converts this suggested fix to a VS Code Quick Fix code action
50 */
51 public toCodeAction(): vscode.CodeAction {
52 const codeAction = new vscode.CodeAction(
53 this.title,
54 vscode.CodeActionKind.QuickFix,
55 );
56
57 const edit = new vscode.WorkspaceEdit();
58 edit.replace(this.location.uri, this.location.range, this.replacement);
59 codeAction.edit = edit;
60
61 codeAction.isPreferred =
62 this.applicability === SuggestionApplicability.MachineApplicable;
63
64 codeAction.diagnostics = [...this.diagnostics];
65 return codeAction;
66 }
67}
diff --git a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts
deleted file mode 100644
index 57c9856cf..000000000
--- a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts
+++ /dev/null
@@ -1,77 +0,0 @@
1import * as vscode from 'vscode';
2import SuggestedFix from './SuggestedFix';
3
4/**
5 * Collection of suggested fixes across multiple documents
6 *
7 * This stores `SuggestedFix` model objects and returns them via the
8 * `vscode.CodeActionProvider` interface.
9 */
10export default class SuggestedFixCollection
11 implements vscode.CodeActionProvider {
12 public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix];
13
14 /**
15 * Map of document URI strings to suggested fixes
16 */
17 private suggestedFixes: Map<string, SuggestedFix[]>;
18
19 constructor() {
20 this.suggestedFixes = new Map();
21 }
22
23 /**
24 * Clears all suggested fixes across all documents
25 */
26 public clear(): void {
27 this.suggestedFixes = new Map();
28 }
29
30 /**
31 * Adds a suggested fix for the given diagnostic
32 *
33 * Some suggested fixes will appear in multiple diagnostics. For example,
34 * forgetting a `mut` on a variable will suggest changing the delaration on
35 * every mutable usage site. If the suggested fix has already been added
36 * this method will instead associate the existing fix with the new
37 * diagnostic.
38 */
39 public addSuggestedFixForDiagnostic(
40 suggestedFix: SuggestedFix,
41 diagnostic: vscode.Diagnostic,
42 ): void {
43 const fileUriString = suggestedFix.location.uri.toString();
44 const fileSuggestions = this.suggestedFixes.get(fileUriString) || [];
45
46 const existingSuggestion = fileSuggestions.find(s =>
47 s.isEqual(suggestedFix),
48 );
49
50 if (existingSuggestion) {
51 // The existing suggestion also applies to this new diagnostic
52 existingSuggestion.diagnostics.push(diagnostic);
53 } else {
54 // We haven't seen this suggestion before
55 suggestedFix.diagnostics.push(diagnostic);
56 fileSuggestions.push(suggestedFix);
57 }
58
59 this.suggestedFixes.set(fileUriString, fileSuggestions);
60 }
61
62 /**
63 * Filters suggested fixes by their document and range and converts them to
64 * code actions
65 */
66 public provideCodeActions(
67 document: vscode.TextDocument,
68 range: vscode.Range,
69 ): vscode.CodeAction[] {
70 const documentUriString = document.uri.toString();
71
72 const suggestedFixes = this.suggestedFixes.get(documentUriString);
73 return (suggestedFixes || [])
74 .filter(({ location }) => location.range.intersection(range))
75 .map(suggestedEdit => suggestedEdit.toCodeAction());
76 }
77}
diff --git a/editors/code/src/utils/diagnostics/rust.ts b/editors/code/src/utils/diagnostics/rust.ts
deleted file mode 100644
index 1f0c0d3e4..000000000
--- a/editors/code/src/utils/diagnostics/rust.ts
+++ /dev/null
@@ -1,299 +0,0 @@
1import * as path from 'path';
2import * as vscode from 'vscode';
3
4import SuggestedFix from './SuggestedFix';
5
6export enum SuggestionApplicability {
7 MachineApplicable = 'MachineApplicable',
8 HasPlaceholders = 'HasPlaceholders',
9 MaybeIncorrect = 'MaybeIncorrect',
10 Unspecified = 'Unspecified',
11}
12
13export interface RustDiagnosticSpanMacroExpansion {
14 span: RustDiagnosticSpan;
15 macro_decl_name: string;
16 def_site_span?: RustDiagnosticSpan;
17}
18
19// Reference:
20// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
21export interface RustDiagnosticSpan {
22 line_start: number;
23 line_end: number;
24 column_start: number;
25 column_end: number;
26 is_primary: boolean;
27 file_name: string;
28 label?: string;
29 expansion?: RustDiagnosticSpanMacroExpansion;
30 suggested_replacement?: string;
31 suggestion_applicability?: SuggestionApplicability;
32}
33
34export interface RustDiagnostic {
35 spans: RustDiagnosticSpan[];
36 rendered: string;
37 message: string;
38 level: string;
39 code?: {
40 code: string;
41 };
42 children: RustDiagnostic[];
43}
44
45export interface MappedRustDiagnostic {
46 location: vscode.Location;
47 diagnostic: vscode.Diagnostic;
48 suggestedFixes: SuggestedFix[];
49}
50
51interface MappedRustChildDiagnostic {
52 related?: vscode.DiagnosticRelatedInformation;
53 suggestedFix?: SuggestedFix;
54 messageLine?: string;
55}
56
57/**
58 * Converts a Rust level string to a VsCode severity
59 */
60function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity {
61 if (s === 'error') {
62 return vscode.DiagnosticSeverity.Error;
63 }
64 if (s.startsWith('warn')) {
65 return vscode.DiagnosticSeverity.Warning;
66 }
67 return vscode.DiagnosticSeverity.Information;
68}
69
70/**
71 * Check whether a file name is from macro invocation
72 */
73function isFromMacro(fileName: string): boolean {
74 return fileName.startsWith('<') && fileName.endsWith('>');
75}
76
77/**
78 * Converts a Rust macro span to a VsCode location recursively
79 */
80function mapMacroSpanToLocation(
81 spanMacro: RustDiagnosticSpanMacroExpansion,
82): vscode.Location | undefined {
83 if (!isFromMacro(spanMacro.span.file_name)) {
84 return mapSpanToLocation(spanMacro.span);
85 }
86
87 if (spanMacro.span.expansion) {
88 return mapMacroSpanToLocation(spanMacro.span.expansion);
89 }
90
91 return;
92}
93
94/**
95 * Converts a Rust span to a VsCode location
96 */
97function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location {
98 if (isFromMacro(span.file_name) && span.expansion) {
99 const macroLoc = mapMacroSpanToLocation(span.expansion);
100 if (macroLoc) {
101 return macroLoc;
102 }
103 }
104
105 const fileName = path.join(vscode.workspace.rootPath || '', span.file_name);
106 const fileUri = vscode.Uri.file(fileName);
107
108 const range = new vscode.Range(
109 new vscode.Position(span.line_start - 1, span.column_start - 1),
110 new vscode.Position(span.line_end - 1, span.column_end - 1),
111 );
112
113 return new vscode.Location(fileUri, range);
114}
115
116/**
117 * Converts a secondary Rust span to a VsCode related information
118 *
119 * If the span is unlabelled this will return `undefined`.
120 */
121function mapSecondarySpanToRelated(
122 span: RustDiagnosticSpan,
123): vscode.DiagnosticRelatedInformation | undefined {
124 if (!span.label) {
125 // Nothing to label this with
126 return;
127 }
128
129 const location = mapSpanToLocation(span);
130 return new vscode.DiagnosticRelatedInformation(location, span.label);
131}
132
133/**
134 * Determines if diagnostic is related to unused code
135 */
136function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean {
137 if (!rd.code) {
138 return false;
139 }
140
141 return [
142 'dead_code',
143 'unknown_lints',
144 'unreachable_code',
145 'unused_attributes',
146 'unused_imports',
147 'unused_macros',
148 'unused_variables',
149 ].includes(rd.code.code);
150}
151
152/**
153 * Determines if diagnostic is related to deprecated code
154 */
155function isDeprecated(rd: RustDiagnostic): boolean {
156 if (!rd.code) {
157 return false;
158 }
159
160 return ['deprecated'].includes(rd.code.code);
161}
162
163/**
164 * Converts a Rust child diagnostic to a VsCode related information
165 *
166 * This can have three outcomes:
167 *
168 * 1. If this is no primary span this will return a `noteLine`
169 * 2. If there is a primary span with a suggested replacement it will return a
170 * `codeAction`.
171 * 3. If there is a primary span without a suggested replacement it will return
172 * a `related`.
173 */
174function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
175 const span = rd.spans.find(s => s.is_primary);
176
177 if (!span) {
178 // `rustc` uses these spanless children as a way to print multi-line
179 // messages
180 return { messageLine: rd.message };
181 }
182
183 // If we have a primary span use its location, otherwise use the parent
184 const location = mapSpanToLocation(span);
185
186 // We need to distinguish `null` from an empty string
187 if (span && typeof span.suggested_replacement === 'string') {
188 // Include our replacement in the title unless it's empty
189 const title = span.suggested_replacement
190 ? `${rd.message}: \`${span.suggested_replacement}\``
191 : rd.message;
192
193 return {
194 suggestedFix: new SuggestedFix(
195 title,
196 location,
197 span.suggested_replacement,
198 span.suggestion_applicability,
199 ),
200 };
201 } else {
202 const related = new vscode.DiagnosticRelatedInformation(
203 location,
204 rd.message,
205 );
206
207 return { related };
208 }
209}
210
211/**
212 * Converts a Rust root diagnostic to VsCode form
213 *
214 * This flattens the Rust diagnostic by:
215 *
216 * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
217 * 2. Adding any labelled secondary spans to `relatedInformation`
218 * 3. Categorising child diagnostics as either `SuggestedFix`es,
219 * `relatedInformation` or additional message lines.
220 *
221 * If the diagnostic has no primary span this will return `undefined`
222 */
223export function mapRustDiagnosticToVsCode(
224 rd: RustDiagnostic,
225): MappedRustDiagnostic | undefined {
226 const primarySpan = rd.spans.find(s => s.is_primary);
227 if (!primarySpan) {
228 return;
229 }
230
231 const location = mapSpanToLocation(primarySpan);
232 const secondarySpans = rd.spans.filter(s => !s.is_primary);
233
234 const severity = mapLevelToSeverity(rd.level);
235 let primarySpanLabel = primarySpan.label;
236
237 const vd = new vscode.Diagnostic(location.range, rd.message, severity);
238
239 let source = 'rustc';
240 let code = rd.code && rd.code.code;
241 if (code) {
242 // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
243 const scopedCode = code.split('::');
244 if (scopedCode.length === 2) {
245 [source, code] = scopedCode;
246 }
247 }
248
249 vd.source = source;
250 vd.code = code;
251 vd.relatedInformation = [];
252 vd.tags = [];
253
254 for (const secondarySpan of secondarySpans) {
255 const related = mapSecondarySpanToRelated(secondarySpan);
256 if (related) {
257 vd.relatedInformation.push(related);
258 }
259 }
260
261 const suggestedFixes = [];
262 for (const child of rd.children) {
263 const { related, suggestedFix, messageLine } = mapRustChildDiagnostic(
264 child,
265 );
266
267 if (related) {
268 vd.relatedInformation.push(related);
269 }
270 if (suggestedFix) {
271 suggestedFixes.push(suggestedFix);
272 }
273 if (messageLine) {
274 vd.message += `\n${messageLine}`;
275
276 // These secondary messages usually duplicate the content of the
277 // primary span label.
278 primarySpanLabel = undefined;
279 }
280 }
281
282 if (primarySpanLabel) {
283 vd.message += `\n${primarySpanLabel}`;
284 }
285
286 if (isUnusedOrUnnecessary(rd)) {
287 vd.tags.push(vscode.DiagnosticTag.Unnecessary);
288 }
289
290 if (isDeprecated(rd)) {
291 vd.tags.push(vscode.DiagnosticTag.Deprecated);
292 }
293
294 return {
295 location,
296 diagnostic: vd,
297 suggestedFixes,
298 };
299}
diff --git a/editors/code/src/utils/diagnostics/vscode.ts b/editors/code/src/utils/diagnostics/vscode.ts
deleted file mode 100644
index f4a5450e2..000000000
--- a/editors/code/src/utils/diagnostics/vscode.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import * as vscode from 'vscode';
2
3/** Compares two `vscode.Diagnostic`s for equality */
4export function areDiagnosticsEqual(
5 left: vscode.Diagnostic,
6 right: vscode.Diagnostic,
7): boolean {
8 return (
9 left.source === right.source &&
10 left.severity === right.severity &&
11 left.range.isEqual(right.range) &&
12 left.message === right.message
13 );
14}
diff --git a/editors/code/src/utils/processes.ts b/editors/code/src/utils/processes.ts
deleted file mode 100644
index a1d6b7eaf..000000000
--- a/editors/code/src/utils/processes.ts
+++ /dev/null
@@ -1,51 +0,0 @@
1'use strict';
2
3import * as cp from 'child_process';
4import ChildProcess = cp.ChildProcess;
5
6import { join } from 'path';
7
8const isWindows = process.platform === 'win32';
9const isMacintosh = process.platform === 'darwin';
10const isLinux = process.platform === 'linux';
11
12// this is very complex, but is basically copy-pased from VSCode implementation here:
13// https://github.com/Microsoft/vscode-languageserver-node/blob/dbfd37e35953ad0ee14c4eeced8cfbc41697b47e/client/src/utils/processes.ts#L15
14
15// And see discussion at
16// https://github.com/rust-analyzer/rust-analyzer/pull/1079#issuecomment-478908109
17
18export function terminate(process: ChildProcess, cwd?: string): boolean {
19 if (isWindows) {
20 try {
21 // This we run in Atom execFileSync is available.
22 // Ignore stderr since this is otherwise piped to parent.stderr
23 // which might be already closed.
24 const options: any = {
25 stdio: ['pipe', 'pipe', 'ignore'],
26 };
27 if (cwd) {
28 options.cwd = cwd;
29 }
30 cp.execFileSync(
31 'taskkill',
32 ['/T', '/F', '/PID', process.pid.toString()],
33 options,
34 );
35 return true;
36 } catch (err) {
37 return false;
38 }
39 } else if (isLinux || isMacintosh) {
40 try {
41 const cmd = join(__dirname, 'terminateProcess.sh');
42 const result = cp.spawnSync(cmd, [process.pid.toString()]);
43 return result.error ? false : true;
44 } catch (err) {
45 return false;
46 }
47 } else {
48 process.kill('SIGKILL');
49 return true;
50 }
51}
diff --git a/editors/code/src/utils/terminateProcess.sh b/editors/code/src/utils/terminateProcess.sh
deleted file mode 100644
index 2ec9e1c2e..000000000
--- a/editors/code/src/utils/terminateProcess.sh
+++ /dev/null
@@ -1,12 +0,0 @@
1#!/bin/bash
2
3terminateTree() {
4 for cpid in $(pgrep -P $1); do
5 terminateTree $cpid
6 done
7 kill -9 $1 > /dev/null 2>&1
8}
9
10for pid in $*; do
11 terminateTree $pid
12done \ No newline at end of file