Skip to content

Commit

Permalink
Implements #131940. The suggest widget is not being stopped from auto…
Browse files Browse the repository at this point in the history
…matically opening anymore if suggest preview is enabled. Instead, the first inline suggestion is used to preselect an item in the suggest widget.
  • Loading branch information
hediet committed Sep 10, 2021
1 parent 7de611f commit 2c99163
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 54 deletions.
37 changes: 37 additions & 0 deletions src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,40 @@ export async function provideInlineCompletions(
},
};
}

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

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): 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 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 @@ -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;
}
26 changes: 24 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 Set<ISuggestItemPreselector>();

constructor(
editor: ICodeEditor,
Expand Down Expand Up @@ -223,7 +224,18 @@ 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;
// Highest priority first
const selectors = [...this._selectors].sort((s1, s2) => s2.priority - s1.priority);
for (const s of selectors) {
index = s.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 +611,16 @@ export class SuggestController implements IEditorContribution {
}
this.widget.value.stopForceRenderingAbove();
}

registerSelector(selector: ISuggestItemPreselector): IDisposable {
if (this._selectors.has(selector)) {
throw new Error('Selector is already registered');
}
this._selectors.add(selector);
return toDisposable(() => {
this._selectors.delete(selector);
});
}
}

export class TriggerSuggestAction extends EditorAction {
Expand Down
20 changes: 14 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,25 @@ export const enum State {
Auto = 2
}

function shouldPreventQuickSuggest(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean {
function getDefault<T>(value: T | undefined, defaultValue: T): T {
return value === undefined ? defaultValue : value;
}

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(getDefault(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(getDefault(configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters'), isSuggestPreviewEnabled(editor)))
);
}

Expand Down Expand Up @@ -231,7 +239,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 +386,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 2c99163

Please sign in to comment.