Skip to content

Commit f633b62

Browse files
committed
add diagnostic & code actions features
1 parent eed66ef commit f633b62

10 files changed

Lines changed: 416 additions & 34 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Extension features:
4949
* Generates localization code depending on json files.
5050
* Forms & animation made easy.
5151
* Customizable! so developers can add their own properties and modify some features.
52-
* Supports Code completion, hover information, Go to Definition.
52+
* Supports Code completion, hover information, Go to Definition, diagnostics and code actions.
5353

5454
# Get Started
5555

src/dart/extension/providers/dart_completion_item_provider.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
3737
allResults = (await this.getTagNameCompletions(document, position, token)).concat(this.getClosingTagNameCompletions(document, position));
3838
}
3939
else if (isAttributeValue(document, position)) {
40-
allResults = this.getAttributeValueCompletions(document, position, token);
40+
allResults = await this.getAttributeValueCompletions(document, position, token);
4141
}
4242
else if (isAttribute(document, position)) {
4343
allResults = await this.getAttributeCompletions(document, position, token);
@@ -129,9 +129,26 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
129129
return await this.getAttributeCompletionsForElement(tag, false, document, position, token);
130130
}
131131

132+
private hasUnnamedArgs(tag: string): boolean {
133+
return tag === 'Text';
134+
}
135+
136+
private getUnnamedArgsRange(tag: string, dartDocument: TextDocument, offset: number): number {
137+
const dart = dartDocument.getText();
138+
offset = dart.indexOf(' ' + tag + '(', offset - tag.length - 4);
139+
const pos = dartDocument.positionAt(offset).translate({ lineDelta: 1, characterDelta: 200 });
140+
offset = dartDocument.offsetAt(pos);
141+
return offset;
142+
}
143+
132144
private async getAttributeCompletionsForElement(elementTag: string, isTag: boolean, xmlDocument: TextDocument, xmlPosition: Position, token: CancellationToken): Promise<CompletionItem[]> {
133145
const dartDocument = await getDartDocument(xmlDocument);
134-
const dartOffset = await getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, null);
146+
const hasUnnamedArgs = this.hasUnnamedArgs(elementTag);
147+
const wordRange = xmlDocument.getWordRangeAtPosition(xmlPosition);
148+
let dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange, false, !!elementTag, hasUnnamedArgs);
149+
if (hasUnnamedArgs) {
150+
dartOffset = this.getUnnamedArgsRange(elementTag, dartDocument, dartOffset);
151+
}
135152

136153
const line = xmlDocument.lineAt(xmlPosition.line).text.slice(0, xmlPosition.character);
137154
const nextCharacter = xmlDocument.getText(new Range(xmlPosition, xmlPosition.translate({ characterDelta: 200 }))).trim().substr(0, 1);
@@ -143,13 +160,8 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
143160
offset: dartOffset,
144161
});
145162

146-
// get used attributes
147-
// todo
148-
const currentUsedAttributes: string[] = [];
149-
150163
// map results
151164
const includedResults = resp.results
152-
.filter((a: any) => currentUsedAttributes.indexOf(a.displayText || a.completion) === -1)
153165
.map((r: any) => {
154166
const a = this.convertResult(xmlDocument, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, resp, r);
155167
const item = new vs.CompletionItem(a.label.replace(':', ''), a.kind);
@@ -168,7 +180,7 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
168180
});
169181

170182
const cachedResults = await this.getCachedResults(xmlDocument, token, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, xmlDocument.offsetAt(xmlPosition), resp);
171-
return [...includedResults, ...cachedResults, ...(isTag ? this.getWrapperPropertiesElementsCompletionItems(xmlDocument, xmlPosition) : this.getWrapperPropertiesCompletionItems(xmlDocument, xmlPosition))];
183+
return [...includedResults, ...(cachedResults && cachedResults.length ? cachedResults : []), ...(isTag ? this.getWrapperPropertiesElementsCompletionItems(xmlDocument, xmlPosition) : this.getWrapperPropertiesCompletionItems(xmlDocument, xmlPosition))];
172184
}
173185

