Bring existing items by provider to the suggest-call, refactor trigger options into one argument-object, simpily completionModel
This commit is contained in:
Johannes Rieken 2022-12-06 18:04:26 +01:00 committed by GitHub
parent 3b4ff94607
commit d08acd8f1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 116 deletions

View file

@ -46,7 +46,8 @@ export class CompletionModel {
private _lineContext: LineContext;
private _refilterKind: Refilter;
private _filteredItems?: StrictCompletionItem[];
private _providerInfo?: Map<CompletionItemProvider, boolean>;
private _itemsByProvider?: Map<CompletionItemProvider, CompletionItem[]>;
private _stats?: ICompletionStats;
constructor(
@ -92,40 +93,22 @@ export class CompletionModel {
return this._filteredItems!;
}
get allProvider(): IterableIterator<CompletionItemProvider> {
getItemsByProvider(): ReadonlyMap<CompletionItemProvider, CompletionItem[]> {
this._ensureCachedState();
return this._providerInfo!.keys();
return this._itemsByProvider!;
}
get incomplete(): Set<CompletionItemProvider> {
getIncompleteProvider(): Set<CompletionItemProvider> {
this._ensureCachedState();
const result = new Set<CompletionItemProvider>();
for (const [provider, incomplete] of this._providerInfo!) {
if (incomplete) {
for (const [provider, items] of this.getItemsByProvider()) {
if (items.length > 0 && items[0].container.incomplete) {
result.add(provider);
}
}
return result;
}
adopt(except: Set<CompletionItemProvider>): CompletionItem[] {
const res: CompletionItem[] = [];
for (let i = 0; i < this._items.length;) {
if (!except.has(this._items[i].provider)) {
res.push(this._items[i]);
// unordered removed
this._items[i] = this._items[this._items.length - 1];
this._items.pop();
} else {
// continue with next item
i++;
}
}
this._refilterKind = Refilter.All;
return res;
}
get stats(): ICompletionStats {
this._ensureCachedState();
return this._stats!;
@ -139,7 +122,7 @@ export class CompletionModel {
private _createCachedState(): void {
this._providerInfo = new Map();
this._itemsByProvider = new Map();
const labelLengths: number[] = [];
@ -164,8 +147,13 @@ export class CompletionModel {
continue; // SKIP invalid items
}
// collect all support, know if their result is incomplete
this._providerInfo.set(item.provider, Boolean(item.container.incomplete));
// keep all items by their provider
const arr = this._itemsByProvider.get(item.provider);
if (arr) {
arr.push(item);
} else {
this._itemsByProvider.set(item.provider, [item]);
}
// 'word' is that remainder of the current line that we
// filter and score against. In theory each suggestion uses a

View file

@ -167,6 +167,7 @@ export class CompletionOptions {
readonly snippetSortOrder = SnippetSortOrder.Bottom,
readonly kindFilter = new Set<languages.CompletionItemKind>(),
readonly providerFilter = new Set<languages.CompletionItemProvider>(),
readonly providerItemsToReuse: ReadonlyMap<languages.CompletionItemProvider, CompletionItem[]> = new Map<languages.CompletionItemProvider, CompletionItem[]>(),
readonly showDeprecated = true
) { }
}
@ -281,6 +282,14 @@ export async function provideSuggestionItems(
// for each support in the group ask for suggestions
let didAddResult = false;
await Promise.all(providerGroup.map(async provider => {
// we have items from a previous session that we can reuse
if (options.providerItemsToReuse.has(provider)) {
const items = options.providerItemsToReuse.get(provider)!;
items.forEach(item => result.push(item));
didAddResult = didAddResult || items.length > 0;
return;
}
// check if this provider is filtered out
if (options.providerFilter.size > 0 && !options.providerFilter.has(provider)) {
return;
}

View file

@ -405,7 +405,7 @@ export class SuggestController implements IEditorContribution {
if (item.completion.command) {
if (item.completion.command.id === TriggerSuggestAction.id) {
// retigger
this.model.trigger({ auto: true, shy: false }, true);
this.model.trigger({ auto: true, retrigger: true });
} else {
// exec command, done
tasks.push(this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])).catch(onUnexpectedError));
@ -500,7 +500,10 @@ export class SuggestController implements IEditorContribution {
triggerSuggest(onlyFrom?: Set<CompletionItemProvider>, auto?: boolean, noFilter?: boolean): void {
if (this.editor.hasModel()) {
this.model.trigger({ auto: auto ?? false, shy: false }, false, onlyFrom, undefined, noFilter);
this.model.trigger({
auto: auto ?? false,
completionOptions: { providerFilter: onlyFrom, kindFilter: noFilter ? new Set() : undefined }
});
this.editor.revealPosition(this.editor.getPosition(), ScrollType.Smooth);
this.editor.focus();
}

View file

@ -57,7 +57,7 @@ class InlineCompletionResults extends RefCountedDisposable implements InlineComp
&& this.line === line
&& this.word.word.length > 0
&& this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word
&& this.completionModel.incomplete.size === 0; // no incomplete results
&& this.completionModel.getIncompleteProvider().size === 0; // no incomplete results
}
get items(): SuggestInlineCompletion[] {

View file

@ -25,7 +25,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILogService } from 'vs/platform/log/common/log';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CompletionModel } from './completionModel';
import { CompletionDurations, CompletionItem, CompletionOptions, getSnippetSuggestSupport, getSuggestionComparator, provideSuggestionItems, QuickSuggestionsOptions, SnippetSortOrder } from './suggest';
import { CompletionDurations, CompletionItem, CompletionOptions, getSnippetSuggestSupport, provideSuggestionItems, QuickSuggestionsOptions, SnippetSortOrder } from './suggest';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { FuzzyScoreOptions } from 'vs/base/common/filters';
@ -48,11 +48,14 @@ export interface ISuggestEvent {
readonly shy: boolean;
}
export interface SuggestTriggerContext {
export interface SuggestTriggerOptions {
readonly auto: boolean;
readonly shy: boolean;
readonly shy?: boolean;
readonly retrigger?: boolean;
readonly triggerKind?: CompletionTriggerKind;
readonly triggerCharacter?: string;
readonly clipboardText?: string;
completionOptions?: Partial<CompletionOptions>;
}
export class LineContext {
@ -85,13 +88,13 @@ export class LineContext {
readonly auto: boolean;
readonly shy: boolean;
constructor(model: ITextModel, position: Position, auto: boolean, shy: boolean) {
constructor(model: ITextModel, position: Position, auto: boolean, shy: boolean | undefined) {
this.leadingLineContent = model.getLineContent(position.lineNumber).substr(0, position.column - 1);
this.leadingWord = model.getWordUntilPosition(position);
this.lineNumber = position.lineNumber;
this.column = position.column;
this.auto = auto;
this.shy = shy;
this.shy = shy ?? false;
}
}
@ -276,12 +279,25 @@ export class SuggestModel implements IDisposable {
const supports = supportsByTriggerCharacter.get(lastChar);
if (supports) {
// keep existing items that where not computed by the
// supports/providers that want to trigger now
const existing = this._completionModel
? { items: this._completionModel.adopt(supports), clipboardText: this._completionModel.clipboardText }
: undefined;
this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, existing);
const providerItemsToReuse = new Map<CompletionItemProvider, CompletionItem[]>();
if (this._completionModel) {
for (const [provider, items] of this._completionModel.getItemsByProvider()) {
if (!supports.has(provider)) {
providerItemsToReuse.set(provider, items);
}
}
}
this.trigger({
auto: true,
triggerCharacter: lastChar,
retrigger: Boolean(this._completionModel),
clipboardText: this._completionModel?.clipboardText,
completionOptions: { providerFilter: supports, providerItemsToReuse }
});
}
};
@ -316,7 +332,7 @@ export class SuggestModel implements IDisposable {
if (!this._editor.hasModel() || !this._languageFeaturesService.completionProvider.has(this._editor.getModel())) {
this.cancel();
} else {
this.trigger({ auto: this._state === State.Auto, shy: false }, true);
this.trigger({ auto: this._state === State.Auto, retrigger: true });
}
}
}
@ -415,7 +431,7 @@ export class SuggestModel implements IDisposable {
}
// we made it till here -> trigger now
this.trigger({ auto: true, shy: false });
this.trigger({ auto: true });
}, this._editor.getOption(EditorOption.quickSuggestionsDelay));
}
@ -429,29 +445,29 @@ export class SuggestModel implements IDisposable {
this._onNewContext(ctx);
}
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set<CompletionItemProvider>, existing?: { items: CompletionItem[]; clipboardText: string | undefined }, noFilter?: boolean): void {
trigger(options: SuggestTriggerOptions): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const auto = context.auto;
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
const auto = options.auto;
const ctx = new LineContext(model, this._editor.getPosition(), auto, options.shy);
// Cancel previous requests, change state & update UI
this.cancel(retrigger);
this.cancel(options.retrigger);
this._state = auto ? State.Auto : State.Manual;
this._onDidTrigger.fire({ auto, shy: context.shy, position: this._editor.getPosition() });
this._onDidTrigger.fire({ auto, shy: options.shy ?? false, position: this._editor.getPosition() });
// Capture context when request was sent
this._context = ctx;
// Build context for request
let suggestCtx: CompletionContext = { triggerKind: context.triggerKind ?? CompletionTriggerKind.Invoke };
if (context.triggerCharacter) {
let suggestCtx: CompletionContext = { triggerKind: options.triggerKind ?? CompletionTriggerKind.Invoke };
if (options.triggerCharacter) {
suggestCtx = {
triggerKind: CompletionTriggerKind.TriggerCharacter,
triggerCharacter: context.triggerCharacter
triggerCharacter: options.triggerCharacter
};
}
@ -474,7 +490,7 @@ export class SuggestModel implements IDisposable {
}
const { itemKind: itemKindFilter, showDeprecated } = SuggestModel._createSuggestFilter(this._editor);
const completionOptions = new CompletionOptions(snippetSortOrder, !noFilter ? itemKindFilter : new Set(), onlyFrom, showDeprecated);
const completionOptions = new CompletionOptions(snippetSortOrder, options.completionOptions?.kindFilter ?? itemKindFilter, options.completionOptions?.providerFilter, options.completionOptions?.providerItemsToReuse, showDeprecated);
const wordDistance = WordDistance.create(this._editorWorkerService, this._editor);
const completions = provideSuggestionItems(
@ -494,7 +510,7 @@ export class SuggestModel implements IDisposable {
return;
}
let clipboardText = existing?.clipboardText;
let clipboardText = options?.clipboardText;
if (!clipboardText && completions.needsClipboard) {
clipboardText = await this._clipboardService.readText();
}
@ -504,19 +520,19 @@ export class SuggestModel implements IDisposable {
}
const model = this._editor.getModel();
let items = completions.items;
// const items = completions.items;
if (existing) {
const cmpFn = getSuggestionComparator(snippetSortOrder);
items = items.concat(existing.items).sort(cmpFn);
}
// if (existing) {
// const cmpFn = getSuggestionComparator(snippetSortOrder);
// items = items.concat(existing.items).sort(cmpFn);
// }
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
const ctx = new LineContext(model, this._editor.getPosition(), auto, options.shy);
const fuzzySearchOptions = {
...FuzzyScoreOptions.default,
firstMatchCanBeWeak: !this._editor.getOption(EditorOption.suggest).matchOnWordStartOnly
};
this._completionModel = new CompletionModel(items, this._context!.column, {
this._completionModel = new CompletionModel(completions.items, this._context!.column, {
leadingLineContent: ctx.leadingLineContent,
characterCountDelta: ctx.column - this._context!.column
},
@ -625,7 +641,7 @@ export class SuggestModel implements IDisposable {
if (ctx.column < this._context.column) {
// typed -> moved cursor LEFT -> retrigger if still on a word
if (ctx.leadingWord.word) {
this.trigger({ auto: this._context.auto, shy: false }, true);
this.trigger({ auto: this._context.auto, retrigger: true });
} else {
this.cancel();
}
@ -638,24 +654,37 @@ export class SuggestModel implements IDisposable {
}
if (ctx.leadingWord.word.length !== 0 && ctx.leadingWord.startColumn > this._context.leadingWord.startColumn) {
// started a new word while IntelliSense shows -> retrigger
// Select those providers have not contributed to this completion model and re-trigger completions for
// them. Also adopt the existing items and merge them into the new completion model
const inactiveProvider = new Set(this._languageFeaturesService.completionProvider.all(this._editor.getModel()!));
for (const provider of this._completionModel.allProvider) {
inactiveProvider.delete(provider);
}
const items = this._completionModel.adopt(new Set());
this.trigger({ auto: this._context.auto, shy: false }, true, inactiveProvider, { items, clipboardText: this._completionModel.clipboardText });
// started a new word while IntelliSense shows -> retrigger but reuse all items that we currently have
const map = this._completionModel.getItemsByProvider();
this.trigger({
auto: this._context.auto,
retrigger: true,
clipboardText: this._completionModel.clipboardText,
completionOptions: { providerItemsToReuse: map }
});
return;
}
if (ctx.column > this._context.column && this._completionModel.incomplete.size > 0 && ctx.leadingWord.word.length !== 0) {
if (ctx.column > this._context.column && this._completionModel.getIncompleteProvider().size > 0 && ctx.leadingWord.word.length !== 0) {
// typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger
const { incomplete } = this._completionModel;
const items = this._completionModel.adopt(incomplete);
this.trigger({ auto: this._state === State.Auto, shy: false, triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions }, true, incomplete, { items, clipboardText: this._completionModel.clipboardText });
const providerItemsToReuse = new Map<CompletionItemProvider, CompletionItem[]>();
const providerFilter = new Set<CompletionItemProvider>();
for (const [provider, items] of this._completionModel.getItemsByProvider()) {
if (items.length > 0 && items[0].container.incomplete) {
providerFilter.add(provider);
} else {
providerItemsToReuse.set(provider, items);
}
}
this.trigger({
auto: this._state === State.Auto,
triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions,
retrigger: true,
clipboardText: this._completionModel.clipboardText,
completionOptions: { providerFilter, providerItemsToReuse }
});
} else {
// typed -> moved cursor RIGHT -> update UI
@ -678,7 +707,7 @@ export class SuggestModel implements IDisposable {
if (shouldAutoTrigger && this._context.leadingWord.endColumn < ctx.leadingWord.startColumn) {
// retrigger when heading into a new word
this.trigger({ auto: this._context.auto, shy: false }, true);
this.trigger({ auto: this._context.auto, retrigger: true });
return;
}

View file

@ -102,7 +102,7 @@ suite('CompletionModel', function () {
test('complete/incomplete', () => {
assert.strictEqual(model.incomplete.size, 0);
assert.strictEqual(model.getIncompleteProvider().size, 0);
const incompleteModel = new CompletionModel([
createSuggestItem('foo', 3, undefined, true),
@ -111,25 +111,7 @@ suite('CompletionModel', function () {
leadingLineContent: 'foo',
characterCountDelta: 0
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
assert.strictEqual(incompleteModel.incomplete.size, 1);
});
test('replaceIncomplete', () => {
const completeItem = createSuggestItem('foobar', 1, undefined, false, { lineNumber: 1, column: 2 });
const incompleteItem = createSuggestItem('foofoo', 1, undefined, true, { lineNumber: 1, column: 2 });
const model = new CompletionModel([completeItem, incompleteItem], 2, { leadingLineContent: 'f', characterCountDelta: 0 }, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
assert.strictEqual(model.incomplete.size, 1);
assert.strictEqual(model.items.length, 2);
const { incomplete } = model;
const complete = model.adopt(incomplete);
assert.strictEqual(incomplete.size, 1);
assert.ok(incomplete.has(incompleteItem.provider));
assert.strictEqual(complete.length, 1);
assert.ok(complete[0] === completeItem);
assert.strictEqual(incompleteModel.getIncompleteProvider().size, 1);
});
test('Fuzzy matching of snippets stopped working with inline snippet suggestions #49895', function () {
@ -150,15 +132,8 @@ suite('CompletionModel', function () {
incompleteItem1,
], 2, { leadingLineContent: 'f', characterCountDelta: 0 }, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined
);
assert.strictEqual(model.incomplete.size, 1);
assert.strictEqual(model.getIncompleteProvider().size, 1);
assert.strictEqual(model.items.length, 6);
const { incomplete } = model;
const complete = model.adopt(incomplete);
assert.strictEqual(incomplete.size, 1);
assert.ok(incomplete.has(incompleteItem1.provider));
assert.strictEqual(complete.length, 5);
});
test('proper current word when length=0, #16380', function () {

View file

@ -253,7 +253,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return Promise.all([
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: true, shy: false });
model.trigger({ auto: true });
}, function (event) {
assert.strictEqual(event.auto, true);
@ -265,13 +265,13 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
}),
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: true, shy: false });
model.trigger({ auto: true });
}, function (event) {
assert.strictEqual(event.auto, true);
}),
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, function (event) {
assert.strictEqual(event.auto, false);
})
@ -287,12 +287,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle(model => {
return Promise.all([
assertEvent(model.onDidCancel, function () {
model.trigger({ auto: true, shy: false });
model.trigger({ auto: true });
}, function (event) {
assert.strictEqual(event.retrigger, false);
}),
assertEvent(model.onDidSuggest, function () {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, function (event) {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.isFrozen, false);
@ -343,7 +343,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return assertEvent(model.onDidSuggest, () => {
// make sure completionModel starts here!
model.trigger({ auto: true, shy: false });
model.trigger({ auto: true });
}, event => {
return assertEvent(model.onDidSuggest, () => {
@ -456,7 +456,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.setPosition({ lineNumber: 1, column: 3 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, event => {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.isFrozen, false);
@ -481,7 +481,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.setPosition({ lineNumber: 1, column: 3 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, event => {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.isFrozen, false);
@ -518,10 +518,10 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.setPosition({ lineNumber: 1, column: 4 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, event => {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.completionModel.incomplete.size, 1);
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
assert.strictEqual(event.completionModel.items.length, 1);
return assertEvent(model.onDidCancel, () => {
@ -555,10 +555,10 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.setPosition({ lineNumber: 1, column: 4 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false, shy: false });
model.trigger({ auto: false });
}, event => {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.completionModel.incomplete.size, 1);
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
assert.strictEqual(event.completionModel.items.length, 1);
return assertEvent(model.onDidSuggest, () => {
@ -568,7 +568,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.trigger('keyboard', Handler.Type, { text: ';' });
}, event => {
assert.strictEqual(event.auto, false);
assert.strictEqual(event.completionModel.incomplete.size, 1);
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
assert.strictEqual(event.completionModel.items.length, 1);
});
@ -714,7 +714,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
await assertEvent(sugget.onDidSuggest, () => {
editor.setPosition({ lineNumber: 1, column: 3 });
sugget.trigger({ auto: false, shy: false });
sugget.trigger({ auto: false });
}, event => {
assert.strictEqual(event.completionModel.items.length, 1);
@ -941,4 +941,59 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
});
});
});
test('Unexpected suggest scoring #167242', async function () {
disposables.add(registry.register('*', {
// word-based
provideCompletionItems(doc, pos) {
const word = doc.getWordUntilPosition(pos);
return {
suggestions: [{
kind: CompletionItemKind.Text,
label: 'pull',
insertText: 'pull',
range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn)
}],
};
}
}));
disposables.add(registry.register({ scheme: 'test' }, {
// JSON-based
provideCompletionItems(doc, pos) {
return {
suggestions: [{
kind: CompletionItemKind.Class,
label: 'git.pull',
insertText: 'git.pull',
range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)
}],
};
},
}));
return withOracle(async function (model, editor) {
await assertEvent(model.onDidSuggest, () => {
editor.setValue('gi');
editor.setSelection(new Selection(1, 3, 1, 3));
editor.trigger('keyboard', Handler.Type, { text: 't' });
}, event => {
assert.strictEqual(event.auto, true);
assert.strictEqual(event.completionModel.items.length, 1);
assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');
});
editor.trigger('keyboard', Handler.Type, { text: '.' });
await assertEvent(model.onDidSuggest, () => {
editor.trigger('keyboard', Handler.Type, { text: 'p' });
}, event => {
assert.strictEqual(event.auto, true);
assert.strictEqual(event.completionModel.items.length, 1);
assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');
});
});
});
});