11import * 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" ;
34import * as as from "../analysis/analysis_server_types" ;
45import { Analyzer } from "../analysis/analyzer" ;
6+ import { hasOverlappingEdits } from "../commands/edit" ;
57import { config } from "../config" ;
68import { cleanDartdoc } from "../dartdocs" ;
9+ import { flatMap , IAmDisposable } from "../debug/utils" ;
710import { 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