174186
private getAttributeCompletionsForAttribute(elementTag: string, document: TextDocument, position: Position): vs.CompletionItem[] {
@@ -267,13 +279,13 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
267279
return item;
268280
}
269281

270-
private getAttributeValueCompletions(document: TextDocument, position: Position, token: CancellationToken): CompletionItem[] {
271-
const completions: CompletionItem[] = [];
272-
282+
private async getAttributeValueCompletions(xmlDocument: TextDocument, xmlPosition: Position, token: CancellationToken): Promise<CompletionItem[]> {
273283
// Get the attribute name
274-
const wordRange = document.getWordRangeAtPosition(position);
275-
const wordStart = wordRange ? wordRange.start : position;
276-
const line = document.getText(new Range(wordStart.line, 0, wordStart.line, wordStart.character));
284+
const wordRange = xmlDocument.getWordRangeAtPosition(xmlPosition);
285+
const wordStart = wordRange ? wordRange.start : xmlPosition;
286+
const wordEnd = wordRange ? wordRange.end : xmlPosition;
287+
const lineRange = new Range(wordStart.line, 0, wordStart.line, wordEnd.character);
288+
const line = xmlDocument.getText(lineRange);
277289
const attrNamePattern = /[\.\-:_a-zA-Z0-9]+=/g;
278290
const match = line.match(attrNamePattern);
279291

@@ -285,7 +297,7 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
285297
attrName = attrName.slice(0, -1);
286298

287299
// Get the XPath
288-
const xPath = getXPath(document, position);
300+
const xPath = getXPath(xmlDocument, xmlPosition);
289301
const isTopLevelElement = xPath.length === 1;
290302
const isSecondLevelElement = xPath.length === 2;
291303

@@ -300,15 +312,37 @@ export class DartCompletionItemProvider implements CompletionItemProvider, IAmDi
300312
}
301313
}
302314

303-
// todo
304-
const children: any[] = [];
315+
const dartDocument = await getDartDocument(xmlDocument);
316+
let attrValue = xmlDocument.getText(new Range(new Position(xmlPosition.line, 0), xmlPosition.translate({ characterDelta: 200 })));
317+
let endIndex = attrValue.indexOf('" ');
318+
endIndex = endIndex === -1 ? attrValue.indexOf('">') : endIndex;
319+
attrValue = attrValue.substring(attrValue.indexOf(attrName + '="') + attrName.length + 2, endIndex);
320+
const cursorPos = line.substr(line.lastIndexOf(attrName + '="') + 2).length - attrName.length - Math.abs(wordEnd.character - xmlPosition.character);
321+
const dartOffset = dartDocument.getText().indexOf(attrName + ': ' + attrValue) + attrName.length + 2 + cursorPos;
322+
const nextCharacter = xmlDocument.getText(new Range(xmlPosition, xmlPosition.translate({ characterDelta: 200 }))).trim().substr(0, 1);
323+
const conf = config.for(xmlDocument.uri);
324+
const enableCommitCharacters = conf.enableCompletionCommitCharacters;
325+
const insertArgumentPlaceholders = !enableCommitCharacters && conf.insertArgumentPlaceholders && this.shouldAllowArgPlaceholders(line);
305326

