Skip to content

Commit 32b7d58

Browse files
committed
Basic implementation for adding imports during completions
Fixes #1060.
1 parent 6df308e commit 32b7d58

3 files changed

Lines changed: 184 additions & 5 deletions

File tree

‎src/analysis/analyzer_status_reporter.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export class AnalyzerStatusReporter {
2020
private analyzingPromise?: PromiseCompleter<void>;
2121

2222
constructor(private readonly analyzer: Analyzer, private readonly sdks: Sdks, private readonly analytics: Analytics) {
23+
// TODO: Should these go in disposables?
24+
// If so, do we need to worry about server cleaning them up if it diposes first?
2325
analyzer.registerForServerStatus((n) => this.handleServerStatus(n));
2426
analyzer.registerForServerError((e) => this.handleServerError(e));
2527
analyzer.registerForRequestError((e) => this.handleRequestError(e));

‎src/extension.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ export function activate(context: vs.ExtensionContext, isRestart: boolean = fals
304304
});
305305

306306
context.subscriptions.push(new OpenFileTracker(analyzer));
307+
308+
// Set up completions for unimported items.
309+
analyzer.completionSetSubscriptions({
310+
subscriptions: ["AVAILABLE_SUGGESTION_SETS"],
311+
});
307312
});
308313

309314
// Handle config changes so we can reanalyze if necessary.

‎src/providers/dart_completion_item_provider.ts‎

Lines changed: 177 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import * as path from "path";
2-
import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, MarkdownString, Position, Range, SnippetString, TextDocument } from "vscode";
2+
import * as vs from "vscode";
3+
import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, Disposable, MarkdownString, Position, Range, SnippetString, TextDocument } from "vscode";
34
import * as as from "../analysis/analysis_server_types";
45
import { Analyzer } from "../analysis/analyzer";
6+
import { hasOverlappingEdits } from "../commands/edit";
57
import { config } from "../config";
68
import { cleanDartdoc } from "../dartdocs";
9+
import { flatMap, IAmDisposable } from "../debug/utils";
710
import { fsPath } from "../utils";
11+
import { logError, logWarn } from "../utils/log";
812

