diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-06-29 10:50:56 +0100 |
---|---|---|
committer | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-06-29 10:50:56 +0100 |
commit | 8865db6768fd913b971df56fc0bcf7439fcb1e71 (patch) | |
tree | 3fff3954f7f5ca0c6045f9f7d96c1cd75bd896ee /editors/code/src/utils | |
parent | 64f71dd3fff7b902656a2c2465a1b5071b2b1903 (diff) | |
parent | 50c6ab709e38b63fb1e3ee5db40426ef131cbd71 (diff) |
Merge #1454
1454: Fix `cargo watch` code action filtering r=etaoins a=etaoins
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.
Co-authored-by: Ryan Cumming <[email protected]>
Diffstat (limited to 'editors/code/src/utils')
-rw-r--r-- | editors/code/src/utils/diagnostics/SuggestedFix.ts | 67 | ||||
-rw-r--r-- | editors/code/src/utils/diagnostics/SuggestedFixCollection.ts | 77 | ||||
-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.ts | 14 | ||||
-rw-r--r-- | editors/code/src/utils/vscode_diagnostics.ts | 73 |
5 files changed, 185 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 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | import { 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 | */ | ||
12 | export 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..132ce12f8 --- /dev/null +++ b/editors/code/src/utils/diagnostics/SuggestedFixCollection.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | import 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 | */ | ||
10 | export default class SuggestedFixCollection | ||
11 | implements vscode.CodeActionProvider { | ||
12 | public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix]; | ||
13 | |||
14 | /** | ||
15 | * Map of document URI strings to suggested fixes | ||
16 | */ | ||
17 | private suggestedFixes: Map<string, SuggestedFix[]>; | ||
18 | |||
19 | constructor() { | ||
20 | this.suggestedFixes = new Map(); | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Clears all suggested fixes across all documents | ||
25 | */ | ||
26 | public clear(): void { | ||
27 | this.suggestedFixes = new Map(); | ||
28 | } | ||
29 | |||
30 | /** | ||
31 | * Adds a suggested fix for the given diagnostic | ||
32 | * | ||
33 | * Some suggested fixes will appear in multiple diagnostics. For example, | ||
34 | * forgetting a `mut` on a variable will suggest changing the delaration on | ||
35 | * every mutable usage site. If the suggested fix has already been added | ||
36 | * this method will instead associate the existing fix with the new | ||
37 | * diagnostic. | ||
38 | */ | ||
39 | public addSuggestedFixForDiagnostic( | ||
40 | suggestedFix: SuggestedFix, | ||
41 | diagnostic: vscode.Diagnostic | ||
42 | ): void { | ||
43 | const fileUriString = suggestedFix.location.uri.toString(); | ||
44 | const fileSuggestions = this.suggestedFixes.get(fileUriString) || []; | ||
45 | |||
46 | const existingSuggestion = fileSuggestions.find(s => | ||
47 | s.isEqual(suggestedFix) | ||
48 | ); | ||
49 | |||
50 | if (existingSuggestion) { | ||
51 | // The existing suggestion also applies to this new diagnostic | ||
52 | existingSuggestion.diagnostics.push(diagnostic); | ||
53 | } else { | ||
54 | // We haven't seen this suggestion before | ||
55 | suggestedFix.diagnostics.push(diagnostic); | ||
56 | fileSuggestions.push(suggestedFix); | ||
57 | } | ||
58 | |||
59 | this.suggestedFixes.set(fileUriString, fileSuggestions); | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * Filters suggested fixes by their document and range and converts them to | ||
64 | * code actions | ||
65 | */ | ||
66 | public provideCodeActions( | ||
67 | document: vscode.TextDocument, | ||
68 | range: vscode.Range | ||
69 | ): vscode.CodeAction[] { | ||
70 | const documentUriString = document.uri.toString(); | ||
71 | |||
72 | const suggestedFixes = this.suggestedFixes.get(documentUriString); | ||
73 | return (suggestedFixes || []) | ||
74 | .filter(({ location }) => location.range.intersection(range)) | ||
75 | .map(suggestedEdit => suggestedEdit.toCodeAction()); | ||
76 | } | ||
77 | } | ||
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 @@ | |||
1 | import * as path from 'path'; | 1 | import * as path from 'path'; |
2 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | 3 | ||
4 | import SuggestedFix from './SuggestedFix'; | ||
5 | |||
6 | export 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 |
6 | export interface RustDiagnosticSpan { | 15 | export 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 | ||
22 | export interface RustDiagnostic { | 27 | export interface RustDiagnostic { |
@@ -33,12 +38,12 @@ export interface RustDiagnostic { | |||
33 | export interface MappedRustDiagnostic { | 38 | export 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 | ||
39 | interface MappedRustChildDiagnostic { | 44 | interface 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 { | |||
173 | export function mapRustDiagnosticToVsCode( | 173 | export 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 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | /** Compares two `vscode.Diagnostic`s for equality */ | ||
4 | export 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 @@ | |||
1 | import * as vscode from 'vscode'; | ||
2 | |||
3 | /** Compares two `vscode.Diagnostic`s for equality */ | ||
4 | export 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 */ | ||
17 | function 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 */ | ||
33 | export 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 | } | ||