306-
// Apply a filter with the current prefix and return.
307-
children.forEach((child: any) => {
308-
const suggestion = new CompletionItem(child.displayText);
309-
suggestion.detail = child.rightLabel;
310-
completions.push(suggestion);
327+
const resp = await this.analyzer.completionGetSuggestionsResults({
328+
file: fsPath(dartDocument.uri),
329+
offset: dartOffset,
311330
});
331+
332+
// todo
333+
const completions: CompletionItem[] = resp.results
334+
.map((r: any) => {
335+
const a = this.convertResult(xmlDocument, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, resp, r);
336+
const item = new vs.CompletionItem(a.label.replace(':', ''), a.kind);
337+
(item as any)._documentation = (a as any)._documentation;
338+
// item.insertText = new SnippetString(item.label + '>$0</' + item.label + '>');
339+
// item.insertText = new SnippetString(item.label + '>$0');
340+
// item.insertText = new SnippetString(item.label + '="$0"');
341+
item.detail = a.detail;
342+
item.filterText = a.filterText;
343+
item.sortText = a.sortText;
344+
return item;
345+
});
312346

313347
return completions;
314348
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Diagnostic, DiagnosticCollection, DiagnosticSeverity, DiagnosticTag, Uri, TextDocument, workspace } from "vscode";
2+
import * as as from "../../shared/analysis_server_types";
3+
// import { Analyzer } from "../analysis/analyzer";
4+
import { config } from "../config";
5+
import { toRangeOnLine } from "../utils";
6+
import * as fs from 'fs';
7+
import { denodeify } from 'q';
8+
import { getXmlCodeWordLocation } from "../../utils";
9+
// const readFileSync = denodeify(fs.readFileSync);
10+
11+
// TODO: This is not a provider?
12+
export class DartDiagnosticProvider {
13+
private lastErrorJson: string | undefined;
14+
constructor(private readonly analyzer: any, private readonly diagnostics: DiagnosticCollection) {
15+
this.analyzer.registerForAnalysisErrors((es: any) => this.handleErrors(es));
16+
17+
// Fired when files are deleted
18+
this.analyzer.registerForAnalysisFlushResults((es: any) => this.flushResults(es));
19+
}
20+
21+
private async handleErrors(notification: as.AnalysisErrorsNotification) {
22+
const notificationJson = JSON.stringify(notification);
23+
24+
// As a workaround for https://github.com/Dart-Code/Dart-Code/issues/1678, if
25+
// the errors we got are exactly the same as the previous set, do not give
26+
// them to VS Code. This avoids a potential loop of refreshing the error view
27+
// which triggers a request for Code Actions, which could result in analysis
28+
// of the file (which triggers errors to be sent, which triggers a refresh
29+
// of the error view... etc.!).
30+
if (this.lastErrorJson === notificationJson) {
31+
// TODO: Come up with a better fix than this!
32+
// log("Skipping error notification as it was the same as the previous one");
33+
return;
34+
}
35+
36+
const errors = notification.errors.filter((error) => error.severity === 'ERROR');
37+
if (errors.length) {
38+
const dartCode = fs.readFileSync(notification.file).toString();
39+
const file = notification.file.replace('.xml.dart', '.xml');
40+
const xmlDocument = await workspace.openTextDocument(Uri.file(file));
41+
const allMessages: string[] = [];
42+
const mappedErrors = errors
43+
.map((e) => DartDiagnosticProvider.createDiagnostic(dartCode, xmlDocument, e))
44+
.filter(a => {
45+
// remove duplicated diagnostics
46+
const res = !allMessages.find(m => m === a.message);
47+
if (res) {
48+
allMessages.push(a.message);
49+
}
50+
return res;
51+
});
52+
this.diagnostics.set(
53+
Uri.file(file),
54+
mappedErrors
55+
);
56+
}
57+
else {
58+
const file = notification.file.replace('.xml.dart', '.xml');
59+
this.diagnostics.set(Uri.file(file), []);
60+
}
61+
62+
this.lastErrorJson = notificationJson;
63+
}
64+
65+
public static createDiagnostic(dartCode: string, xmlDocument: TextDocument, error: as.AnalysisError): Diagnostic {
66+
const xmlWordRange = getXmlCodeWordLocation(xmlDocument, dartCode, error.location);
67+
const diag = new DartDiagnostic(
68+
xmlWordRange,
69+
error.message,
70+
DartDiagnosticProvider.getSeverity(error.severity, error.type),
71+
);
72+
diag.code = error.code;
73+
diag.source = "dart";
74+
diag.tags = DartDiagnosticProvider.getTags(error);
75+
diag.type = error.type;
76+
if (error.correction) {
77+
diag.message += `\n${error.correction}`;
78+
}
79+
return diag;
80+
}
81+
82+
public static getSeverity(severity: as.AnalysisErrorSeverity, type: as.AnalysisErrorType): DiagnosticSeverity {
83+
switch (severity) {
84+
case "ERROR":
85+
return DiagnosticSeverity.Error;
86+
case "WARNING":
87+
return DiagnosticSeverity.Warning;
88+
case "INFO":
89+
switch (type) {
90+
case "TODO":
91+
return DiagnosticSeverity.Information; // https://github.com/Microsoft/vscode/issues/48376
92+
default:
93+
return DiagnosticSeverity.Information;
94+
}
95+
default:
96+
throw new Error("Unknown severity type: " + severity);
97+
}
98+
}
99+
100+
public static getTags(error: as.AnalysisError): DiagnosticTag[] {
101+
const tags: DiagnosticTag[] = [];
102+
if (error.code === "dead_code" || error.code === "unused_local_variable" || error.code === "unused_import")
103+
tags.push(DiagnosticTag.Unnecessary);
104+
return tags;
105+
}
106+
107+
private flushResults(notification: as.AnalysisFlushResultsNotification) {
108+
this.lastErrorJson = undefined;
109+
const entries = notification.files.map<[Uri, Diagnostic[]]>((file) => [Uri.file(file), undefined]);
110+
this.diagnostics.set(entries);
111+
}
112+
}
113+
114+
export class DartDiagnostic extends Diagnostic {
115+
public type: string;
116+
}

src/dart/extension/providers/dart_hover_provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class DartHoverProvider implements HoverProvider {
1818
}
1919

2020
const dartDocument = await getDartDocument(xmlDocument);
21-
const dartOffset = await getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange, true);
21+
const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange, true);
2222

