aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/utils
diff options
context:
space:
mode:
authorRyan Cumming <[email protected]>2019-06-27 12:30:23 +0100
committerRyan Cumming <[email protected]>2019-06-29 08:39:36 +0100
commitabc0784e57610a0cceca63301489918015418df6 (patch)
tree05aec9fef88f31cee82e3507903a1dbcd6b4d30d /editors/code/src/utils
parent0e1912de528b5092c10eedaf94c43c67d5f86f1a (diff)
Fix `cargo watch` code action filtering
There are two issues with the implementation of `provideCodeActions` introduced in #1439: 1. We're returning the code action based on the file its diagnostic is in; not the file the suggested fix is in. I'm not sure how often fixes are suggested cross-file but it's something we should handle. 2. We're not filtering code actions based on the passed range. The means if there is any suggestion in a file we'll show an action for every line of the file. I naively thought that VS Code would filter for us but that was wrong. Unfortunately the VS Code `CodeAction` object is very complex - it can handle edits across multiple files, run commands, etc. This makes it complex to check them for equality or see if any of their edits intersects with a specified range. To make it easier to work with suggestions this introduces a `SuggestedFix` model object and a `SuggestFixCollection` code action provider. This is a layer between the raw Rust JSON and VS Code's `CodeAction`s. I was reluctant to introduce another layer of abstraction here but my attempt to work directly with VS Code's model objects was worse.
Diffstat (limited to 'editors/code/src/utils')
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFix.ts67
-rw-r--r--editors/code/src/utils/diagnostics/SuggestedFixCollection.ts74
-rw-r--r--editors/code/src/utils/diagnostics/rust.ts (renamed from editors/code/src/utils/rust_diagnostics.ts)55
-rw-r--r--editors/code/src/utils/diagnostics/vscode.ts14
-rw-r--r--editors/code/src/utils/vscode_diagnostics.ts73
5 files changed, 182 insertions, 101 deletions
diff --git a/editors/code/src/utils/diagnostics/SuggestedFix.ts b/editors/code/src/utils/diagnostics/SuggestedFix.ts
new file mode 100644
index 000000000..b1be2a225
--- /dev/null
+++ b/editors/code/src/utils/diagnostics/SuggestedFix.ts
@@ -0,0 +1,67 @@
1import * as vscode from 'vscode';
2
3import { SuggestionApplicability } from './rust';
4
5/**
6 * Model object for text replacements suggested by the Rust compiler
7 *
8 * This is an intermediate form between the raw `rustc` JSON and a
9 * `vscode.CodeAction`. It's optimised for the use-cases of
10 * `SuggestedFixCollection`.
11 */
12export default class SuggestedFix {
13 public readonly title: string;
14 public readonly location: vscode.Location;
15 public readonly replacement: string;
16 public readonly applicability: SuggestionApplicability;
17
18 /**
19 * Diagnostics this suggested fix could resolve
20 */
21 public diagnostics: vscode.Diagnostic[];
22
23 constructor(
24 title: string,
25 location: vscode.Location,
26 replacement: string,
27 applicability: SuggestionApplicability = SuggestionApplicability.Unspecified
28 ) {
29 this.title = title;
30 this.location = location;
31 this.replacement = replacement;
32 this.applicability = applicability;
33 this.diagnostics = [];
34 }
35
36 /**
37 * Determines if this suggested fix is equivalent to another instance
38 */
39 public isEqual(other: SuggestedFix): boolean {
40 return (
41 this.title === other.title &&
42 this.location.range.isEqual(other.location.range) &&
43 this.replacement === other.replacement &&
44 this.applicability === other.applicability
45 );
46 }
47
48 /**
49 * Converts this suggested fix to a VS Code Quick Fix code action
50 */
51 public toCodeAction(): vscode.CodeAction {
52 const codeAction = new vscode.CodeAction(
53 this.title,
54 vscode.CodeActionKind.QuickFix
55 );
56
57 const edit = new vscode.WorkspaceEdit();
58 edit.replace(this.location.uri, this.location.range, this.replacement);
59 codeAction.edit = edit;
60
61 codeAction.isPreferred =
62 this.applicability === SuggestionApplicability.MachineApplicable;
63
64 codeAction.diagnostics = [...this.diagnostics];
65 return codeAction;
66 }
67}
diff --git a/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts
new file mode 100644
index 000000000..3b0bf7468
--- /dev/null
+++ b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts
@@ -0,0 +1,74 @@
1import * as vscode from 'vscode';
2import SuggestedFix from './SuggestedFix';
3
4/**
5 * Collection of suggested fixes across multiple documents
6 *
7 * This stores `SuggestedFix` model objects and returns them via the
8 * `vscode.CodeActionProvider` interface.
9 */
10export default class SuggestedFixCollection
11 implements vscode.CodeActionProvider {
12 public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix];
13
14 private suggestedFixes: Map<string, SuggestedFix[]>;
15
16 constructor() {
17 this.suggestedFixes = new Map();
18 }
19
20 /**
21 * Clears all suggested fixes across all documents
22 */
23 public clear(): void {
24 this.suggestedFixes = new Map();
25 }
26
27 /**
28 * Adds a suggested fix for the given diagnostic
29 *
30 * Some suggested fixes will appear in multiple diagnostics. For example,
31 * forgetting a `mut` on a variable will suggest changing the delaration on
32 * every mutable usage site. If the suggested fix has already been added
33 * this method will instead associate the existing fix with the new
34 * diagnostic.
35 */
36 public addSuggestedFixForDiagnostic(
37 suggestedFix: SuggestedFix,
38 diagnostic: vscode.Diagnostic
39 ): void {
40 const fileUriString = suggestedFix.location.uri.toString();
41 const fileSuggestions = this.suggestedFixes.get(fileUriString) || [];
42
43 const existingSuggestion = fileSuggestions.find(s =>
44 s.isEqual(suggestedFix)
45 );
46
47 if (existingSuggestion) {
48 // The existing suggestion also applies to this new diagnostic
49 existingSuggestion.diagnostics.push(diagnostic);
50 } else {
51 // We haven't seen this suggestion before
52 suggestedFix.diagnostics.push(diagnostic);
53 fileSuggestions.push(suggestedFix);
54 }
55
56 this.suggestedFixes.set(fileUriString, fileSuggestions);
57 }
58
59 /**
60 * Filters suggested fixes by their document and range and converts them to
61 * code actions
62 */
63 public provideCodeActions(
64 document: vscode.TextDocument,
65 range: vscode.Range
66 ): vscode.CodeAction[] {
67 const documentUriString = document.uri.toString();
68
69 const suggestedFixes = this.suggestedFixes.get(documentUriString);
70 return (suggestedFixes || [])
71 .filter(({ location }) => location.range.intersection(range))
72 .map(suggestedEdit => suggestedEdit.toCodeAction());
73 }
74}
diff --git a/editors/code/src/utils/rust_diagnostics.ts b/editors/code/src/utils/diagnostics/rust.ts
index 3c524cb37..d16576eb1 100644
--- a/editors/code/src/utils/rust_diagnostics.ts
+++ b/editors/code/src/utils/diagnostics/rust.ts
@@ -1,6 +1,15 @@
1import * as path from 'path'; 1import * as path from 'path';
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3 3
4import SuggestedFix from './SuggestedFix';
5
6export enum SuggestionApplicability {
7 MachineApplicable = 'MachineApplicable',
8 HasPlaceholders = 'HasPlaceholders',
9 MaybeIncorrect = 'MaybeIncorrect',
10 Unspecified = 'Unspecified'
11}
12
4// Reference: 13// Reference:
5// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs 14// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
6export interface RustDiagnosticSpan { 15export interface RustDiagnosticSpan {
@@ -12,11 +21,7 @@ export interface RustDiagnosticSpan {
12 file_name: string; 21 file_name: string;
13 label?: string; 22 label?: string;
14 suggested_replacement?: string; 23 suggested_replacement?: string;
15 suggestion_applicability?: 24 suggestion_applicability?: SuggestionApplicability;
16 | 'MachineApplicable'
17 | 'HasPlaceholders'
18 | 'MaybeIncorrect'
19 | 'Unspecified';
20} 25}
21 26
22export interface RustDiagnostic { 27export interface RustDiagnostic {
@@ -33,12 +38,12 @@ export interface RustDiagnostic {
33export interface MappedRustDiagnostic { 38export interface MappedRustDiagnostic {
34 location: vscode.Location; 39 location: vscode.Location;
35 diagnostic: vscode.Diagnostic; 40 diagnostic: vscode.Diagnostic;
36 codeActions: vscode.CodeAction[]; 41 suggestedFixes: SuggestedFix[];
37} 42}
38 43
39interface MappedRustChildDiagnostic { 44interface MappedRustChildDiagnostic {
40 related?: vscode.DiagnosticRelatedInformation; 45 related?: vscode.DiagnosticRelatedInformation;
41 codeAction?: vscode.CodeAction; 46 suggestedFix?: SuggestedFix;
42 messageLine?: string; 47 messageLine?: string;
43} 48}
44 49
@@ -130,24 +135,19 @@ function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
130 135
131 // We need to distinguish `null` from an empty string 136 // We need to distinguish `null` from an empty string
132 if (span && typeof span.suggested_replacement === 'string') { 137 if (span && typeof span.suggested_replacement === 'string') {
133 const edit = new vscode.WorkspaceEdit(); 138 // Include our replacement in the title unless it's empty
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 139 const title = span.suggested_replacement
138 ? `${rd.message}: \`${span.suggested_replacement}\`` 140 ? `${rd.message}: \`${span.suggested_replacement}\``
139 : rd.message; 141 : rd.message;
140 142
141 const codeAction = new vscode.CodeAction( 143 return {
142 title, 144 suggestedFix: new SuggestedFix(
143 vscode.CodeActionKind.QuickFix 145 title,
144 ); 146 location,
145 147 span.suggested_replacement,
146 codeAction.edit = edit; 148 span.suggestion_applicability
147 codeAction.isPreferred = 149 )
148 span.suggestion_applicability === 'MachineApplicable'; 150 };
149
150 return { codeAction };
151 } else { 151 } else {
152 const related = new vscode.DiagnosticRelatedInformation( 152 const related = new vscode.DiagnosticRelatedInformation(
153 location, 153 location,
@@ -165,7 +165,7 @@ function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
165 * 165 *
166 * 1. Creating a `vscode.Diagnostic` with the root message and primary span. 166 * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
167 * 2. Adding any labelled secondary spans to `relatedInformation` 167 * 2. Adding any labelled secondary spans to `relatedInformation`
168 * 3. Categorising child diagnostics as either Quick Fix actions, 168 * 3. Categorising child diagnostics as either `SuggestedFix`es,
169 * `relatedInformation` or additional message lines. 169 * `relatedInformation` or additional message lines.
170 * 170 *
171 * If the diagnostic has no primary span this will return `undefined` 171 * If the diagnostic has no primary span this will return `undefined`
@@ -173,8 +173,6 @@ function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
173export function mapRustDiagnosticToVsCode( 173export function mapRustDiagnosticToVsCode(
174 rd: RustDiagnostic 174 rd: RustDiagnostic
175): MappedRustDiagnostic | undefined { 175): MappedRustDiagnostic | undefined {
176 const codeActions = [];
177
178 const primarySpan = rd.spans.find(s => s.is_primary); 176 const primarySpan = rd.spans.find(s => s.is_primary);
179 if (!primarySpan) { 177 if (!primarySpan) {
180 return; 178 return;
@@ -208,16 +206,17 @@ export function mapRustDiagnosticToVsCode(
208 } 206 }
209 } 207 }
210 208
209 const suggestedFixes = [];
211 for (const child of rd.children) { 210 for (const child of rd.children) {
212 const { related, codeAction, messageLine } = mapRustChildDiagnostic( 211 const { related, suggestedFix, messageLine } = mapRustChildDiagnostic(
213 child 212 child
214 ); 213 );
215 214
216 if (related) { 215 if (related) {
217 vd.relatedInformation.push(related); 216 vd.relatedInformation.push(related);
218 } 217 }
219 if (codeAction) { 218 if (suggestedFix) {
220 codeActions.push(codeAction); 219 suggestedFixes.push(suggestedFix);
221 } 220 }
222 if (messageLine) { 221 if (messageLine) {
223 vd.message += `\n${messageLine}`; 222 vd.message += `\n${messageLine}`;
@@ -231,6 +230,6 @@ export function mapRustDiagnosticToVsCode(
231 return { 230 return {
232 location, 231 location,
233 diagnostic: vd, 232 diagnostic: vd,
234 codeActions 233 suggestedFixes
235 }; 234 };
236} 235}
diff --git a/editors/code/src/utils/diagnostics/vscode.ts b/editors/code/src/utils/diagnostics/vscode.ts
new file mode 100644
index 000000000..d8b85b720
--- /dev/null
+++ b/editors/code/src/utils/diagnostics/vscode.ts
@@ -0,0 +1,14 @@
1import * as vscode from 'vscode';
2
3/** Compares two `vscode.Diagnostic`s for equality */
4export function areDiagnosticsEqual(
5 left: vscode.Diagnostic,
6 right: vscode.Diagnostic
7): boolean {
8 return (
9 left.source === right.source &&
10 left.severity === right.severity &&
11 left.range.isEqual(right.range) &&
12 left.message === right.message
13 );
14}
diff --git a/editors/code/src/utils/vscode_diagnostics.ts b/editors/code/src/utils/vscode_diagnostics.ts
deleted file mode 100644
index 9d763c8d6..000000000
--- a/editors/code/src/utils/vscode_diagnostics.ts
+++ /dev/null
@@ -1,73 +0,0 @@
1import * as vscode from 'vscode';
2
3/** Compares two `vscode.Diagnostic`s for equality */
4export function areDiagnosticsEqual(
5 left: vscode.Diagnostic,
6 right: vscode.Diagnostic
7): boolean {
8 return (
9 left.source === right.source &&
10 left.severity === right.severity &&
11 left.range.isEqual(right.range) &&
12 left.message === right.message
13 );
14}
15
16/** Compares two `vscode.TextEdit`s for equality */
17function areTextEditsEqual(
18 left: vscode.TextEdit,
19 right: vscode.TextEdit
20): boolean {
21 if (!left.range.isEqual(right.range)) {
22 return false;
23 }
24
25 if (left.newText !== right.newText) {
26 return false;
27 }
28
29 return true;
30}
31
32/** Compares two `vscode.CodeAction`s for equality */
33export function areCodeActionsEqual(
34 left: vscode.CodeAction,
35 right: vscode.CodeAction
36): boolean {
37 if (
38 left.kind !== right.kind ||
39 left.title !== right.title ||
40 !left.edit ||
41 !right.edit
42 ) {
43 return false;
44 }
45
46 const leftEditEntries = left.edit.entries();
47 const rightEditEntries = right.edit.entries();
48
49 if (leftEditEntries.length !== rightEditEntries.length) {
50 return false;
51 }
52
53 for (let i = 0; i < leftEditEntries.length; i++) {
54 const [leftUri, leftEdits] = leftEditEntries[i];
55 const [rightUri, rightEdits] = rightEditEntries[i];
56
57 if (leftUri.toString() !== rightUri.toString()) {
58 return false;
59 }
60
61 if (leftEdits.length !== rightEdits.length) {
62 return false;
63 }
64
65 for (let j = 0; j < leftEdits.length; j++) {
66 if (!areTextEditsEqual(leftEdits[j], rightEdits[j])) {
67 return false;
68 }
69 }
70 }
71
72 return true;
73}