aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2019-06-25 13:37:07 +0100
committerbors[bot] <26634292+bors[bot]@users.noreply.github.com>2019-06-25 13:37:07 +0100
commit4b0c37bd6e4cb3d47614ec6b42fb1deef9bc9324 (patch)
tree31ac3e97ade900dd51bc39c007316aa20a2e85fc
parentba97a5fbd2e14f38c633948f0d1551d0cf086ca3 (diff)
parent5c6ab1145319414e897a8eaca2bf1ad5558ccf24 (diff)
Merge #1439
1439: Rich mapping of cargo watch output r=matklad a=etaoins Currently we depend on the ASCII rendering string that `rustc` provides to populate Visual Studio Code's diagnostic. This has a number of shortcomings: 1. It's not a very good use of space in the error list 2. We can't jump to secondary spans (e.g. where a called function is defined) 3. We can't use Code Actions aka Quick Fix This moves all of the low-level parsing and mapping to a `rust_diagnostics.ts`. This uses some heuristics to map Rust diagnostics to VsCode: 1. As before, the Rust diagnostic message and primary span is used for the root diagnostic. However, we now just use the message instead of the rendered version. 2. Every secondary span is converted to "related information". This shows as child in the error list and can be jumped to. 3. Every child diagnostic is categorised in to three buckets: 1. If they have no span they're treated as another line of the root messages 2. If they have replacement text they're treated as a Code Action 3. If they have a span but no replacement text they're treated as related information (same as secondary spans). Co-authored-by: Ryan Cumming <[email protected]>
-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}