diff options
-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 | } | ||