Skip to content

Commit

Permalink
Merge pull request #132866 from microsoft/hediet/autocomplete-presele…
Browse files Browse the repository at this point in the history
…ct-from-inline-suggestion

Use Inline Completions To Preselect Item In Autocomplete
  • Loading branch information
hediet authored Sep 14, 2021
2 parents a017ece + 0be69b3 commit 634e7b4
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 54 deletions.
22 changes: 22 additions & 0 deletions src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { commonPrefixLength, commonSuffixLength } from 'vs/base/common/strings';
import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
Expand Down Expand Up @@ -581,3 +582,24 @@ export async function provideInlineCompletions(
},
};
}

export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion): NormalizedInlineCompletion;
export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion | undefined): NormalizedInlineCompletion | undefined;
export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion | undefined): NormalizedInlineCompletion | undefined {
if (!inlineCompletion) {
return inlineCompletion;
}
const valueToReplace = model.getValueInRange(inlineCompletion.range);
const commonPrefixLen = commonPrefixLength(valueToReplace, inlineCompletion.text);
const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen;
const start = model.getPositionAt(startOffset);

const remainingValueToReplace = valueToReplace.substr(commonPrefixLen);
const commonSuffixLen = commonSuffixLength(remainingValueToReplace, inlineCompletion.text);
const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen));

