aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editors/code/src/commands/cargo_watch.ts186
-rw-r--r--editors/code/src/utils/rust_diagnostics.ts226
2 files changed, 358 insertions, 54 deletions
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
index 13adf4c10..126a8b1b3 100644
--- a/editors/code/src/commands/cargo_watch.ts
+++ b/editors/code/src/commands/cargo_watch.ts
@@ -4,6 +4,10 @@ import * as path from 'path';
4import * as vscode from 'vscode'; 4import * as vscode from 'vscode';
5import { Server } from '../server'; 5import { Server } from '../server';
6import { terminate } from '../utils/processes'; 6import { terminate } from '../utils/processes';
7import {
8 mapRustDiagnosticToVsCode,
9 RustDiagnostic
10} from '../utils/rust_diagnostics';
7import { LineBuffer } from './line_buffer'; 11import { LineBuffer } from './line_buffer';
8import { StatusDisplay } from './watch_status'; 12import { StatusDisplay } from './watch_status';
9 13
@@ -33,10 +37,17 @@ export function registerCargoWatchProvider(
33 return provider; 37 return provider;
34} 38}
35 39
36export class CargoWatchProvider implements vscode.Disposable { 40export class CargoWatchProvider
41 implements vscode.Disposable, vscode.CodeActionProvider {
37 private readonly diagnosticCollection: vscode.DiagnosticCollection; 42 private readonly diagnosticCollection: vscode.DiagnosticCollection;
38 private readonly statusDisplay: StatusDisplay; 43 private readonly statusDisplay: StatusDisplay;
39 private readonly outputChannel: vscode.OutputChannel; 44 private readonly outputChannel: vscode.OutputChannel;
45
46 private codeActions: {
47 [fileUri: string]: vscode.CodeAction[];
48 };
49 private readonly codeActionDispose: vscode.Disposable;
50
40 private cargoProcess?: child_process.ChildProcess; 51 private cargoProcess?: child_process.ChildProcess;
41 52
42 constructor() { 53 constructor() {
@@ -49,6 +60,16 @@ export class CargoWatchProvider implements vscode.Disposable {
49 this.outputChannel = vscode.window.createOutputChannel( 60 this.outputChannel = vscode.window.createOutputChannel(
50 'Cargo Watch Trace' 61 'Cargo Watch Trace'
51 ); 62 );
63
64 // Register code actions for rustc's suggested fixes
65 this.codeActions = {};
66 this.codeActionDispose = vscode.languages.registerCodeActionsProvider(
67 [{ scheme: 'file', language: 'rust' }],
68 this,
69 {
70 providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
71 }
72 );
52 } 73 }
53 74
54 public start() { 75 public start() {
@@ -127,6 +148,14 @@ export class CargoWatchProvider implements vscode.Disposable {
127 this.diagnosticCollection.dispose(); 148 this.diagnosticCollection.dispose();
128 this.outputChannel.dispose(); 149 this.outputChannel.dispose();
129 this.statusDisplay.dispose(); 150 this.statusDisplay.dispose();
151 this.codeActionDispose.dispose();
152 }
153
154 public provideCodeActions(
155 document: vscode.TextDocument
156 ): vscode.ProviderResult<Array<vscode.Command | vscode.CodeAction>> {
157 const documentActions = this.codeActions[document.uri.toString()];
158 return documentActions || [];
130 } 159 }
131 160
132 private logInfo(line: string) { 161 private logInfo(line: string) {
@@ -147,6 +176,7 @@ export class CargoWatchProvider implements vscode.Disposable {
147 private parseLine(line: string) { 176 private parseLine(line: string) {
148 if (line.startsWith('[Running')) { 177 if (line.startsWith('[Running')) {
149 this.diagnosticCollection.clear(); 178 this.diagnosticCollection.clear();
179 this.codeActions = {};
150 this.statusDisplay.show(); 180 this.statusDisplay.show();
151 } 181 }
152 182
@@ -154,34 +184,65 @@ export class CargoWatchProvider implements vscode.Disposable {
154 this.statusDisplay.hide(); 184 this.statusDisplay.hide();
155 } 185 }
156 186
157 function getLevel(s: string): vscode.DiagnosticSeverity { 187 function areDiagnosticsEqual(
158 if (s === 'error') { 188 left: vscode.Diagnostic,
159 return vscode.DiagnosticSeverity.Error; 189 right: vscode.Diagnostic
190 ): boolean {
191 return (
192 left.source === right.source &&
193 left.severity === right.severity &&
194 left.range.isEqual(right.range) &&
195 left.message === right.message
196 );
197 }
198
199 function areCodeActionsEqual(
200 left: vscode.CodeAction,
201 right: vscode.CodeAction
202 ): boolean {
203 if (
204 left.kind !== right.kind ||
205 left.title !== right.title ||
206 !left.edit ||
207 !right.edit
208 ) {
209 return false;
160 } 210 }
161 if (s.startsWith('warn')) { 211
162 return vscode.DiagnosticSeverity.Warning; 212 const leftEditEntries = left.edit.entries();
213 const rightEditEntries = right.edit.entries();
214
215 if (leftEditEntries.length !== rightEditEntries.length) {
216 return false;
163 } 217 }
164 return vscode.DiagnosticSeverity.Information;
165 }
166 218
167 // Reference: 219 for (let i = 0; i < leftEditEntries.length; i++) {
168 // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs 220 const [leftUri, leftEdits] = leftEditEntries[i];
169 interface RustDiagnosticSpan { 221 const [rightUri, rightEdits] = rightEditEntries[i];
170 line_start: number; 222
171 line_end: number; 223 if (leftUri.toString() !== rightUri.toString()) {
172 column_start: number; 224 return false;
173 column_end: number; 225 }
174 is_primary: boolean;
175 file_name: string;
176 }
177 226
178 interface RustDiagnostic { 227 if (leftEdits.length !== rightEdits.length) {
179 spans: RustDiagnosticSpan[]; 228 return false;
180 rendered: string; 229 }
181 level: string; 230
182 code?: { 231 for (let j = 0; j < leftEdits.length; j++) {
183 code: string; 232 const leftEdit = leftEdits[j];
184 }; 233 const rightEdit = rightEdits[j];
234
235 if (!leftEdit.range.isEqual(rightEdit.range)) {
236 return false;
237 }
238
239 if (leftEdit.newText !== rightEdit.newText) {
240 return false;
241 }
242 }
243 }
244
245 return true;
185 } 246 }
186 247
187 interface CargoArtifact { 248 interface CargoArtifact {
@@ -215,41 +276,58 @@ export class CargoWatchProvider implements vscode.Disposable {
215 } else if (data.reason === 'compiler-message') { 276 } else if (data.reason === 'compiler-message') {
216 const msg = data.message as RustDiagnostic; 277 const msg = data.message as RustDiagnostic;
217 278
218 const spans = msg.spans.filter(o => o.is_primary); 279 const mapResult = mapRustDiagnosticToVsCode(msg);
219 280 if (!mapResult) {
220 // We only handle primary span right now. 281 return;
221 if (spans.length > 0) { 282 }
222 const o = spans[0];
223 283
224 const rendered = msg.rendered; 284 const { location, diagnostic, codeActions } = mapResult;
225 const level = getLevel(msg.level); 285 const fileUri = location.uri;
226 const range = new vscode.Range(
227 new vscode.Position(o.line_start - 1, o.column_start - 1),
228 new vscode.Position(o.line_end - 1, o.column_end - 1)
229 );
230 286
231 const fileName = path.join( 287 const diagnostics: vscode.Diagnostic[] = [
232 vscode.workspace.rootPath!, 288 ...(this.diagnosticCollection!.get(fileUri) || [])
233 o.file_name 289 ];
234 );
235 const diagnostic = new vscode.Diagnostic(
236 range,
237 rendered,
238 level
239 );
240 290
241 diagnostic.source = 'rustc'; 291 // If we're building multiple targets it's possible we've already seen this diagnostic
242 diagnostic.code = msg.code ? msg.code.code : undefined; 292 const isDuplicate = diagnostics.some(d =>
243 diagnostic.relatedInformation = []; 293 areDiagnosticsEqual(d, diagnostic)
294 );
244 295
245 const fileUrl = vscode.Uri.file(fileName!); 296 if (isDuplicate) {
297 return;
298 }
246 299
247 const diagnostics: vscode.Diagnostic[] = [ 300 diagnostics.push(diagnostic);
248 ...(this.diagnosticCollection!.get(fileUrl) || []) 301 this.diagnosticCollection!.set(fileUri, diagnostics);
249 ]; 302
250 diagnostics.push(diagnostic); 303 if (codeActions.length) {
304 const fileUriString = fileUri.toString();
305 const existingActions = this.codeActions[fileUriString] || [];
306
307 for (const newAction of codeActions) {
308 const existingAction = existingActions.find(existing =>
309 areCodeActionsEqual(existing, newAction)
310 );
311
312 if (existingAction) {
313 if (!existingAction.diagnostics) {
314 existingAction.diagnostics = [];
315 }
316 // This action also applies to this diagnostic
317 existingAction.diagnostics.push(diagnostic);
318 } else {
319 newAction.diagnostics = [diagnostic];
320 existingActions.push(newAction);
321 }
322 }
251 323
252 this.diagnosticCollection!.set(fileUrl, diagnostics); 324 // Have VsCode query us for the code actions
325 this.codeActions[fileUriString] = existingActions;
326 vscode.commands.executeCommand(
327 'vscode.executeCodeActionProvider',
328 fileUri,
329 diagnostic.range
330 );
253 } 331 }
254 } 332 }
255 } 333 }
diff --git a/editors/code/src/utils/rust_diagnostics.ts b/editors/code/src/utils/rust_diagnostics.ts
new file mode 100644
index 000000000..ed049c95e
--- /dev/null
+++ b/editors/code/src/utils/rust_diagnostics.ts
@@ -0,0 +1,226 @@
1import * as path from 'path';
2import * as vscode from 'vscode';
3
4// Reference:
5// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
6export interface RustDiagnosticSpan {
7 line_start: number;
8 line_end: number;
9 column_start: number;
10 column_end: number;
11 is_primary: boolean;
12 file_name: string;
13 label?: string;
14 suggested_replacement?: string;
15 suggestion_applicability?:
16 | 'MachineApplicable'
17 | 'HasPlaceholders'
18 | 'MaybeIncorrect'
19 | 'Unspecified';
20}
21
22export interface RustDiagnostic {
23 spans: RustDiagnosticSpan[];
24 rendered: string;
25 message: string;
26 level: string;
27 code?: {
28 code: string;
29 };
30 children: RustDiagnostic[];
31}
32
33export interface MappedRustDiagnostic {
34 location: vscode.Location;
35 diagnostic: vscode.Diagnostic;
36 codeActions: vscode.CodeAction[];
37}
38
39interface MappedRustChildDiagnostic {
40 related?: vscode.DiagnosticRelatedInformation;
41 codeAction?: vscode.CodeAction;
42 messageLine?: string;
43}
44
45/**
46 * Converts a Rust level string to a VsCode severity
47 */
48function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity {
49 if (s === 'error') {
50 return vscode.DiagnosticSeverity.Error;
51 }
52 if (s.startsWith('warn')) {
53 return vscode.DiagnosticSeverity.Warning;
54 }
55 return vscode.DiagnosticSeverity.Information;
56}
57
58/**
59 * Converts a Rust span to a VsCode location
60 */
61function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location {
62 const fileName = path.join(vscode.workspace.rootPath!, span.file_name);
63 const fileUri = vscode.Uri.file(fileName);
64
65 const range = new vscode.Range(
66 new vscode.Position(span.line_start - 1, span.column_start - 1),
67 new vscode.Position(span.line_end - 1, span.column_end - 1)
68 );
69
70 return new vscode.Location(fileUri, range);
71}
72
73/**
74 * Converts a secondary Rust span to a VsCode related information
75 *
76 * If the span is unlabelled this will return `undefined`.
77 */
78function mapSecondarySpanToRelated(
79 span: RustDiagnosticSpan
80): vscode.DiagnosticRelatedInformation | undefined {
81 if (!span.label) {
82 // Nothing to label this with
83 return;
84 }
85
86 const location = mapSpanToLocation(span);
87 return new vscode.DiagnosticRelatedInformation(location, span.label);
88}
89
90/**
91 * Determines if diagnostic is related to unused code
92 */
93function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean {
94 if (!rd.code) {
95 return false;
96 }
97
98 return [
99 'dead_code',
100 'unknown_lints',
101 'unused_attributes',
102 'unused_imports',
103 'unused_macros',
104 'unused_variables'
105 ].includes(rd.code.code);
106}
107
108/**
109 * Converts a Rust child diagnostic to a VsCode related information
110 *
111 * This can have three outcomes:
112 *
113 * 1. If this is no primary span this will return a `noteLine`
114 * 2. If there is a primary span with a suggested replacement it will return a
115 * `codeAction`.
116 * 3. If there is a primary span without a suggested replacement it will return
117 * a `related`.
118 */
119function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
120 const span = rd.spans.find(s => s.is_primary);
121
122 if (!span) {
123 // `rustc` uses these spanless children as a way to print multi-line
124 // messages
125 return { messageLine: rd.message };
126 }
127
128 // If we have a primary span use its location, otherwise use the parent
129 const location = mapSpanToLocation(span);
130
131 // We need to distinguish `null` from an empty string
132 if (span && typeof span.suggested_replacement === 'string') {
133 const edit = new vscode.WorkspaceEdit();
134 edit.replace(location.uri, location.range, span.suggested_replacement);
135
136 // Include our replacement in the label unless it's empty
137 const title = span.suggested_replacement
138 ? `${rd.message}: \`${span.suggested_replacement}\``
139 : rd.message;
140
141 const codeAction = new vscode.CodeAction(
142 title,
143 vscode.CodeActionKind.QuickFix
144 );
145
146 codeAction.edit = edit;
147 codeAction.isPreferred =
148 span.suggestion_applicability === 'MachineApplicable';
149
150 return { codeAction };
151 } else {
152 const related = new vscode.DiagnosticRelatedInformation(
153 location,
154 rd.message
155 );
156
157 return { related };
158 }
159}
160
161/**
162 * Converts a Rust root diagnostic to VsCode form
163 *
164 * This flattens the Rust diagnostic by:
165 *
166 * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
167 * 2. Adding any labelled secondary spans to `relatedInformation`
168 * 3. Categorising child diagnostics as either Quick Fix actions,
169 * `relatedInformation` or additional message lines.
170 *
171 * If the diagnostic has no primary span this will return `undefined`
172 */
173export function mapRustDiagnosticToVsCode(
174 rd: RustDiagnostic
175): MappedRustDiagnostic | undefined {
176 const codeActions = [];
177
178 const primarySpan = rd.spans.find(s => s.is_primary);
179 if (!primarySpan) {
180 return;
181 }
182
183 const location = mapSpanToLocation(primarySpan);
184 const secondarySpans = rd.spans.filter(s => !s.is_primary);
185
186 const severity = mapLevelToSeverity(rd.level);
187
188 const vd = new vscode.Diagnostic(location.range, rd.message, severity);
189
190 vd.source = 'rustc';
191 vd.code = rd.code ? rd.code.code : undefined;
192 vd.relatedInformation = [];
193
194 for (const secondarySpan of secondarySpans) {
195 const related = mapSecondarySpanToRelated(secondarySpan);
196 if (related) {
197 vd.relatedInformation.push(related);
198 }
199 }
200
201 for (const child of rd.children) {
202 const { related, codeAction, messageLine } = mapRustChildDiagnostic(
203 child
204 );
205
206 if (related) {
207 vd.relatedInformation.push(related);
208 }
209 if (codeAction) {
210 codeActions.push(codeAction);
211 }
212 if (messageLine) {
213 vd.message += `\n${messageLine}`;
214 }
215 }
216
217 if (isUnusedOrUnnecessary(rd)) {
218 vd.tags = [vscode.DiagnosticTag.Unnecessary];
219 }
220
221 return {
222 location,
223 diagnostic: vd,
224 codeActions
225 };
226}