@@ -7,7 +7,7 @@ import { compareUndefinedSmallest, numberComparator } from '../../../../../base/
7
7
import { findLastMax } from '../../../../../base/common/arraysFind.js' ;
8
8
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js' ;
9
9
import { equalsIfDefined , itemEquals } from '../../../../../base/common/equals.js' ;
10
- import { Disposable , IDisposable , MutableDisposable } from '../../../../../base/common/lifecycle.js' ;
10
+ import { Disposable , DisposableStore , IDisposable , MutableDisposable , toDisposable } from '../../../../../base/common/lifecycle.js' ;
11
11
import { derived , IObservable , IObservableWithChange , ITransaction , observableValue , recordChanges , transaction } from '../../../../../base/common/observable.js' ;
12
12
// eslint-disable-next-line local/code-no-deep-import-of-internal
13
13
import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js' ;
@@ -27,7 +27,7 @@ import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js
27
27
import { formatRecordableLogEntry , IRecordableEditorLogEntry , IRecordableLogEntry , StructuredLogger } from '../structuredLogger.js' ;
28
28
import { wait } from '../utils.js' ;
29
29
import { InlineSuggestionIdentity , InlineSuggestionItem } from './inlineSuggestionItem.js' ;
30
- import { InlineCompletionContextWithoutUuid , InlineCompletionEditorType , InlineCompletionProviderResult , provideInlineCompletions } from './provideInlineCompletions.js' ;
30
+ import { InlineCompletionContextWithoutUuid , InlineCompletionEditorType , provideInlineCompletions , runWhenCancelled } from './provideInlineCompletions.js' ;
31
31
32
32
export class InlineCompletionsSource extends Disposable {
33
33
private static _requestId = 0 ;
@@ -47,6 +47,7 @@ export class InlineCompletionsSource extends Disposable {
47
47
private readonly _textModel : ITextModel ,
48
48
private readonly _versionId : IObservableWithChange < number | null , IModelContentChangedEvent | undefined > ,
49
49
private readonly _debounceValue : IFeatureDebounceInformation ,
50
+ private readonly _cursorPosition : IObservable < Position > ,
50
51
@ILanguageConfigurationService private readonly _languageConfigurationService : ILanguageConfigurationService ,
51
52
@ILogService private readonly _logService : ILogService ,
52
53
@IConfigurationService private readonly _configurationService : IConfigurationService ,
@@ -105,7 +106,7 @@ export class InlineCompletionsSource extends Disposable {
105
106
106
107
private _log ( entry :
107
108
{ sourceId : string ; kind : 'start' ; requestId : number ; context : unknown } & IRecordableEditorLogEntry
108
- | { sourceId : string ; kind : 'end' ; error : unknown ; durationMs : number ; result : unknown ; requestId : number } & IRecordableLogEntry
109
+ | { sourceId : string ; kind : 'end' ; error : unknown ; durationMs : number ; result : unknown ; requestId : number ; didAllProvidersReturn : boolean } & IRecordableLogEntry
109
110
) {
110
111
if ( this . _loggingEnabled . get ( ) ) {
111
112
this . _logService . info ( formatRecordableLogEntry ( entry ) ) ;
@@ -116,7 +117,8 @@ export class InlineCompletionsSource extends Disposable {
116
117
private readonly _loadingCount ;
117
118
public readonly loading ;
118
119
119
- public fetch ( providers : InlineCompletionsProvider [ ] , position : Position , context : InlineCompletionContextWithoutUuid , activeInlineCompletion : InlineSuggestionIdentity | undefined , withDebounce : boolean , userJumpedToActiveCompletion : IObservable < boolean > , providerhasChangedCompletion : boolean , editorType : InlineCompletionEditorType ) : Promise < boolean > {
120
+ public fetch ( providers : InlineCompletionsProvider [ ] , context : InlineCompletionContextWithoutUuid , activeInlineCompletion : InlineSuggestionIdentity | undefined , withDebounce : boolean , userJumpedToActiveCompletion : IObservable < boolean > , providerhasChangedCompletion : boolean , editorType : InlineCompletionEditorType ) : Promise < boolean > {
121
+ const position = this . _cursorPosition . get ( ) ;
120
122
const request = new UpdateRequest ( position , context , this . _textModel . getVersionId ( ) ) ;
121
123
122
124
const target = context . selectedSuggestionInfo ? this . suggestWidgetInlineCompletions . get ( ) : this . inlineCompletions . get ( ) ;
@@ -134,6 +136,7 @@ export class InlineCompletionsSource extends Disposable {
134
136
135
137
const promise = ( async ( ) => {
136
138
this . _loadingCount . set ( this . _loadingCount . get ( ) + 1 , undefined ) ;
139
+ const store = new DisposableStore ( ) ;
137
140
try {
138
141
const recommendedDebounceValue = this . _debounceValue . get ( this . _textModel ) ;
139
142
const debounceValue = findLastMax (
@@ -158,67 +161,92 @@ export class InlineCompletionsSource extends Disposable {
158
161
}
159
162
160
163
const startTime = new Date ( ) ;
161
- let providerResult : InlineCompletionProviderResult | undefined = undefined ;
162
- let error : unknown = undefined ;
163
- try {
164
- providerResult = await provideInlineCompletions (
165
- providers ,
166
- position ,
167
- this . _textModel ,
168
- context ,
169
- editorType ,
170
- source . token ,
171
- this . _languageConfigurationService
172
- ) ;
173
- } catch ( e ) {
174
- error = e ;
175
- throw e ;
176
- } finally {
177
- if ( this . _loggingEnabled . get ( ) || this . _structuredFetchLogger . isEnabled . get ( ) ) {
178
- if ( source . token . isCancellationRequested || this . _store . isDisposed || this . _textModel . getVersionId ( ) !== request . versionId ) {
179
- error = 'canceled' ;
164
+ const providerResult = provideInlineCompletions ( providers , this . _cursorPosition . get ( ) , this . _textModel , context , editorType , this . _languageConfigurationService ) ;
165
+
166
+ runWhenCancelled ( source . token , ( ) => providerResult . cancelAndDispose ( { kind : 'tokenCancellation' } ) ) ;
167
+
168
+ let shouldStopEarly = false ;
169
+
170
+ const suggestions : InlineSuggestionItem [ ] = [ ] ;
171
+ for await ( const list of providerResult . lists ) {
172
+ if ( ! list ) {
173
+ continue ;
174
+ }
175
+ list . addRef ( ) ;
176
+ store . add ( toDisposable ( ( ) => list . removeRef ( list . inlineSuggestionsData . length === 0 ? { kind : 'empty' } : { kind : 'notTaken' } ) ) ) ;
177
+
178
+ for ( const item of list . inlineSuggestionsData ) {
179
+ if ( ! context . includeInlineEdits && ( item . isInlineEdit || item . showInlineEditMenu ) ) {
180
+ continue ;
180
181
}
181
- const result = providerResult ?. completions . map ( c => ( {
182
- range : c . range . toString ( ) ,
183
- text : c . insertText ,
184
- isInlineEdit : ! ! c . isInlineEdit ,
185
- source : c . source . provider . groupId ,
186
- } ) ) ;
187
- this . _log ( { sourceId : 'InlineCompletions.fetch' , kind : 'end' , requestId, durationMs : ( Date . now ( ) - startTime . getTime ( ) ) , error, result, time : Date . now ( ) } ) ;
182
+ if ( ! context . includeInlineCompletions && ! ( item . isInlineEdit || item . showInlineEditMenu ) ) {
183
+ continue ;
184
+ }
185
+
186
+ const i = InlineSuggestionItem . create ( item , this . _textModel ) ;
187
+ suggestions . push ( i ) ;
188
+ // Stop after first visible inline completion
189
+ if ( ! i . isInlineEdit && ! i . showInlineEditMenu && context . triggerKind === InlineCompletionTriggerKind . Automatic ) {
190
+ if ( i . isVisible ( this . _textModel , this . _cursorPosition . get ( ) ) ) {
191
+ shouldStopEarly = true ;
192
+ }
193
+ }
194
+ }
195
+
196
+ if ( shouldStopEarly ) {
197
+ break ;
198
+ }
199
+ }
200
+
201
+ providerResult . cancelAndDispose ( { kind : 'lostRace' } ) ;
202
+
203
+ if ( this . _loggingEnabled . get ( ) || this . _structuredFetchLogger . isEnabled . get ( ) ) {
204
+ const didAllProvidersReturn = providerResult . didAllProvidersReturn ;
205
+ let error : string | undefined = undefined ;
206
+ if ( source . token . isCancellationRequested || this . _store . isDisposed || this . _textModel . getVersionId ( ) !== request . versionId ) {
207
+ error = 'canceled' ;
188
208
}
209
+ const result = suggestions . map ( c => ( {
210
+ range : c . editRange . toString ( ) ,
211
+ text : c . insertText ,
212
+ isInlineEdit : ! ! c . isInlineEdit ,
213
+ source : c . source . provider . groupId ,
214
+ } ) ) ;
215
+ this . _log ( { sourceId : 'InlineCompletions.fetch' , kind : 'end' , requestId, durationMs : ( Date . now ( ) - startTime . getTime ( ) ) , error, result, time : Date . now ( ) , didAllProvidersReturn } ) ;
189
216
}
190
217
191
- if ( source . token . isCancellationRequested || this . _store . isDisposed || this . _textModel . getVersionId ( ) !== request . versionId || userJumpedToActiveCompletion . get ( ) /* In the meantime the user showed interest for the active completion so dont hide it */ ) {
192
- providerResult . dispose ( ) ;
218
+ if ( source . token . isCancellationRequested || this . _store . isDisposed || this . _textModel . getVersionId ( ) !== request . versionId
219
+ || userJumpedToActiveCompletion . get ( ) /* In the meantime the user showed interest for the active completion so dont hide it */ ) {
193
220
return false ;
194
221
}
195
222
196
223
const endTime = new Date ( ) ;
197
224
this . _debounceValue . update ( this . _textModel , endTime . getTime ( ) - startTime . getTime ( ) ) ;
198
225
226
+ const cursorPosition = this . _cursorPosition . get ( ) ;
199
227
this . _updateOperation . clear ( ) ;
200
228
transaction ( tx => {
201
- const v = this . _state . get ( ) ;
202
229
/** @description Update completions with provider result */
230
+ const v = this . _state . get ( ) ;
231
+
203
232
if ( context . selectedSuggestionInfo ) {
204
233
this . _state . set ( {
205
234
inlineCompletions : InlineCompletionsState . createEmpty ( ) ,
206
- suggestWidgetInlineCompletions : v . suggestWidgetInlineCompletions . createStateWithAppliedResults ( providerResult , request , this . _textModel , activeInlineCompletion ) ,
235
+ suggestWidgetInlineCompletions : v . suggestWidgetInlineCompletions . createStateWithAppliedResults ( suggestions , request , this . _textModel , cursorPosition , activeInlineCompletion ) ,
207
236
} , tx ) ;
208
237
} else {
209
238
this . _state . set ( {
210
- inlineCompletions : v . inlineCompletions . createStateWithAppliedResults ( providerResult , request , this . _textModel , activeInlineCompletion ) ,
239
+ inlineCompletions : v . inlineCompletions . createStateWithAppliedResults ( suggestions , request , this . _textModel , cursorPosition , activeInlineCompletion ) ,
211
240
suggestWidgetInlineCompletions : InlineCompletionsState . createEmpty ( ) ,
212
241
} , tx ) ;
213
242
}
214
243
215
- providerResult . dispose ( ) ;
216
244
v . inlineCompletions . dispose ( ) ;
217
245
v . suggestWidgetInlineCompletions . dispose ( ) ;
218
246
} ) ;
219
-
220
247
} finally {
221
248
this . _loadingCount . set ( this . _loadingCount . get ( ) - 1 , undefined ) ;
249
+ store . dispose ( ) ;
222
250
}
223
251
224
252
return true ;
@@ -348,28 +376,43 @@ class InlineCompletionsState extends Disposable {
348
376
return new InlineCompletionsState ( newInlineCompletions , this . request ) ;
349
377
}
350
378
351
- public createStateWithAppliedResults ( update : InlineCompletionProviderResult , request : UpdateRequest , textModel : ITextModel , itemToPreserve : InlineSuggestionIdentity | undefined ) : InlineCompletionsState {
352
- const items : InlineSuggestionItem [ ] = [ ] ;
379
+ public createStateWithAppliedResults ( updatedSuggestions : InlineSuggestionItem [ ] , request : UpdateRequest , textModel : ITextModel , cursorPosition : Position , itemIdToPreserve : InlineSuggestionIdentity | undefined ) : InlineCompletionsState {
380
+ let updatedItems : InlineSuggestionItem [ ] = [ ] ;
353
381
354
- for ( const item of update . completions ) {
355
- const i = InlineSuggestionItem . create ( item , textModel ) ;
382
+ let itemToPreserve : InlineSuggestionItem | undefined = undefined ;
383
+ if ( itemIdToPreserve ) {
384
+ const preserveCandidate = this . _findById ( itemIdToPreserve ) ;
385
+ if ( preserveCandidate ) {
386
+ const updatedSuggestionsHasItemToPreserve = updatedSuggestions . some ( i => i . hash === preserveCandidate . hash ) ;
387
+ if ( ! updatedSuggestionsHasItemToPreserve && preserveCandidate . canBeReused ( textModel , request . position ) ) {
388
+ itemToPreserve = preserveCandidate ;
389
+ }
390
+ }
391
+ }
392
+
393
+ const preferInlineCompletions = itemToPreserve
394
+ // itemToPreserve has precedence
395
+ ? ! itemToPreserve . isInlineEdit
396
+ // Otherwise: prefer inline completion if there is a visible one
397
+ : updatedSuggestions . some ( i => ! i . isInlineEdit && i . isVisible ( textModel , cursorPosition ) ) ;
398
+
399
+ for ( const i of updatedSuggestions ) {
356
400
const oldItem = this . _findByHash ( i . hash ) ;
357
401
if ( oldItem ) {
358
- items . push ( i . withIdentity ( oldItem . identity ) ) ;
402
+ updatedItems . push ( i . withIdentity ( oldItem . identity ) ) ;
359
403
oldItem . setEndOfLifeReason ( { kind : InlineCompletionEndOfLifeReasonKind . Ignored , userTypingDisagreed : false , supersededBy : i . getSourceCompletion ( ) } ) ;
360
404
} else {
361
- items . push ( i ) ;
405
+ updatedItems . push ( i ) ;
362
406
}
363
407
}
364
408
365
409
if ( itemToPreserve ) {
366
- const item = this . _findById ( itemToPreserve ) ;
367
- if ( item && ! update . has ( item . getSingleTextEdit ( ) ) && item . canBeReused ( textModel , request . position ) ) {
368
- items . unshift ( item ) ;
369
- }
410
+ updatedItems . unshift ( itemToPreserve ) ;
370
411
}
371
412
372
- return new InlineCompletionsState ( items , request ) ;
413
+ updatedItems = preferInlineCompletions ? updatedItems . filter ( i => ! i . isInlineEdit ) : updatedItems . filter ( i => i . isInlineEdit ) ;
414
+
415
+ return new InlineCompletionsState ( updatedItems , request ) ;
373
416
}
374
417
375
418
public clone ( ) : InlineCompletionsState {
0 commit comments