return {
range: Range.fromPositions(start, end),
text: inlineCompletion.text.substr(commonPrefixLen, inlineCompletion.text.length - commonPrefixLen - commonSuffixLen),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { Range } from 'vs/editor/common/core/range';
import { CompletionItemInsertTextRule } from 'vs/editor/common/modes';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession';
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
import { minimizeInlineCompletion } from './inlineCompletionsModel';
import { NormalizedInlineCompletion, normalizedInlineCompletionsEquals } from './inlineCompletionToGhostText';
import { compareBy, compareByNumberAsc, findMinBy } from './utils';

export interface SuggestWidgetState {
/**
Expand Down Expand Up @@ -52,7 +54,10 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
return { selectedItemAsInlineCompletion: this._currentInlineCompletion };
}

constructor(private readonly editor: IActiveCodeEditor) {
constructor(
private readonly editor: IActiveCodeEditor,
private readonly suggestControllerPreselector: () => NormalizedInlineCompletion | undefined
) {
super();

// See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab
Expand All @@ -71,6 +76,31 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {

const suggestController = SuggestController.get(this.editor);
if (suggestController) {
this._register(suggestController.registerSelector({
priority: 100,
select: (model, pos, items) => {
const textModel = this.editor.getModel();
const preselectedMinimized = minimizeInlineCompletion(textModel, this.suggestControllerPreselector());
if (!preselectedMinimized) {
return -1;
}
const position = Position.lift(pos);

const result = findMinBy(
items
.map((item, index) => {
const completion = suggestionToInlineCompletion(suggestController, position, item, this.isShiftKeyPressed);
// Minimization normalizes ranges.
const minimized = minimizeInlineCompletion(textModel, completion);
const valid = minimized.range.equalsRange(preselectedMinimized.range) && preselectedMinimized.text.startsWith(minimized.text);
return { index, valid, length: minimized.text.length };
})
.filter(item => item.valid),
compareBy(s => s.length, compareByNumberAsc()));
return result ? result.index : - 1;
}
}));

let isBoundToSuggestWidget = false;
const bindToSuggestWidget = () => {
if (isBoundToSuggestWidget) {
Expand Down Expand Up @@ -133,7 +163,7 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
return suggestionToInlineCompletion(
suggestController,
this.editor.getPosition(),
focusedItem,
focusedItem.item,
this.isShiftKeyPressed
);
}
Expand All @@ -153,9 +183,8 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
}
}

function suggestionToInlineCompletion(suggestController: SuggestController, position: Position, suggestion: ISelectedSuggestion, toggleMode: boolean): NormalizedInlineCompletion {
const item = suggestion.item;

function suggestionToInlineCompletion(suggestController: SuggestController, position: Position, item: CompletionItem, toggleMode: boolean): NormalizedInlineCompletion {
// additionalTextEdits might not be resolved here, this could be problematic.
if (Array.isArray(item.completion.additionalTextEdits) && item.completion.additionalTextEdits.length > 0) {
// cannot represent additional text edits
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/modes';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel';
import { BaseGhostTextWidgetModel, GhostText } from './ghostText';
import { provideInlineCompletions, UpdateOperation } from './inlineCompletionsModel';
import { minimizeInlineCompletion, provideInlineCompletions, UpdateOperation } from './inlineCompletionsModel';
import { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText';
import { SuggestWidgetInlineCompletionProvider } from './suggestWidgetInlineCompletionProvider';

export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel {
private readonly suggestionInlineCompletionSource = this._register(new SuggestWidgetInlineCompletionProvider(this.editor));
private readonly suggestionInlineCompletionSource = this._register(
new SuggestWidgetInlineCompletionProvider(
this.editor,
// Use the first cache item (if any) as preselection.
() => this.cache.value?.completions[0]?.toLiveInlineCompletion()
)
);
private readonly updateOperation = this._register(new MutableDisposable<UpdateOperation>());
private readonly updateCacheSoon = this._register(new RunOnceScheduler(() => this.updateCache(), 50));

Expand Down Expand Up @@ -155,38 +159,3 @@ export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel {
function sum(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0);
}

export function lengthOfLongestCommonPrefix(str1: string, str2: string): number {
let i = 0;
while (i < str1.length && i < str2.length && str1[i] === str2[i]) {
i++;
}
return i;
}

export function lengthOfLongestCommonSuffix(str1: string, str2: string): number {
let i = 0;
while (i < str1.length && i < str2.length && str1[str1.length - i - 1] === str2[str2.length - i - 1]) {
i++;
}
return i;
}

export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion | undefined): NormalizedInlineCompletion | undefined {
if (!inlineCompletion) {
return inlineCompletion;
}
const valueToReplace = model.getValueInRange(inlineCompletion.range);
const commonPrefixLength = lengthOfLongestCommonPrefix(valueToReplace, inlineCompletion.text);
const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLength;
const start = model.getPositionAt(startOffset);

const remainingValueToReplace = valueToReplace.substr(commonPrefixLength);
const commonSuffixLength = lengthOfLongestCommonSuffix(remainingValueToReplace, inlineCompletion.text);
const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLength));

return {
range: Range.fromPositions(start, end),
text: inlineCompletion.text.substr(commonPrefixLength, inlineCompletion.text.length - commonPrefixLength - commonSuffixLength),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel';
import { minimizeInlineCompletion, SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel';
import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel';
import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/utils';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
Expand All @@ -31,6 +31,7 @@ import assert = require('assert');
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { minimizeInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';

suite('Suggest Widget Model', () => {
test('Active', async () => {
Expand Down
20 changes: 20 additions & 0 deletions src/vs/editor/contrib/inlineCompletions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,23 @@ export function createDisposableRef<T>(object: T, disposable?: IDisposable): IRe
dispose: () => disposable?.dispose(),
};
}

export type Comparator<T> = (a: T, b: T) => number;

export function compareBy<TItem, TCompareBy>(selector: (item: TItem) => TCompareBy, comparator: Comparator<TCompareBy>): Comparator<TItem> {
return (a, b) => comparator(selector(a), selector(b));
}

export function compareByNumberAsc<T>(): Comparator<number> {
return (a, b) => a - b;
}

export function findMinBy<T>(items: T[], comparator: Comparator<T>): T | undefined {
let min: T | undefined = undefined;
for (const item of items) {
if (min === undefined || comparator(item, min) < 0) {
min = item;
}
}
return min;
}
13 changes: 13 additions & 0 deletions src/vs/editor/contrib/suggest/suggest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,16 @@ export function showSimpleSuggestions(editor: ICodeEditor, suggestions: modes.Co
editor.getContribution<SuggestController>('editor.contrib.suggestController').triggerSuggest(new Set<modes.CompletionItemProvider>().add(_provider));
}, 0);
}

export interface ISuggestItemPreselector {
/**
* The preselector with highest priority is asked first.
*/
readonly priority: number;

/**
* Is called to preselect a suggest item.
* When -1 is returned, item preselectors with lower priority are asked.
*/
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number | -1;
}
45 changes: 43 additions & 2 deletions src/vs/editor/contrib/suggest/suggestController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ILogService } from 'vs/platform/log/common/log';
import { CompletionItem, Context as SuggestContext, suggestWidgetStatusbarMenu } from './suggest';
import { CompletionItem, Context as SuggestContext, ISuggestItemPreselector, suggestWidgetStatusbarMenu } from './suggest';
import { SuggestAlternatives } from './suggestAlternatives';
import { CommitCharacterController } from './suggestCommitCharacters';
import { State, SuggestModel } from './suggestModel';
Expand Down Expand Up @@ -112,6 +112,7 @@ export class SuggestController implements IEditorContribution {
private readonly _lineSuffix = new MutableDisposable<LineSuffix>();
private readonly _toDispose = new DisposableStore();
private readonly _overtypingCapturer: IdleValue<OvertypingCapturer>;
private readonly _selectors = new PriorityRegistry<ISuggestItemPreselector>(s => s.priority);

constructor(
editor: ICodeEditor,
Expand Down Expand Up @@ -223,7 +224,16 @@ export class SuggestController implements IEditorContribution {
}));
this._toDispose.add(this.model.onDidSuggest(e => {
if (!e.shy) {
let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);
let index = -1;
for (const selector of this._selectors.itemsOrderedByPriorityDesc) {
index = selector.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);
if (index !== -1) {
break;
}
}
if (index === -1) {
index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);
}
this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
}
}));
Expand Down Expand Up @@ -599,6 +609,37 @@ export class SuggestController implements IEditorContribution {
}
this.widget.value.stopForceRenderingAbove();
}

