Skip to content

Commit 4b0c37b

Browse files
bors[bot]etaoins
andcommitted
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]>
2 parents ba97a5f + 5c6ab11 commit 4b0c37b

File tree

2 files changed

+358
-54
lines changed

2 files changed

+358
-54
lines changed

editors/code/src/commands/cargo_watch.ts

Lines changed: 132 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import * as path from 'path';
44
import * as vscode from 'vscode';
55
import { Server } from '../server';
66
import { terminate } from '../utils/processes';
7+
import {
8+
mapRustDiagnosticToVsCode,
9+
RustDiagnostic
10+
} from '../utils/rust_diagnostics';
711
import { LineBuffer } from './line_buffer';
812
import { StatusDisplay } from './watch_status';
913

@@ -33,10 +37,17 @@ export function registerCargoWatchProvider(
3337
return provider;
3438
}
3539

36-
export class CargoWatchProvider implements vscode.Disposable {
40+
export class CargoWatchProvider
41+
implements vscode.Disposable, vscode.CodeActionProvider {
3742
private readonly diagnosticCollection: vscode.DiagnosticCollection;
3843
private readonly statusDisplay: StatusDisplay;
3944
private readonly outputChannel: vscode.OutputChannel;
45+
46+
private codeActions: {
47+
[fileUri: string]: vscode.CodeAction[];
48+
};
49+
private readonly codeActionDispose: vscode.Disposable;
50+
4051
private cargoProcess?: child_process.ChildProcess;
4152

4253
constructor() {
@@ -49,6 +60,16 @@ export class CargoWatchProvider implements vscode.Disposable {
4960
this.outputChannel = vscode.window.createOutputChannel(
5061
'Cargo Watch Trace'
5162
);
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+
);
5273
}
5374

5475
public start() {
@@ -127,6 +148,14 @@ export class CargoWatchProvider implements vscode.Disposable {
127148
this.diagnosticCollection.dispose();
128149
this.outputChannel.dispose();
129150
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 || [];
130159
}
131160

132161
private logInfo(line: string) {
@@ -147,41 +176,73 @@ export class CargoWatchProvider implements vscode.Disposable {
147176
private parseLine(line: string) {
148177
if (line.startsWith('[Running')) {
149178
this.diagnosticCollection.clear();
179+
this.codeActions = {};
150180
this.statusDisplay.show();
151181
}
152182

153183
if (line.startsWith('[Finished running')) {
154184
this.statusDisplay.hide();
155185
}
156186

157-
function getLevel(s: string): vscode.DiagnosticSeverity {
158-
if (s === 'error') {
159-
return vscode.DiagnosticSeverity.Error;
187+
function areDiagnosticsEqual(
188+
left: vscode.Diagnostic,
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;
160210
}
161-
if (s.startsWith('warn')) {
162-
return vscode.DiagnosticSeverity.Warning;
211+
212+
const leftEditEntries = left.edit.entries();
213+
const rightEditEntries = right.edit.entries();
214+
215+
if (leftEditEntries.length !== rightEditEntries.length) {
216+
return false;
163217
}
164-
return vscode.DiagnosticSeverity.Information;
165-
}
166218

167-
// Reference:
168-
// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
169-
interface RustDiagnosticSpan {
170-
line_start: number;
171-
line_end: number;
172-
column_start: number;
173-
column_end: number;
174-
is_primary: boolean;
175-
file_name: string;
176-
}
219+
for (let i = 0; i < leftEditEntries.length; i++) {
220+
const [leftUri, leftEdits] = leftEditEntries[i];
221+
const [rightUri, rightEdits] = rightEditEntries[i];
222+
223+
if (leftUri.toString() !== rightUri.toString()) {
224+
return false;
225+
}
177226

178-
interface RustDiagnostic {
179-
spans: RustDiagnosticSpan[];
180-
rendered: string;
181-
level: string;
182-
code?: {
183-
code: string;
184-
};
227+
if (leftEdits.length !== rightEdits.length) {
228+
return false;
229+
}
230+
231+
for (let j = 0; j < leftEdits.length; j++) {
232+
const leftEdit = leftEdits[j];
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;
185246
}
186247

187248
interface CargoArtifact {
@@ -215,41 +276,58 @@ export class CargoWatchProvider implements vscode.Disposable {
215276
} else if (data.reason === 'compiler-message') {
216277
const msg = data.message as RustDiagnostic;
217278

218-
const spans = msg.spans.filter(o => o.is_primary);
219-
220-
// We only handle primary span right now.
221-
if (spans.length > 0) {
222-
const o = spans[0];
279+
const mapResult = mapRustDiagnosticToVsCode(msg);
280+
if (!mapResult) {
281+
return;
282+
}
223283

224-
const rendered = msg.rendered;
225-
const level = getLevel(msg.level);
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-
);
284+
const { location, diagnostic, codeActions } = mapResult;
285+
const fileUri = location.uri;
230286

231-
const fileName = path.join(
232-
vscode.workspace.rootPath!,
233-
o.file_name
234-
);
235-
const diagnostic = new vscode.Diagnostic(
236-
range,
237-
rendered,
238-
level
239-
);
287+
const diagnostics: vscode.Diagnostic[] = [
288+
...(this.diagnosticCollection!.get(fileUri) || [])
289+
];
240290

241-
diagnostic.source = 'rustc';
242-
diagnostic.code = msg.code ? msg.code.code : undefined;
243-
diagnostic.relatedInformation = [];
291+
// If we're building multiple targets it's possible we've already seen this diagnostic
292+
const isDuplicate = diagnostics.some(d =>
293+
areDiagnosticsEqual(d, diagnostic)
294+
);
244295

245-
const fileUrl = vscode.Uri.file(fileName!);
296+
if (isDuplicate) {
297+
return;
298+
}
246299

247-
const diagnostics: vscode.Diagnostic[] = [
248-
...(this.diagnosticCollection!.get(fileUrl) || [])
249-
];
250-
diagnostics.push(diagnostic);
300+
diagnostics.push(diagnostic);
301+
this.diagnosticCollection!.set(fileUri, diagnostics);
302+
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+
}
251323

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+
);
253331
}
254332
}
255333
}

0 commit comments

Comments
 (0)