9-
export class DartCompletionItemProvider implements CompletionItemProvider {
10-
constructor(private readonly analyzer: Analyzer) { }
13+
export class DartCompletionItemProvider implements CompletionItemProvider, IAmDisposable {
14+
private disposables: Disposable[] = [];
15+
private cachedCompletions: { [key: number]: as.AvailableSuggestion[] } = {};
16+
17+
constructor(private readonly analyzer: Analyzer) {
18+
this.disposables.push(analyzer.registerForCompletionAvailableSuggestions((n) => this.storeCompletionSuggestions(n)));
19+
}
1120

1221
public async provideCompletionItems(
1322
document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext,
@@ -26,7 +35,12 @@ export class DartCompletionItemProvider implements CompletionItemProvider {
2635
offset: document.offsetAt(position),
2736
});
2837

29-
return new CompletionList(resp.results.map((r) => this.convertResult(document, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, resp, r)));
38+
const includedResults = resp.results.map((r) => this.convertResult(document, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, resp, r));
39+
const cachedResults = this.getCachedResults(document, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, document.offsetAt(position), resp);
40+
41+
const allResults = [...includedResults, ...cachedResults];
42+
43+
return new CompletionList(allResults);
3044
}
3145

3246
private shouldAllowCompletion(line: string, context: CompletionContext): boolean {
@@ -68,8 +82,109 @@ export class DartCompletionItemProvider implements CompletionItemProvider {
6882
return true;
6983
}
7084

85+
private storeCompletionSuggestions(notification: as.CompletionAvailableSuggestionsNotification) {
86+
if (notification.changedLibraries) {
87+
for (const completionSet of notification.changedLibraries) {
88+
this.cachedCompletions[completionSet.id] = completionSet.items;
89+
}
90+
}
91+
if (notification.removedLibraries) {
92+
for (const completionSetID of notification.removedLibraries) {
93+
delete this.cachedCompletions[completionSetID];
94+
}
95+
}
96+
}
97+
98+
public async resolveCompletionItem?(item: DelayedCompletionItem, token: CancellationToken): Promise<CompletionItem> {
99+
if (!item.suggestion)
100+
return;
101+
102+
const suggestion: as.AvailableSuggestion = item.suggestion;
103+
104+
const res = await this.analyzer.completionGetSuggestionDetails({
105+
file: item.filePath,
106+
id: item.suggestionSetID,
107+
label: item.suggestion.label,
108+
offset: item.offset,
109+
});
110+
111+
const completionItem = this.makeCompletion(item.document, item.nextCharacter, item.enableCommitCharacters, item.insertArgumentPlaceholders, {
112+
completionText: res.completion,
113+
displayText: undefined,
114+
docSummary: suggestion.docSummary,
115+
elementKind: suggestion.element ? suggestion.element.kind : undefined,
116+
isDeprecated: false, // Not supported for unimported completions
117+
kind: "ARGUMENT_LIST", // TODO: NEED THIS???? NEED THIS???? NEED THIS???? NEED THIS???? NEED THIS???? NEED THIS???? NEED THIS????
118+
parameterNames: suggestion.parameterNames,
119+
parameterType: "TODO: PARAMETER TYPE??", // TODO
120+
parameters: suggestion.element ? suggestion.element.parameters : undefined,
121+
relevance: 0, // TODO: !!
122+
replacementLength: item.replacementLength,
123+
replacementOffset: item.replacementOffset,
124+
requiredParameterCount: suggestion.requiredParameterCount,
125+
returnType: suggestion.element ? suggestion.element.returnType : undefined,
126+
selectionLength: res.change && res.change.selection ? 0 : undefined,
127+
selectionOffset: res.change && res.change.selection ? res.change.selection.offset : undefined,
128+
});
129+
130+
// Additional edits for the imports.
131+
completionItem.additionalTextEdits = convertSimpleEdits(item.document, res.change);
132+
133+
return completionItem;
134+
}
135+
136+
private getCachedResults(
137+
document: TextDocument,
138+
nextCharacter: string,
139+
enableCommitCharacters: boolean,
140+
insertArgumentPlaceholders: boolean,
141+
offset: number,
142+
resp: as.CompletionResultsNotification,
143+
): CompletionItem[] {
144+
if (!resp.includedSuggestionSets)
145+
return;
146+
147+
const suggestionSetResults: CompletionItem[][] = [];
148+
for (let i = 0; i < resp.includedSuggestionSets.length; i++) {
149+
const suggestionSet = resp.includedSuggestionSets[i];
150+
const elementKinds = resp.includedElementKinds[i];
151+
// TODO: Use this somewhere?
152+
const relevanceTags = resp.includedSuggestionRelevanceTags[i];
153+
154+
const results = this.cachedCompletions[suggestionSet.id];
155+
if (!results) {
156+
logWarn(`Suggestion set ${suggestionSet.id} was not available and therefore not included in the completion results`);
157+
return;
158+
}
159+
160+
const unresolvedItems = results
161+
// TODO: How should we do this?
162+
.filter((r) => !elementKinds || elementKinds.indexOf(r.element.kind) !== -1)
163+
.map((r) => new DelayedCompletionItem(
164+
document,
165+
nextCharacter,
166+
enableCommitCharacters,
167+
insertArgumentPlaceholders,
168+
r,
169+
resp.replacementOffset,
170+
resp.replacementLength,
171+
offset,
172+
fsPath(document.uri),
173+
suggestionSet.id,
174+
));
175+
suggestionSetResults.push(unresolvedItems);
176+
}
177+
178+
return [].concat(...suggestionSetResults);
179+
}
180+
71181
private convertResult(
72-
document: TextDocument, nextCharacter: string, enableCommitCharacters: boolean, insertArgumentPlaceholders: boolean, notification: as.CompletionResultsNotification, suggestion: as.CompletionSuggestion,
182+
document: TextDocument,
183+
nextCharacter: string,
184+
enableCommitCharacters: boolean,
185+
insertArgumentPlaceholders: boolean,
186+
notification: as.CompletionResultsNotification,
187+
suggestion: as.CompletionSuggestion,
73188
): CompletionItem {
74189
return this.makeCompletion(document, nextCharacter, enableCommitCharacters, insertArgumentPlaceholders, {
75190
completionText: suggestion.completion,
@@ -313,4 +428,61 @@ export class DartCompletionItemProvider implements CompletionItemProvider {
313428
return [".", ",", "(", "["];
314429
}
315430
}
431+
432+
public dispose(): any {
433+
this.disposables.forEach((d) => d.dispose());
434+
}
435+
}
436+
437+
class DelayedCompletionItem extends CompletionItem {
438+
constructor(
439+
public readonly document: TextDocument,
440+
public readonly nextCharacter: string,
441+
public readonly enableCommitCharacters: boolean,
442+
public readonly insertArgumentPlaceholders: boolean,
443+
public readonly suggestion: as.AvailableSuggestion,
444+
public readonly replacementOffset: number,
445+
public readonly replacementLength: number,
446+
public readonly offset: number,
447+
public readonly filePath: string,
448+
public readonly suggestionSetID: number,
449+
kind?: CompletionItemKind) {
450+
super(suggestion.label, kind);
451+
}
452+
}
453+
454+
function convertSimpleEdits(document: vs.TextDocument, change: as.SourceChange | undefined): vs.TextEdit[] | undefined {
455+
if (!change)
456+
return undefined;
457+
458+
// VS Code expects offsets to be based on the original document, but the analysis server provides
459+
// them assuming all previous edits have already been made. This means if the server provides us a
460+
// set of edits where any edits offset is *equal to or greater than* a previous edit, it will do the wrong thing.
461+
// If this happens; we will fall back to sequential edits and write a warning.
462+
const hasProblematicEdits = hasOverlappingEdits(change);
463+
464+
if (hasProblematicEdits) {
465+
logError("Unable to insert imports because of overlapping edits from the server.");
466+
vs.window.showErrorMessage(`Unable to insert imports because of overlapping edits from the server`);
467+
return undefined;
468+
}
469+
470+
const filePath = fsPath(document.uri);
471+
472+
if (change.edits.find((e) => e.file !== filePath)) {
473+
logError("Unable to insert imports because of edits included other files.");
474+
vs.window.showErrorMessage(`Unable to insert imports because of edits included other files`);
475+
return undefined;
476+
}
477+
478+
return flatMap(change.edits, (edit) => {
479+
return edit.edits.map((edit) => {
480+
const range = new vs.Range(
481+
document.positionAt(edit.offset),
482+
document.positionAt(edit.offset + edit.length),
483+
);
484+
return new vs.TextEdit(range, edit.replacement);
485+
});
486+
});
487+
316488
}

0 commit comments

Comments
 (0)