aboutsummaryrefslogtreecommitdiff
path: root/editors
diff options
context:
space:
mode:
authorRyan Cumming <[email protected]>2019-06-24 22:40:06 +0100
committerRyan Cumming <[email protected]>2019-06-25 12:16:04 +0100
commit6d6cb25cf46a2327d6cb2278385763abfa7a95a0 (patch)
tree334f256dd02766fd4103abc485ff62918a27e6f9 /editors
parent364ac9b9468e1930f39e0dddc454b2eb7d68f360 (diff)
Rich mapping of cargo watch output
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).
Diffstat (limited to 'editors')
-rw-r--r--editors/code/src/commands/cargo_watch.ts186
-rw-r--r--editors/code/src/utils/rust_diagnostics.ts220
2 files changed, 352 insertions, 54 deletions
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
index 13adf4c10..a662f7cc8 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 !== leftEditEntries.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..7d8cc0e0b
--- /dev/null
+++ b/editors/code/src/utils/rust_diagnostics.ts
@@ -0,0 +1,220 @@
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 const { code } = rd.code;
99 return code.startsWith('unused_') || code === 'dead_code';
100}
101
102/**
103 * Converts a Rust child diagnostic to a VsCode related information
104 *
105 * This can have three outcomes:
106 *
107 * 1. If this is no primary span this will return a `noteLine`
108 * 2. If there is a primary span with a suggested replacement it will return a
109 * `codeAction`.
110 * 3. If there is a primary span without a suggested replacement it will return
111 * a `related`.
112 */
113function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
114 const span = rd.spans.find(s => s.is_primary);
115
116 if (!span) {
117 // `rustc` uses these spanless children as a way to print multi-line
118 // messages
119 return { messageLine: rd.message };
120 }
121
122 // If we have a primary span use its location, otherwise use the parent
123 const location = mapSpanToLocation(span);
124
125 // We need to distinguish `null` from an empty string
126 if (span && typeof span.suggested_replacement === 'string') {
127 const edit = new vscode.WorkspaceEdit();
128 edit.replace(location.uri, location.range, span.suggested_replacement);
129
130 // Include our replacement in the label unless it's empty
131 const title = span.suggested_replacement
132 ? `${rd.message}: \`${span.suggested_replacement}\``
133 : rd.message;
134
135 const codeAction = new vscode.CodeAction(
136 title,
137 vscode.CodeActionKind.QuickFix
138 );
139
140 codeAction.edit = edit;
141 codeAction.isPreferred =
142 span.suggestion_applicability === 'MachineApplicable';
143
144 return { codeAction };
145 } else {
146 const related = new vscode.DiagnosticRelatedInformation(
147 location,
148 rd.message
149 );
150
151 return { related };
152 }
153}
154
155/**
156 * Converts a Rust root diagnostic to VsCode form
157 *
158 * This flattens the Rust diagnostic by:
159 *
160 * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
161 * 2. Adding any labelled secondary spans to `relatedInformation`
162 * 3. Categorising child diagnostics as either Quick Fix actions,
163 * `relatedInformation` or additional message lines.
164 *
165 * If the diagnostic has no primary span this will return `undefined`
166 */
167export function mapRustDiagnosticToVsCode(
168 rd: RustDiagnostic
169): MappedRustDiagnostic | undefined {
170 const codeActions = [];
171
172 const primarySpan = rd.spans.find(s => s.is_primary);
173 if (!primarySpan) {
174 return;
175 }
176
177 const location = mapSpanToLocation(primarySpan);
178 const secondarySpans = rd.spans.filter(s => !s.is_primary);
179
180 const severity = mapLevelToSeverity(rd.level);
181
182 const vd = new vscode.Diagnostic(location.range, rd.message, severity);
183
184 vd.source = 'rustc';
185 vd.code = rd.code ? rd.code.code : undefined;
186 vd.relatedInformation = [];
187
188 for (const secondarySpan of secondarySpans) {
189 const related = mapSecondarySpanToRelated(secondarySpan);
190 if (related) {
191 vd.relatedInformation.push(related);
192 }
193 }
194
195 for (const child of rd.children) {
196 const { related, codeAction, messageLine } = mapRustChildDiagnostic(
197 child
198 );
199
200 if (related) {
201 vd.relatedInformation.push(related);
202 }
203 if (codeAction) {
204 codeActions.push(codeAction);
205 }
206 if (messageLine) {
207 vd.message += `\n${messageLine}`;
208 }
209 }
210
211 if (isUnusedOrUnnecessary(rd)) {
212 vd.tags = [vscode.DiagnosticTag.Unnecessary];
213 }
214
215 return {
216 location,
217 diagnostic: vd,
218 codeActions
219 };
220}