2323
if (dartOffset < 0) {
2424
return;

src/dart/extension/providers/dart_reference_provider.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class DartReferenceProvider implements ReferenceProvider, DefinitionProvi
1616
}
1717

1818
const dartDocument = await getDartDocument(xmlDocument);
19-
const dartOffset = await getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange);
19+
const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange);
2020
let dartPosition = null;
2121

2222
if (dartOffset < 0) {
@@ -57,7 +57,7 @@ export class DartReferenceProvider implements ReferenceProvider, DefinitionProvi
5757
}
5858

5959
const dartDocument = await getDartDocument(xmlDocument);
60-
const dartOffset = await getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange);
60+
const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange);
6161

6262
const resp = await this.analyzer.analysisGetNavigation({
6363
file: fsPath(dartDocument.uri),
@@ -76,10 +76,17 @@ export class DartReferenceProvider implements ReferenceProvider, DefinitionProvi
7676
if (target.startColumn === 0)
7777
target.startColumn = 1;
7878

79+
let file = resp.files[target.fileIndex];
80+
if (file.endsWith('.xml.dart')) {
81+
file = file.replace('.xml.dart', '.xml');
82+
target.startColumn = 1;
83+
target.startLine = 1;
84+
}
85+
7986
return {
8087
originSelectionRange: util.toRange(dartDocument, region.offset, region.length),
8188
targetRange: util.toRangeOnLine(target),
82-
targetUri: Uri.file(resp.files[target.fileIndex]),
89+
targetUri: Uri.file(file),
8390
} as DefinitionLink;
8491
});
8592
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, CodeActionProviderMetadata, DocumentSelector, Range, TextDocument } from "vscode";
2+
import * as as from "../../shared/analysis_server_types";
3+
import { Logger } from "../../shared/interfaces";
4+
import { fsPath } from "../../shared/vscode/utils";
5+
import { isAnalyzableAndInWorkspace } from "../utils";
6+
import { RankedCodeActionProvider } from "./ranking_code_action_provider";
7+
import { getDartDocument, getDartCodeWordIndex } from "../../utils";
8+
9+
export class FixCodeActionProvider implements RankedCodeActionProvider {
10+
constructor(private readonly logger: Logger, public readonly selector: DocumentSelector, private readonly analyzer: any) { }
11+
12+
public readonly rank = 1;
13+
14+
public readonly metadata: CodeActionProviderMetadata = {
15+
providedCodeActionKinds: [CodeActionKind.QuickFix],
16+
};
17+
18+
public async provideCodeActions(xmlDocument: TextDocument, xmlRange: Range, context: CodeActionContext, token: CancellationToken): Promise<CodeAction[] | undefined> {
19+
const dartDocument = await getDartDocument(xmlDocument);
20+
const dartOffset = await getDartCodeWordIndex(xmlDocument, dartDocument, xmlDocument.getWordRangeAtPosition(xmlRange.start));
21+
if (dartOffset === -1) {
22+
return undefined;
23+
}
24+
25+
const rangeStart = dartDocument.positionAt(dartOffset);
26+
27+
if (!isAnalyzableAndInWorkspace(dartDocument)) {
28+
return undefined;
29+
}
30+
31+
// If we were only asked for specific action types and that doesn't include
32+
// quickfix (which is all we supply), bail out.
33+
if (context && context.only && !context.only.contains(CodeActionKind.QuickFix)) {
34+
return undefined;
35+
}
36+
37+
try {
38+
const result = await this.analyzer.editGetFixes({
39+
file: fsPath(dartDocument.uri),
40+
offset: dartDocument.offsetAt(rangeStart),
41+
});
42+
43+
if (token && token.isCancellationRequested) {
44+
return;
45+
}
46+
47+
// Because fixes may be the same for multiple errors, we'll de-dupe them based on their edit.
48+
const allActions: { [key: string]: CodeAction } = {};
49+
50+
for (const errorFix of result.fixes) {
51+
for (const fix of errorFix.fixes) {
52+
allActions[JSON.stringify(fix.edits)] = this.convertResult(xmlDocument, fix, errorFix.error);
53+
}
54+
}
55+
56+
return Object.keys(allActions).map((a) => allActions[a]).filter(a => !!a);
57+
}
58+
catch (e) {
59+
this.logger.error(e);
60+
throw e;
61+
}
62+
}
63+
64+
private convertResult(document: TextDocument, change: as.SourceChange, error: as.AnalysisError): CodeAction {
65+
const title = change.message;
66+
67+
if (title.startsWith('Import library')) {
68+
this.buildImportNamespace(document, change);
69+
}
70+
else {
71+
return null;
72+
}
73+
74+
// const diagnostics = error ? [DartDiagnosticProvider.createDiagnostic(error)] : undefined;
75+
const action = new CodeAction(title, CodeActionKind.QuickFix);
76+
action.command = {
77+
arguments: [document, change],
78+
command: "_dart.applySourceChange",
79+
title,
80+
};
81+
// action.diagnostics = diagnostics;
82+
return action;
83+
}
84+
85+
private buildImportNamespace(document: TextDocument, change: as.SourceChange) {
86+
change.edits[0].edits[0].offset = document.getText().indexOf('>');
87+
change.edits[0].file = change.edits[0].file.replace('.xml.dart', '.xml');
88+
let replacement = change.edits[0].edits[0].replacement;
89+
replacement = replacement.substring(9, replacement.length - 2);
90+
const namespace = replacement.substring(replacement.lastIndexOf('/') + 1).replace('.dart', '').toLowerCase();
91+
change.edits[0].edits[0].replacement = '\n\txmlns:' + namespace + '="' + replacement + '"';
92+
}
93+
}

0 commit comments

Comments
 (0)