import { Server } from '../server';
import { terminate } from '../utils/processes';
+import { LineBuffer } from './line_buffer';
+import { StatusDisplay } from './watch_status';
+
import {
mapRustDiagnosticToVsCode,
RustDiagnostic
-} from '../utils/rust_diagnostics';
-import {
- areCodeActionsEqual,
- areDiagnosticsEqual
-} from '../utils/vscode_diagnostics';
-import { LineBuffer } from './line_buffer';
-import { StatusDisplay } from './watch_status';
+} from '../utils/diagnostics/rust';
+import SuggestedFixCollection from '../utils/diagnostics/SuggestedFixCollection';
+import { areDiagnosticsEqual } from '../utils/diagnostics/vscode';
export function registerCargoWatchProvider(
subscriptions: vscode.Disposable[]
return provider;
}
-export class CargoWatchProvider
- implements vscode.Disposable, vscode.CodeActionProvider {
+export class CargoWatchProvider implements vscode.Disposable {
private readonly diagnosticCollection: vscode.DiagnosticCollection;
private readonly statusDisplay: StatusDisplay;
private readonly outputChannel: vscode.OutputChannel;
- private codeActions: {
- [fileUri: string]: vscode.CodeAction[];
- };
- private readonly codeActionDispose: vscode.Disposable;
+ private suggestedFixCollection: SuggestedFixCollection;
+ private codeActionDispose: vscode.Disposable;
private cargoProcess?: child_process.ChildProcess;
'Cargo Watch Trace'
);
- // Register code actions for rustc's suggested fixes
- this.codeActions = {};
+ // Track `rustc`'s suggested fixes so we can convert them to code actions
+ this.suggestedFixCollection = new SuggestedFixCollection();
this.codeActionDispose = vscode.languages.registerCodeActionsProvider(
[{ scheme: 'file', language: 'rust' }],
- this,
+ this.suggestedFixCollection,
{
- providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
+ providedCodeActionKinds:
+ SuggestedFixCollection.PROVIDED_CODE_ACTION_KINDS
}
);
}
this.codeActionDispose.dispose();
}
- public provideCodeActions(
- document: vscode.TextDocument
- ): vscode.ProviderResult<Array<vscode.Command | vscode.CodeAction>> {
- const documentActions = this.codeActions[document.uri.toString()];
- return documentActions || [];
- }
-
private logInfo(line: string) {
if (Server.config.cargoWatchOptions.trace === 'verbose') {
this.outputChannel.append(line);
private parseLine(line: string) {
if (line.startsWith('[Running')) {
this.diagnosticCollection.clear();
- this.codeActions = {};
+ this.suggestedFixCollection.clear();
this.statusDisplay.show();
}
return;
}
- const { location, diagnostic, codeActions } = mapResult;
+ const { location, diagnostic, suggestedFixes } = mapResult;
const fileUri = location.uri;
const diagnostics: vscode.Diagnostic[] = [
const isDuplicate = diagnostics.some(d =>
areDiagnosticsEqual(d, diagnostic)
);
-
if (isDuplicate) {
return;
}
diagnostics.push(diagnostic);
this.diagnosticCollection!.set(fileUri, diagnostics);
- if (codeActions.length) {
- const fileUriString = fileUri.toString();
- const existingActions = this.codeActions[fileUriString] || [];
-
- for (const newAction of codeActions) {
- const existingAction = existingActions.find(existing =>
- areCodeActionsEqual(existing, newAction)
+ if (suggestedFixes.length) {
+ for (const suggestedFix of suggestedFixes) {
+ this.suggestedFixCollection.addSuggestedFixForDiagnostic(
+ suggestedFix,
+ diagnostic
);
-
- if (existingAction) {
- if (!existingAction.diagnostics) {
- existingAction.diagnostics = [];
- }
- // This action also applies to this diagnostic
- existingAction.diagnostics.push(diagnostic);
- } else {
- newAction.diagnostics = [diagnostic];
- existingActions.push(newAction);
- }
}
// Have VsCode query us for the code actions
- this.codeActions[fileUriString] = existingActions;
vscode.commands.executeCommand(
'vscode.executeCodeActionProvider',
fileUri,
+++ /dev/null
-import * as assert from 'assert';
-import * as fs from 'fs';
-import * as vscode from 'vscode';
-
-import {
- MappedRustDiagnostic,
- mapRustDiagnosticToVsCode,
- RustDiagnostic
-} from '../utils/rust_diagnostics';
-
-function loadDiagnosticFixture(name: string): RustDiagnostic {
- const jsonText = fs
- .readFileSync(
- // We're actually in our JavaScript output directory, climb out
- `${__dirname}/../../src/test/fixtures/rust-diagnostics/${name}.json`
- )
- .toString();
-
- return JSON.parse(jsonText);
-}
-
-function mapFixtureToVsCode(name: string): MappedRustDiagnostic {
- const rd = loadDiagnosticFixture(name);
- const mapResult = mapRustDiagnosticToVsCode(rd);
-
- if (!mapResult) {
- return assert.fail('Mapping unexpectedly failed');
- }
- return mapResult;
-}
-
-describe('mapRustDiagnosticToVsCode', () => {
- it('should map an incompatible type for trait error', () => {
- const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0053');
-
- assert.strictEqual(
- diagnostic.severity,
- vscode.DiagnosticSeverity.Error
- );
- assert.strictEqual(diagnostic.source, 'rustc');
- assert.strictEqual(
- diagnostic.message,
- [
- `method \`next\` has an incompatible type for trait`,
- `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``,
- ` found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``
- ].join('\n')
- );
- assert.strictEqual(diagnostic.code, 'E0053');
- assert.strictEqual(diagnostic.tags, undefined);
-
- // No related information
- assert.deepStrictEqual(diagnostic.relatedInformation, []);
-
- // There are no code actions available
- assert.strictEqual(codeActions.length, 0);
- });
-
- it('should map an unused variable warning', () => {
- const { diagnostic, codeActions } = mapFixtureToVsCode(
- 'warning/unused_variables'
- );
-
- assert.strictEqual(
- diagnostic.severity,
- vscode.DiagnosticSeverity.Warning
- );
- assert.strictEqual(
- diagnostic.message,
- [
- 'unused variable: `foo`',
- '#[warn(unused_variables)] on by default'
- ].join('\n')
- );
- assert.strictEqual(diagnostic.code, 'unused_variables');
- assert.strictEqual(diagnostic.source, 'rustc');
- assert.deepStrictEqual(diagnostic.tags, [
- vscode.DiagnosticTag.Unnecessary
- ]);
-
- // No related information
- assert.deepStrictEqual(diagnostic.relatedInformation, []);
-
- // One code action available to prefix the variable
- assert.strictEqual(codeActions.length, 1);
- const [codeAction] = codeActions;
- assert.strictEqual(
- codeAction.title,
- 'consider prefixing with an underscore: `_foo`'
- );
- assert(codeAction.isPreferred);
- });
-
- it('should map a wrong number of parameters error', () => {
- const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0061');
-
- assert.strictEqual(
- diagnostic.severity,
- vscode.DiagnosticSeverity.Error
- );
- assert.strictEqual(
- diagnostic.message,
- 'this function takes 2 parameters but 3 parameters were supplied'
- );
- assert.strictEqual(diagnostic.code, 'E0061');
- assert.strictEqual(diagnostic.source, 'rustc');
- assert.strictEqual(diagnostic.tags, undefined);
-
- // One related information for the original definition
- const relatedInformation = diagnostic.relatedInformation;
- if (!relatedInformation) {
- return assert.fail('Related information unexpectedly undefined');
- }
- assert.strictEqual(relatedInformation.length, 1);
- const [related] = relatedInformation;
- assert.strictEqual(related.message, 'defined here');
-
- // There are no actions available
- assert.strictEqual(codeActions.length, 0);
- });
-
- it('should map a Clippy copy pass by ref warning', () => {
- const { diagnostic, codeActions } = mapFixtureToVsCode(
- 'clippy/trivially_copy_pass_by_ref'
- );
-
- assert.strictEqual(
- diagnostic.severity,
- vscode.DiagnosticSeverity.Warning
- );
- assert.strictEqual(diagnostic.source, 'clippy');
- assert.strictEqual(
- diagnostic.message,
- [
- 'this argument is passed by reference, but would be more efficient if passed by value',
- '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]',
- 'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref'
- ].join('\n')
- );
- assert.strictEqual(diagnostic.code, 'trivially_copy_pass_by_ref');
- assert.strictEqual(diagnostic.tags, undefined);
-
- // One related information for the lint definition
- const relatedInformation = diagnostic.relatedInformation;
- if (!relatedInformation) {
- return assert.fail('Related information unexpectedly undefined');
- }
- assert.strictEqual(relatedInformation.length, 1);
- const [related] = relatedInformation;
- assert.strictEqual(related.message, 'lint level defined here');
-
- // One code action available to pass by value
- assert.strictEqual(codeActions.length, 1);
- const [codeAction] = codeActions;
- assert.strictEqual(
- codeAction.title,
- 'consider passing by value instead: `self`'
- );
- // Clippy does not mark this as machine applicable
- assert.strictEqual(codeAction.isPreferred, false);
- });
-});
--- /dev/null
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { SuggestionApplicability } from '../../../utils/diagnostics/rust';
+import SuggestedFix from '../../../utils/diagnostics/SuggestedFix';
+
+const location1 = new vscode.Location(
+ vscode.Uri.file('/file/1'),
+ new vscode.Range(new vscode.Position(1, 2), new vscode.Position(3, 4))
+);
+
+const location2 = new vscode.Location(
+ vscode.Uri.file('/file/2'),
+ new vscode.Range(new vscode.Position(5, 6), new vscode.Position(7, 8))
+);
+
+describe('SuggestedFix', () => {
+ describe('isEqual', () => {
+ it('should treat identical instances as equal', () => {
+ const suggestion1 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ const suggestion2 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ assert(suggestion1.isEqual(suggestion2));
+ });
+
+ it('should treat instances with different titles as inequal', () => {
+ const suggestion1 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ const suggestion2 = new SuggestedFix(
+ 'Not the same title!',
+ location1,
+ 'With this!'
+ );
+
+ assert(!suggestion1.isEqual(suggestion2));
+ });
+
+ it('should treat instances with different replacements as inequal', () => {
+ const suggestion1 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ const suggestion2 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With something else!'
+ );
+
+ assert(!suggestion1.isEqual(suggestion2));
+ });
+
+ it('should treat instances with different locations as inequal', () => {
+ const suggestion1 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ const suggestion2 = new SuggestedFix(
+ 'Replace me!',
+ location2,
+ 'With this!'
+ );
+
+ assert(!suggestion1.isEqual(suggestion2));
+ });
+
+ it('should treat instances with different applicability as inequal', () => {
+ const suggestion1 = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!',
+ SuggestionApplicability.MachineApplicable
+ );
+
+ const suggestion2 = new SuggestedFix(
+ 'Replace me!',
+ location2,
+ 'With this!',
+ SuggestionApplicability.HasPlaceholders
+ );
+
+ assert(!suggestion1.isEqual(suggestion2));
+ });
+ });
+
+ describe('toCodeAction', () => {
+ it('should map a simple suggestion', () => {
+ const suggestion = new SuggestedFix(
+ 'Replace me!',
+ location1,
+ 'With this!'
+ );
+
+ const codeAction = suggestion.toCodeAction();
+ assert.strictEqual(codeAction.kind, vscode.CodeActionKind.QuickFix);
+ assert.strictEqual(codeAction.title, 'Replace me!');
+ assert.strictEqual(codeAction.isPreferred, false);
+
+ const edit = codeAction.edit;
+ if (!edit) {
+ return assert.fail('Code Action edit unexpectedly missing');
+ }
+
+ const editEntries = edit.entries();
+ assert.strictEqual(editEntries.length, 1);
+
+ const [[editUri, textEdits]] = editEntries;
+ assert.strictEqual(editUri.toString(), location1.uri.toString());
+
+ assert.strictEqual(textEdits.length, 1);
+ const [textEdit] = textEdits;
+
+ assert(textEdit.range.isEqual(location1.range));
+ assert.strictEqual(textEdit.newText, 'With this!');
+ });
+ });
+});
--- /dev/null
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import SuggestedFix from '../../../utils/diagnostics/SuggestedFix';
+import SuggestedFixCollection from '../../../utils/diagnostics/SuggestedFixCollection';
+
+const uri1 = vscode.Uri.file('/file/1');
+const uri2 = vscode.Uri.file('/file/2');
+
+const mockDocument1 = ({
+ uri: uri1
+} as unknown) as vscode.TextDocument;
+
+const mockDocument2 = ({
+ uri: uri2
+} as unknown) as vscode.TextDocument;
+
+const range1 = new vscode.Range(
+ new vscode.Position(1, 2),
+ new vscode.Position(3, 4)
+);
+const range2 = new vscode.Range(
+ new vscode.Position(5, 6),
+ new vscode.Position(7, 8)
+);
+
+const diagnostic1 = new vscode.Diagnostic(range1, 'First diagnostic');
+const diagnostic2 = new vscode.Diagnostic(range2, 'Second diagnostic');
+
+// This is a mutable object so return a fresh instance every time
+function suggestion1(): SuggestedFix {
+ return new SuggestedFix(
+ 'Replace me!',
+ new vscode.Location(uri1, range1),
+ 'With this!'
+ );
+}
+
+describe('SuggestedFixCollection', () => {
+ it('should add a suggestion then return it as a code action', () => {
+ const suggestedFixes = new SuggestedFixCollection();
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
+
+ // Specify the document and range that exactly matches
+ const codeActions = suggestedFixes.provideCodeActions(
+ mockDocument1,
+ range1
+ );
+
+ assert.strictEqual(codeActions.length, 1);
+ const [codeAction] = codeActions;
+ assert.strictEqual(codeAction.title, suggestion1().title);
+
+ const { diagnostics } = codeAction;
+ if (!diagnostics) {
+ return assert.fail('Diagnostics unexpectedly missing');
+ }
+
+ assert.strictEqual(diagnostics.length, 1);
+ assert.strictEqual(diagnostics[0], diagnostic1);
+ });
+
+ it('should not return code actions for different ranges', () => {
+ const suggestedFixes = new SuggestedFixCollection();
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
+
+ const codeActions = suggestedFixes.provideCodeActions(
+ mockDocument1,
+ range2
+ );
+
+ assert(!codeActions || codeActions.length === 0);
+ });
+
+ it('should not return code actions for different documents', () => {
+ const suggestedFixes = new SuggestedFixCollection();
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
+
+ const codeActions = suggestedFixes.provideCodeActions(
+ mockDocument2,
+ range1
+ );
+
+ assert(!codeActions || codeActions.length === 0);
+ });
+
+ it('should not return code actions that have been cleared', () => {
+ const suggestedFixes = new SuggestedFixCollection();
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
+ suggestedFixes.clear();
+
+ const codeActions = suggestedFixes.provideCodeActions(
+ mockDocument1,
+ range1
+ );
+
+ assert(!codeActions || codeActions.length === 0);
+ });
+
+ it('should merge identical suggestions together', () => {
+ const suggestedFixes = new SuggestedFixCollection();
+
+ // Add the same suggestion for two diagnostics
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic1);
+ suggestedFixes.addSuggestedFixForDiagnostic(suggestion1(), diagnostic2);
+
+ const codeActions = suggestedFixes.provideCodeActions(
+ mockDocument1,
+ range1
+ );
+
+ assert.strictEqual(codeActions.length, 1);
+ const [codeAction] = codeActions;
+ const { diagnostics } = codeAction;
+
+ if (!diagnostics) {
+ return assert.fail('Diagnostics unexpectedly missing');
+ }
+
+ // We should be associated with both diagnostics
+ assert.strictEqual(diagnostics.length, 2);
+ assert.strictEqual(diagnostics[0], diagnostic1);
+ assert.strictEqual(diagnostics[1], diagnostic2);
+ });
+});
--- /dev/null
+import * as assert from 'assert';
+import * as fs from 'fs';
+import * as vscode from 'vscode';
+
+import {
+ MappedRustDiagnostic,
+ mapRustDiagnosticToVsCode,
+ RustDiagnostic,
+ SuggestionApplicability
+} from '../../../utils/diagnostics/rust';
+
+function loadDiagnosticFixture(name: string): RustDiagnostic {
+ const jsonText = fs
+ .readFileSync(
+ // We're actually in our JavaScript output directory, climb out
+ `${__dirname}/../../../../src/test/fixtures/rust-diagnostics/${name}.json`
+ )
+ .toString();
+
+ return JSON.parse(jsonText);
+}
+
+function mapFixtureToVsCode(name: string): MappedRustDiagnostic {
+ const rd = loadDiagnosticFixture(name);
+ const mapResult = mapRustDiagnosticToVsCode(rd);
+
+ if (!mapResult) {
+ return assert.fail('Mapping unexpectedly failed');
+ }
+ return mapResult;
+}
+
+describe('mapRustDiagnosticToVsCode', () => {
+ it('should map an incompatible type for trait error', () => {
+ const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
+ 'error/E0053'
+ );
+
+ assert.strictEqual(
+ diagnostic.severity,
+ vscode.DiagnosticSeverity.Error
+ );
+ assert.strictEqual(diagnostic.source, 'rustc');
+ assert.strictEqual(
+ diagnostic.message,
+ [
+ `method \`next\` has an incompatible type for trait`,
+ `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``,
+ ` found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``
+ ].join('\n')
+ );
+ assert.strictEqual(diagnostic.code, 'E0053');
+ assert.strictEqual(diagnostic.tags, undefined);
+
+ // No related information
+ assert.deepStrictEqual(diagnostic.relatedInformation, []);
+
+ // There are no suggested fixes
+ assert.strictEqual(suggestedFixes.length, 0);
+ });
+
+ it('should map an unused variable warning', () => {
+ const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
+ 'warning/unused_variables'
+ );
+
+ assert.strictEqual(
+ diagnostic.severity,
+ vscode.DiagnosticSeverity.Warning
+ );
+ assert.strictEqual(
+ diagnostic.message,
+ [
+ 'unused variable: `foo`',
+ '#[warn(unused_variables)] on by default'
+ ].join('\n')
+ );
+ assert.strictEqual(diagnostic.code, 'unused_variables');
+ assert.strictEqual(diagnostic.source, 'rustc');
+ assert.deepStrictEqual(diagnostic.tags, [
+ vscode.DiagnosticTag.Unnecessary
+ ]);
+
+ // No related information
+ assert.deepStrictEqual(diagnostic.relatedInformation, []);
+
+ // One suggested fix available to prefix the variable
+ assert.strictEqual(suggestedFixes.length, 1);
+ const [suggestedFix] = suggestedFixes;
+ assert.strictEqual(
+ suggestedFix.title,
+ 'consider prefixing with an underscore: `_foo`'
+ );
+ assert.strictEqual(
+ suggestedFix.applicability,
+ SuggestionApplicability.MachineApplicable
+ );
+ });
+
+ it('should map a wrong number of parameters error', () => {
+ const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
+ 'error/E0061'
+ );
+
+ assert.strictEqual(
+ diagnostic.severity,
+ vscode.DiagnosticSeverity.Error
+ );
+ assert.strictEqual(
+ diagnostic.message,
+ 'this function takes 2 parameters but 3 parameters were supplied'
+ );
+ assert.strictEqual(diagnostic.code, 'E0061');
+ assert.strictEqual(diagnostic.source, 'rustc');
+ assert.strictEqual(diagnostic.tags, undefined);
+
+ // One related information for the original definition
+ const relatedInformation = diagnostic.relatedInformation;
+ if (!relatedInformation) {
+ return assert.fail('Related information unexpectedly undefined');
+ }
+ assert.strictEqual(relatedInformation.length, 1);
+ const [related] = relatedInformation;
+ assert.strictEqual(related.message, 'defined here');
+
+ // There are no suggested fixes
+ assert.strictEqual(suggestedFixes.length, 0);
+ });
+
+ it('should map a Clippy copy pass by ref warning', () => {
+ const { diagnostic, suggestedFixes } = mapFixtureToVsCode(
+ 'clippy/trivially_copy_pass_by_ref'
+ );
+
+ assert.strictEqual(
+ diagnostic.severity,
+ vscode.DiagnosticSeverity.Warning
+ );
+ assert.strictEqual(diagnostic.source, 'clippy');
+ assert.strictEqual(
+ diagnostic.message,
+ [
+ 'this argument is passed by reference, but would be more efficient if passed by value',
+ '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]',
+ 'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref'
+ ].join('\n')
+ );
+ assert.strictEqual(diagnostic.code, 'trivially_copy_pass_by_ref');
+ assert.strictEqual(diagnostic.tags, undefined);
+
+ // One related information for the lint definition
+ const relatedInformation = diagnostic.relatedInformation;
+ if (!relatedInformation) {
+ return assert.fail('Related information unexpectedly undefined');
+ }
+ assert.strictEqual(relatedInformation.length, 1);
+ const [related] = relatedInformation;
+ assert.strictEqual(related.message, 'lint level defined here');
+
+ // One suggested fix to pass by value
+ assert.strictEqual(suggestedFixes.length, 1);
+ const [suggestedFix] = suggestedFixes;
+ assert.strictEqual(
+ suggestedFix.title,
+ 'consider passing by value instead: `self`'
+ );
+ // Clippy does not mark this with any applicability
+ assert.strictEqual(
+ suggestedFix.applicability,
+ SuggestionApplicability.Unspecified
+ );
+ });
+});
--- /dev/null
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { areDiagnosticsEqual } from '../../../utils/diagnostics/vscode';
+
+const range1 = new vscode.Range(
+ new vscode.Position(1, 2),
+ new vscode.Position(3, 4)
+);
+
+const range2 = new vscode.Range(
+ new vscode.Position(5, 6),
+ new vscode.Position(7, 8)
+);
+
+describe('areDiagnosticsEqual', () => {
+ it('should treat identical diagnostics as equal', () => {
+ const diagnostic1 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ const diagnostic2 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ assert(areDiagnosticsEqual(diagnostic1, diagnostic2));
+ });
+
+ it('should treat diagnostics with different sources as inequal', () => {
+ const diagnostic1 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+ diagnostic1.source = 'rustc';
+
+ const diagnostic2 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+ diagnostic2.source = 'clippy';
+
+ assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+ });
+
+ it('should treat diagnostics with different ranges as inequal', () => {
+ const diagnostic1 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ const diagnostic2 = new vscode.Diagnostic(
+ range2,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+ });
+
+ it('should treat diagnostics with different messages as inequal', () => {
+ const diagnostic1 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ const diagnostic2 = new vscode.Diagnostic(
+ range1,
+ 'Goodbye!, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+ });
+
+ it('should treat diagnostics with different severities as inequal', () => {
+ const diagnostic1 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Warning
+ );
+
+ const diagnostic2 = new vscode.Diagnostic(
+ range1,
+ 'Hello, world!',
+ vscode.DiagnosticSeverity.Error
+ );
+
+ assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+ });
+});
+++ /dev/null
-import * as assert from 'assert';
-import * as vscode from 'vscode';
-
-import {
- areCodeActionsEqual,
- areDiagnosticsEqual
-} from '../utils/vscode_diagnostics';
-
-const uri = vscode.Uri.file('/file/1');
-
-const range1 = new vscode.Range(
- new vscode.Position(1, 2),
- new vscode.Position(3, 4)
-);
-
-const range2 = new vscode.Range(
- new vscode.Position(5, 6),
- new vscode.Position(7, 8)
-);
-
-describe('areDiagnosticsEqual', () => {
- it('should treat identical diagnostics as equal', () => {
- const diagnostic1 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- const diagnostic2 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- assert(areDiagnosticsEqual(diagnostic1, diagnostic2));
- });
-
- it('should treat diagnostics with different sources as inequal', () => {
- const diagnostic1 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
- diagnostic1.source = 'rustc';
-
- const diagnostic2 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
- diagnostic2.source = 'clippy';
-
- assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
- });
-
- it('should treat diagnostics with different ranges as inequal', () => {
- const diagnostic1 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- const diagnostic2 = new vscode.Diagnostic(
- range2,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
- });
-
- it('should treat diagnostics with different messages as inequal', () => {
- const diagnostic1 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- const diagnostic2 = new vscode.Diagnostic(
- range1,
- 'Goodbye!, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
- });
-
- it('should treat diagnostics with different severities as inequal', () => {
- const diagnostic1 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Warning
- );
-
- const diagnostic2 = new vscode.Diagnostic(
- range1,
- 'Hello, world!',
- vscode.DiagnosticSeverity.Error
- );
-
- assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
- });
-});
-
-describe('areCodeActionsEqual', () => {
- it('should treat identical actions as equal', () => {
- const codeAction1 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.QuickFix
- );
-
- const codeAction2 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.QuickFix
- );
-
- const edit = new vscode.WorkspaceEdit();
- edit.replace(uri, range1, 'Replace with this');
- codeAction1.edit = edit;
- codeAction2.edit = edit;
-
- assert(areCodeActionsEqual(codeAction1, codeAction2));
- });
-
- it('should treat actions with different types as inequal', () => {
- const codeAction1 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.Refactor
- );
-
- const codeAction2 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.QuickFix
- );
-
- const edit = new vscode.WorkspaceEdit();
- edit.replace(uri, range1, 'Replace with this');
- codeAction1.edit = edit;
- codeAction2.edit = edit;
-
- assert(!areCodeActionsEqual(codeAction1, codeAction2));
- });
-
- it('should treat actions with different titles as inequal', () => {
- const codeAction1 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.Refactor
- );
-
- const codeAction2 = new vscode.CodeAction(
- 'Do something different!',
- vscode.CodeActionKind.Refactor
- );
-
- const edit = new vscode.WorkspaceEdit();
- edit.replace(uri, range1, 'Replace with this');
- codeAction1.edit = edit;
- codeAction2.edit = edit;
-
- assert(!areCodeActionsEqual(codeAction1, codeAction2));
- });
-
- it('should treat actions with different edits as inequal', () => {
- const codeAction1 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.Refactor
- );
- const edit1 = new vscode.WorkspaceEdit();
- edit1.replace(uri, range1, 'Replace with this');
- codeAction1.edit = edit1;
-
- const codeAction2 = new vscode.CodeAction(
- 'Fix me!',
- vscode.CodeActionKind.Refactor
- );
- const edit2 = new vscode.WorkspaceEdit();
- edit2.replace(uri, range1, 'Replace with this other thing');
- codeAction2.edit = edit2;
-
- assert(!areCodeActionsEqual(codeAction1, codeAction2));
- });
-});
--- /dev/null
+import * as vscode from 'vscode';
+
+import { SuggestionApplicability } from './rust';
+
+/**
+ * Model object for text replacements suggested by the Rust compiler
+ *
+ * This is an intermediate form between the raw `rustc` JSON and a
+ * `vscode.CodeAction`. It's optimised for the use-cases of
+ * `SuggestedFixCollection`.
+ */
+export default class SuggestedFix {
+ public readonly title: string;
+ public readonly location: vscode.Location;
+ public readonly replacement: string;
+ public readonly applicability: SuggestionApplicability;
+
+ /**
+ * Diagnostics this suggested fix could resolve
+ */
+ public diagnostics: vscode.Diagnostic[];
+
+ constructor(
+ title: string,
+ location: vscode.Location,
+ replacement: string,
+ applicability: SuggestionApplicability = SuggestionApplicability.Unspecified
+ ) {
+ this.title = title;
+ this.location = location;
+ this.replacement = replacement;
+ this.applicability = applicability;
+ this.diagnostics = [];
+ }
+
+ /**
+ * Determines if this suggested fix is equivalent to another instance
+ */
+ public isEqual(other: SuggestedFix): boolean {
+ return (
+ this.title === other.title &&
+ this.location.range.isEqual(other.location.range) &&
+ this.replacement === other.replacement &&
+ this.applicability === other.applicability
+ );
+ }
+
+ /**
+ * Converts this suggested fix to a VS Code Quick Fix code action
+ */
+ public toCodeAction(): vscode.CodeAction {
+ const codeAction = new vscode.CodeAction(
+ this.title,
+ vscode.CodeActionKind.QuickFix
+ );
+
+ const edit = new vscode.WorkspaceEdit();
+ edit.replace(this.location.uri, this.location.range, this.replacement);
+ codeAction.edit = edit;
+
+ codeAction.isPreferred =
+ this.applicability === SuggestionApplicability.MachineApplicable;
+
+ codeAction.diagnostics = [...this.diagnostics];
+ return codeAction;
+ }
+}
--- /dev/null
+import * as vscode from 'vscode';
+import SuggestedFix from './SuggestedFix';
+
+/**
+ * Collection of suggested fixes across multiple documents
+ *
+ * This stores `SuggestedFix` model objects and returns them via the
+ * `vscode.CodeActionProvider` interface.
+ */
+export default class SuggestedFixCollection
+ implements vscode.CodeActionProvider {
+ public static PROVIDED_CODE_ACTION_KINDS = [vscode.CodeActionKind.QuickFix];
+
+ private suggestedFixes: Map<string, SuggestedFix[]>;
+
+ constructor() {
+ this.suggestedFixes = new Map();
+ }
+
+ /**
+ * Clears all suggested fixes across all documents
+ */
+ public clear(): void {
+ this.suggestedFixes = new Map();
+ }
+
+ /**
+ * Adds a suggested fix for the given diagnostic
+ *
+ * Some suggested fixes will appear in multiple diagnostics. For example,
+ * forgetting a `mut` on a variable will suggest changing the delaration on
+ * every mutable usage site. If the suggested fix has already been added
+ * this method will instead associate the existing fix with the new
+ * diagnostic.
+ */
+ public addSuggestedFixForDiagnostic(
+ suggestedFix: SuggestedFix,
+ diagnostic: vscode.Diagnostic
+ ): void {
+ const fileUriString = suggestedFix.location.uri.toString();
+ const fileSuggestions = this.suggestedFixes.get(fileUriString) || [];
+
+ const existingSuggestion = fileSuggestions.find(s =>
+ s.isEqual(suggestedFix)
+ );
+
+ if (existingSuggestion) {
+ // The existing suggestion also applies to this new diagnostic
+ existingSuggestion.diagnostics.push(diagnostic);
+ } else {
+ // We haven't seen this suggestion before
+ suggestedFix.diagnostics.push(diagnostic);
+ fileSuggestions.push(suggestedFix);
+ }
+
+ this.suggestedFixes.set(fileUriString, fileSuggestions);
+ }
+
+ /**
+ * Filters suggested fixes by their document and range and converts them to
+ * code actions
+ */
+ public provideCodeActions(
+ document: vscode.TextDocument,
+ range: vscode.Range
+ ): vscode.CodeAction[] {
+ const documentUriString = document.uri.toString();
+
+ const suggestedFixes = this.suggestedFixes.get(documentUriString);
+ return (suggestedFixes || [])
+ .filter(({ location }) => location.range.intersection(range))
+ .map(suggestedEdit => suggestedEdit.toCodeAction());
+ }
+}
--- /dev/null
+import * as path from 'path';
+import * as vscode from 'vscode';
+
+import SuggestedFix from './SuggestedFix';
+
+export enum SuggestionApplicability {
+ MachineApplicable = 'MachineApplicable',
+ HasPlaceholders = 'HasPlaceholders',
+ MaybeIncorrect = 'MaybeIncorrect',
+ Unspecified = 'Unspecified'
+}
+
+// Reference:
+// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
+export interface RustDiagnosticSpan {
+ line_start: number;
+ line_end: number;
+ column_start: number;
+ column_end: number;
+ is_primary: boolean;
+ file_name: string;
+ label?: string;
+ suggested_replacement?: string;
+ suggestion_applicability?: SuggestionApplicability;
+}
+
+export interface RustDiagnostic {
+ spans: RustDiagnosticSpan[];
+ rendered: string;
+ message: string;
+ level: string;
+ code?: {
+ code: string;
+ };
+ children: RustDiagnostic[];
+}
+
+export interface MappedRustDiagnostic {
+ location: vscode.Location;
+ diagnostic: vscode.Diagnostic;
+ suggestedFixes: SuggestedFix[];
+}
+
+interface MappedRustChildDiagnostic {
+ related?: vscode.DiagnosticRelatedInformation;
+ suggestedFix?: SuggestedFix;
+ messageLine?: string;
+}
+
+/**
+ * Converts a Rust level string to a VsCode severity
+ */
+function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity {
+ if (s === 'error') {
+ return vscode.DiagnosticSeverity.Error;
+ }
+ if (s.startsWith('warn')) {
+ return vscode.DiagnosticSeverity.Warning;
+ }
+ return vscode.DiagnosticSeverity.Information;
+}
+
+/**
+ * Converts a Rust span to a VsCode location
+ */
+function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location {
+ const fileName = path.join(vscode.workspace.rootPath!, span.file_name);
+ const fileUri = vscode.Uri.file(fileName);
+
+ const range = new vscode.Range(
+ new vscode.Position(span.line_start - 1, span.column_start - 1),
+ new vscode.Position(span.line_end - 1, span.column_end - 1)
+ );
+
+ return new vscode.Location(fileUri, range);
+}
+
+/**
+ * Converts a secondary Rust span to a VsCode related information
+ *
+ * If the span is unlabelled this will return `undefined`.
+ */
+function mapSecondarySpanToRelated(
+ span: RustDiagnosticSpan
+): vscode.DiagnosticRelatedInformation | undefined {
+ if (!span.label) {
+ // Nothing to label this with
+ return;
+ }
+
+ const location = mapSpanToLocation(span);
+ return new vscode.DiagnosticRelatedInformation(location, span.label);
+}
+
+/**
+ * Determines if diagnostic is related to unused code
+ */
+function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean {
+ if (!rd.code) {
+ return false;
+ }
+
+ return [
+ 'dead_code',
+ 'unknown_lints',
+ 'unused_attributes',
+ 'unused_imports',
+ 'unused_macros',
+ 'unused_variables'
+ ].includes(rd.code.code);
+}
+
+/**
+ * Converts a Rust child diagnostic to a VsCode related information
+ *
+ * This can have three outcomes:
+ *
+ * 1. If this is no primary span this will return a `noteLine`
+ * 2. If there is a primary span with a suggested replacement it will return a
+ * `codeAction`.
+ * 3. If there is a primary span without a suggested replacement it will return
+ * a `related`.
+ */
+function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
+ const span = rd.spans.find(s => s.is_primary);
+
+ if (!span) {
+ // `rustc` uses these spanless children as a way to print multi-line
+ // messages
+ return { messageLine: rd.message };
+ }
+
+ // If we have a primary span use its location, otherwise use the parent
+ const location = mapSpanToLocation(span);
+
+ // We need to distinguish `null` from an empty string
+ if (span && typeof span.suggested_replacement === 'string') {
+ // Include our replacement in the title unless it's empty
+ const title = span.suggested_replacement
+ ? `${rd.message}: \`${span.suggested_replacement}\``
+ : rd.message;
+
+ return {
+ suggestedFix: new SuggestedFix(
+ title,
+ location,
+ span.suggested_replacement,
+ span.suggestion_applicability
+ )
+ };
+ } else {
+ const related = new vscode.DiagnosticRelatedInformation(
+ location,
+ rd.message
+ );
+
+ return { related };
+ }
+}
+
+/**
+ * Converts a Rust root diagnostic to VsCode form
+ *
+ * This flattens the Rust diagnostic by:
+ *
+ * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
+ * 2. Adding any labelled secondary spans to `relatedInformation`
+ * 3. Categorising child diagnostics as either `SuggestedFix`es,
+ * `relatedInformation` or additional message lines.
+ *
+ * If the diagnostic has no primary span this will return `undefined`
+ */
+export function mapRustDiagnosticToVsCode(
+ rd: RustDiagnostic
+): MappedRustDiagnostic | undefined {
+ const primarySpan = rd.spans.find(s => s.is_primary);
+ if (!primarySpan) {
+ return;
+ }
+
+ const location = mapSpanToLocation(primarySpan);
+ const secondarySpans = rd.spans.filter(s => !s.is_primary);
+
+ const severity = mapLevelToSeverity(rd.level);
+
+ const vd = new vscode.Diagnostic(location.range, rd.message, severity);
+
+ let source = 'rustc';
+ let code = rd.code && rd.code.code;
+ if (code) {
+ // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
+ const scopedCode = code.split('::');
+ if (scopedCode.length === 2) {
+ [source, code] = scopedCode;
+ }
+ }
+
+ vd.source = source;
+ vd.code = code;
+ vd.relatedInformation = [];
+
+ for (const secondarySpan of secondarySpans) {
+ const related = mapSecondarySpanToRelated(secondarySpan);
+ if (related) {
+ vd.relatedInformation.push(related);
+ }
+ }
+
+ const suggestedFixes = [];
+ for (const child of rd.children) {
+ const { related, suggestedFix, messageLine } = mapRustChildDiagnostic(
+ child
+ );
+
+ if (related) {
+ vd.relatedInformation.push(related);
+ }
+ if (suggestedFix) {
+ suggestedFixes.push(suggestedFix);
+ }
+ if (messageLine) {
+ vd.message += `\n${messageLine}`;
+ }
+ }
+
+ if (isUnusedOrUnnecessary(rd)) {
+ vd.tags = [vscode.DiagnosticTag.Unnecessary];
+ }
+
+ return {
+ location,
+ diagnostic: vd,
+ suggestedFixes
+ };
+}
--- /dev/null
+import * as vscode from 'vscode';
+
+/** Compares two `vscode.Diagnostic`s for equality */
+export function areDiagnosticsEqual(
+ left: vscode.Diagnostic,
+ right: vscode.Diagnostic
+): boolean {
+ return (
+ left.source === right.source &&
+ left.severity === right.severity &&
+ left.range.isEqual(right.range) &&
+ left.message === right.message
+ );
+}
+++ /dev/null
-import * as path from 'path';
-import * as vscode from 'vscode';
-
-// Reference:
-// https://github.com/rust-lang/rust/blob/master/src/libsyntax/json.rs
-export interface RustDiagnosticSpan {
- line_start: number;
- line_end: number;
- column_start: number;
- column_end: number;
- is_primary: boolean;
- file_name: string;
- label?: string;
- suggested_replacement?: string;
- suggestion_applicability?:
- | 'MachineApplicable'
- | 'HasPlaceholders'
- | 'MaybeIncorrect'
- | 'Unspecified';
-}
-
-export interface RustDiagnostic {
- spans: RustDiagnosticSpan[];
- rendered: string;
- message: string;
- level: string;
- code?: {
- code: string;
- };
- children: RustDiagnostic[];
-}
-
-export interface MappedRustDiagnostic {
- location: vscode.Location;
- diagnostic: vscode.Diagnostic;
- codeActions: vscode.CodeAction[];
-}
-
-interface MappedRustChildDiagnostic {
- related?: vscode.DiagnosticRelatedInformation;
- codeAction?: vscode.CodeAction;
- messageLine?: string;
-}
-
-/**
- * Converts a Rust level string to a VsCode severity
- */
-function mapLevelToSeverity(s: string): vscode.DiagnosticSeverity {
- if (s === 'error') {
- return vscode.DiagnosticSeverity.Error;
- }
- if (s.startsWith('warn')) {
- return vscode.DiagnosticSeverity.Warning;
- }
- return vscode.DiagnosticSeverity.Information;
-}
-
-/**
- * Converts a Rust span to a VsCode location
- */
-function mapSpanToLocation(span: RustDiagnosticSpan): vscode.Location {
- const fileName = path.join(vscode.workspace.rootPath!, span.file_name);
- const fileUri = vscode.Uri.file(fileName);
-
- const range = new vscode.Range(
- new vscode.Position(span.line_start - 1, span.column_start - 1),
- new vscode.Position(span.line_end - 1, span.column_end - 1)
- );
-
- return new vscode.Location(fileUri, range);
-}
-
-/**
- * Converts a secondary Rust span to a VsCode related information
- *
- * If the span is unlabelled this will return `undefined`.
- */
-function mapSecondarySpanToRelated(
- span: RustDiagnosticSpan
-): vscode.DiagnosticRelatedInformation | undefined {
- if (!span.label) {
- // Nothing to label this with
- return;
- }
-
- const location = mapSpanToLocation(span);
- return new vscode.DiagnosticRelatedInformation(location, span.label);
-}
-
-/**
- * Determines if diagnostic is related to unused code
- */
-function isUnusedOrUnnecessary(rd: RustDiagnostic): boolean {
- if (!rd.code) {
- return false;
- }
-
- return [
- 'dead_code',
- 'unknown_lints',
- 'unused_attributes',
- 'unused_imports',
- 'unused_macros',
- 'unused_variables'
- ].includes(rd.code.code);
-}
-
-/**
- * Converts a Rust child diagnostic to a VsCode related information
- *
- * This can have three outcomes:
- *
- * 1. If this is no primary span this will return a `noteLine`
- * 2. If there is a primary span with a suggested replacement it will return a
- * `codeAction`.
- * 3. If there is a primary span without a suggested replacement it will return
- * a `related`.
- */
-function mapRustChildDiagnostic(rd: RustDiagnostic): MappedRustChildDiagnostic {
- const span = rd.spans.find(s => s.is_primary);
-
- if (!span) {
- // `rustc` uses these spanless children as a way to print multi-line
- // messages
- return { messageLine: rd.message };
- }
-
- // If we have a primary span use its location, otherwise use the parent
- const location = mapSpanToLocation(span);
-
- // We need to distinguish `null` from an empty string
- if (span && typeof span.suggested_replacement === 'string') {
- const edit = new vscode.WorkspaceEdit();
- edit.replace(location.uri, location.range, span.suggested_replacement);
-
- // Include our replacement in the label unless it's empty
- const title = span.suggested_replacement
- ? `${rd.message}: \`${span.suggested_replacement}\``
- : rd.message;
-
- const codeAction = new vscode.CodeAction(
- title,
- vscode.CodeActionKind.QuickFix
- );
-
- codeAction.edit = edit;
- codeAction.isPreferred =
- span.suggestion_applicability === 'MachineApplicable';
-
- return { codeAction };
- } else {
- const related = new vscode.DiagnosticRelatedInformation(
- location,
- rd.message
- );
-
- return { related };
- }
-}
-
-/**
- * Converts a Rust root diagnostic to VsCode form
- *
- * This flattens the Rust diagnostic by:
- *
- * 1. Creating a `vscode.Diagnostic` with the root message and primary span.
- * 2. Adding any labelled secondary spans to `relatedInformation`
- * 3. Categorising child diagnostics as either Quick Fix actions,
- * `relatedInformation` or additional message lines.
- *
- * If the diagnostic has no primary span this will return `undefined`
- */
-export function mapRustDiagnosticToVsCode(
- rd: RustDiagnostic
-): MappedRustDiagnostic | undefined {
- const codeActions = [];
-
- const primarySpan = rd.spans.find(s => s.is_primary);
- if (!primarySpan) {
- return;
- }
-
- const location = mapSpanToLocation(primarySpan);
- const secondarySpans = rd.spans.filter(s => !s.is_primary);
-
- const severity = mapLevelToSeverity(rd.level);
-
- const vd = new vscode.Diagnostic(location.range, rd.message, severity);
-
- let source = 'rustc';
- let code = rd.code && rd.code.code;
- if (code) {
- // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
- const scopedCode = code.split('::');
- if (scopedCode.length === 2) {
- [source, code] = scopedCode;
- }
- }
-
- vd.source = source;
- vd.code = code;
- vd.relatedInformation = [];
-
- for (const secondarySpan of secondarySpans) {
- const related = mapSecondarySpanToRelated(secondarySpan);
- if (related) {
- vd.relatedInformation.push(related);
- }
- }
-
- for (const child of rd.children) {
- const { related, codeAction, messageLine } = mapRustChildDiagnostic(
- child
- );
-
- if (related) {
- vd.relatedInformation.push(related);
- }
- if (codeAction) {
- codeActions.push(codeAction);
- }
- if (messageLine) {
- vd.message += `\n${messageLine}`;
- }
- }
-
- if (isUnusedOrUnnecessary(rd)) {
- vd.tags = [vscode.DiagnosticTag.Unnecessary];
- }
-
- return {
- location,
- diagnostic: vd,
- codeActions
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-
-/** Compares two `vscode.Diagnostic`s for equality */
-export function areDiagnosticsEqual(
- left: vscode.Diagnostic,
- right: vscode.Diagnostic
-): boolean {
- return (
- left.source === right.source &&
- left.severity === right.severity &&
- left.range.isEqual(right.range) &&
- left.message === right.message
- );
-}
-
-/** Compares two `vscode.TextEdit`s for equality */
-function areTextEditsEqual(
- left: vscode.TextEdit,
- right: vscode.TextEdit
-): boolean {
- if (!left.range.isEqual(right.range)) {
- return false;
- }
-
- if (left.newText !== right.newText) {
- return false;
- }
-
- return true;
-}
-
-/** Compares two `vscode.CodeAction`s for equality */
-export function areCodeActionsEqual(
- left: vscode.CodeAction,
- right: vscode.CodeAction
-): boolean {
- if (
- left.kind !== right.kind ||
- left.title !== right.title ||
- !left.edit ||
- !right.edit
- ) {
- return false;
- }
-
- const leftEditEntries = left.edit.entries();
- const rightEditEntries = right.edit.entries();
-
- if (leftEditEntries.length !== rightEditEntries.length) {
- return false;
- }
-
- for (let i = 0; i < leftEditEntries.length; i++) {
- const [leftUri, leftEdits] = leftEditEntries[i];
- const [rightUri, rightEdits] = rightEditEntries[i];
-
- if (leftUri.toString() !== rightUri.toString()) {
- return false;
- }
-
- if (leftEdits.length !== rightEdits.length) {
- return false;
- }
-
- for (let j = 0; j < leftEdits.length; j++) {
- if (!areTextEditsEqual(leftEdits[j], rightEdits[j])) {
- return false;
- }
- }
- }
-
- return true;
-}