diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-06-25 13:37:07 +0100 |
---|---|---|
committer | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-06-25 13:37:07 +0100 |
commit | 4b0c37bd6e4cb3d47614ec6b42fb1deef9bc9324 (patch) | |
tree | 31ac3e97ade900dd51bc39c007316aa20a2e85fc | |
parent | ba97a5fbd2e14f38c633948f0d1551d0cf086ca3 (diff) | |
parent | 5c6ab1145319414e897a8eaca2bf1ad5558ccf24 (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.ts | 186 | ||||
-rw-r--r-- | editors/code/src/utils/rust_diagnostics.ts | 226 |
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'; | |||
4 | import * as vscode from 'vscode'; | 4 | import * as vscode from 'vscode'; |
5 | import { Server } from '../server'; | 5 | import { Server } from '../server'; |
6 | import { terminate } from '../utils/processes'; | 6 | import { terminate } from '../utils/processes'; |
7 | import { | ||
8 | mapRustDiagnosticToVsCode, | ||
9 | RustDiagnostic | ||
10 | } from '../utils/rust_diagnostics'; | ||
7 | import { LineBuffer } from './line_buffer'; | 11 | import { LineBuffer } from './line_buffer'; |
8 | import { StatusDisplay } from './watch_status'; | 12 | import { StatusDisplay } from './watch_status'; |
9 | 13 | ||
@@ -33,10 +37,17 @@ export function registerCargoWatchProvider( | |||
33 | return provider; | 37 | return provider; |
34 | } | 38 | } |
35 | 39 | ||
36 | export class CargoWatchProvider implements vscode.Disposable { | 40 | export 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 @@ | |||
1 | import * as path from 'path'; | ||
2 | import * as vscode from 'vscode'; | ||
3 | |||
4 | // Reference: | ||
5 | // https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs | ||
6 | export 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 | |||
22 | export 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 | |||
33 | export interface MappedRustDiagnostic { | ||
34 | location: vscode.Location; | ||
35 | diagnostic: vscode.Diagnostic; | ||
36 | codeActions: vscode.CodeAction[]; | ||
37 | } | ||
38 | |||
39 | interface 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 | */ | ||
48 | function 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 | */ | ||
61 | function 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 | */ | ||
78 | function 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 | */ | ||
93 | function 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 | */ | ||
119 | function 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 | */ | ||
173 | export 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 | } | ||