registerSelector(selector: ISuggestItemPreselector): IDisposable {
return this._selectors.register(selector);
}
}

class PriorityRegistry<T> {
private readonly _items = new Array<T>();

constructor(private readonly prioritySelector: (item: T) => number) { }

register(value: T): IDisposable {
if (this._items.indexOf(value) !== -1) {
throw new Error('Value is already registered');
}
this._items.push(value);
this._items.sort((s1, s2) => this.prioritySelector(s2) - this.prioritySelector(s1));

return {
dispose: () => {
const idx = this._items.indexOf(value);
if (idx >= 0) {
this._items.splice(idx, 1);
}
}
};
}

get itemsOrderedByPriorityDesc(): readonly T[] {
return this._items;
}
}

export class TriggerSuggestAction extends EditorAction {
Expand Down
16 changes: 10 additions & 6 deletions src/vs/editor/contrib/suggest/suggestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,21 @@ export const enum State {
Auto = 2
}

function shouldPreventQuickSuggest(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean {
function isSuggestPreviewEnabled(editor: ICodeEditor): boolean {
return editor.getOption(EditorOption.suggest).preview;
}

function shouldPreventQuickSuggest(editor: ICodeEditor, contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean {
return (
Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible'))
&& !Boolean(configurationService.getValue('editor.inlineSuggest.allowQuickSuggestions'))
&& !Boolean(configurationService.getValue('editor.inlineSuggest.allowQuickSuggestions') ?? isSuggestPreviewEnabled(editor))
);
}

function shouldPreventSuggestOnTriggerCharacters(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean {
function shouldPreventSuggestOnTriggerCharacters(editor: ICodeEditor, contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean {
return (
Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible'))
&& !Boolean(configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters'))
&& !Boolean(configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters') ?? isSuggestPreviewEnabled(editor))
);
}

Expand Down Expand Up @@ -231,7 +235,7 @@ export class SuggestModel implements IDisposable {

const checkTriggerCharacter = (text?: string) => {

if (shouldPreventSuggestOnTriggerCharacters(this._contextKeyService, this._configurationService)) {
if (shouldPreventSuggestOnTriggerCharacters(this._editor, this._contextKeyService, this._configurationService)) {
return;
}

Expand Down Expand Up @@ -378,7 +382,7 @@ export class SuggestModel implements IDisposable {
}
}

if (shouldPreventQuickSuggest(this._contextKeyService, this._configurationService)) {
if (shouldPreventQuickSuggest(this._editor, this._contextKeyService, this._configurationService)) {
// do not trigger quick suggestions if inline suggestions are shown
return;
}
Expand Down

0 comments on commit 634e7b4

Please sign in to comment.