Merge pull request #131183 from microsoft/hediet/inline-completions-autocomplete

allows inline suggestion providers to interact with the suggest widget.
This commit is contained in:
Henning Dieterichs 2021-08-23 09:50:28 +02:00 committed by GitHub
commit 03ed03a373
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 664 additions and 420 deletions

View file

@ -691,6 +691,13 @@ export interface InlineCompletionContext {
* How the completion was triggered.
*/
readonly triggerKind: InlineCompletionTriggerKind;
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: IRange;
text: string;
}
export interface InlineCompletion {

View file

@ -23,3 +23,7 @@
opacity: 0;
font-size: 0;
}
.monaco-editor .ghost-text-decoration-preview {
font-style: italic;
}

View file

@ -100,6 +100,10 @@ export class GhostTextPart {
constructor(
readonly column: number,
readonly lines: readonly string[],
/**
* Indicates if this part is a preview of an inline suggestion when a suggestion is previewed.
*/
readonly preview: boolean,
) {
}

View file

@ -5,14 +5,15 @@
import { Disposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel';
import { InlineCompletionsModel, LiveInlineCompletions, SynchronizedInlineCompletionsCache } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { Emitter } from 'vs/base/common/event';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { createDisposableRef } from 'vs/editor/contrib/inlineCompletions/utils';
import { GhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostText';
import { InlineCompletionTriggerKind } from 'vs/editor/common/modes';
export abstract class DelegatingModel extends Disposable implements GhostTextWidgetModel {
private readonly onDidChangeEmitter = new Emitter<void>();
@ -65,8 +66,9 @@ export abstract class DelegatingModel extends Disposable implements GhostTextWid
* A ghost text model that is both driven by inline completions and the suggest widget.
*/
export class GhostTextModel extends DelegatingModel implements GhostTextWidgetModel {
public readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetAdapterModel(this.editor));
public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.commandService));
public readonly sharedCache = this._register(new SharedInlineCompletionCache());
public readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetPreviewModel(this.editor, this.sharedCache));
public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.sharedCache, this.commandService));
public get activeInlineCompletionsModel(): InlineCompletionsModel | undefined {
if (this.targetModel === this.inlineCompletionsModel) {
@ -129,3 +131,34 @@ export class GhostTextModel extends DelegatingModel implements GhostTextWidgetMo
return result !== undefined ? result : false;
}
}
export class SharedInlineCompletionCache extends Disposable {
private readonly onDidChangeEmitter = new Emitter<void>();
public readonly onDidChange = this.onDidChangeEmitter.event;
private readonly cache = this._register(new MutableDisposable<SynchronizedInlineCompletionsCache>());
public get value(): SynchronizedInlineCompletionsCache | undefined {
return this.cache.value;
}
public setValue(editor: IActiveCodeEditor,
completionsSource: LiveInlineCompletions,
triggerKind: InlineCompletionTriggerKind
) {
this.cache.value = new SynchronizedInlineCompletionsCache(
editor,
completionsSource,
() => this.onDidChangeEmitter.fire(),
triggerKind
);
}
public clearAndLeak(): SynchronizedInlineCompletionsCache | undefined {
return this.cache.clearAndLeak();
}
public clear() {
this.cache.clear();
}
}

View file

@ -116,6 +116,7 @@ export class GhostTextWidget extends Disposable {
inlineTexts.push({
column: part.column,
text: lines[0],
preview: part.preview,
});
lines = lines.slice(1);
} else {
@ -192,6 +193,7 @@ interface HiddenText {
interface InsertedInlineText {
column: number;
text: string;
preview: boolean;
}
class DecorationsWidget implements IDisposable {
@ -273,6 +275,7 @@ class DecorationsWidget implements IDisposable {
opacity,
color,
border,
fontWeight: p.preview ? 'bold' : 'normal',
},
}));
@ -280,7 +283,7 @@ class DecorationsWidget implements IDisposable {
range: Range.fromPositions(new Position(lineNumber, p.column)),
options: shouldUseInjectedText ? {
description: 'ghost-text',
after: { content: contentText, inlineClassName: 'ghost-text-decoration' }
after: { content: contentText, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' }
} : {
...decorationType.resolve()
}
@ -495,6 +498,7 @@ registerThemingParticipant((theme, collector) => {
const color = Color.Format.CSS.format(opaque(foreground))!;
collector.addRule(`.monaco-editor .ghost-text-decoration { opacity: ${opacity}; color: ${color}; }`);
collector.addRule(`.monaco-editor .ghost-text-decoration-preview { color: ${foreground.toString()}; }`);
collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { opacity: ${opacity}; color: ${color}; }`);
}

View file

@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { InlineCompletion } from 'vs/editor/common/modes';
import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff';
import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/ghostText';
export interface NormalizedInlineCompletion extends InlineCompletion {
range: Range;
}
export function inlineCompletionToGhostText(
inlineCompletion: NormalizedInlineCompletion,
textModel: ITextModel,
mode: 'prefix' | 'subword' | 'subwordSmart',
cursorPosition?: Position,
previewSuffixLength = 0
): GhostText | undefined {
if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
// Only single line replacements are supported.
return undefined;
}
const modifiedLength = inlineCompletion.text.length;
const previewStartInModified = modifiedLength - previewSuffixLength;
// This is a single line string
const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
const changes = cachingDiff(valueToBeReplaced, inlineCompletion.text);
const lineNumber = inlineCompletion.range.startLineNumber;
const parts = new Array<GhostTextPart>();
if (mode === 'prefix') {
const filteredChanges = changes.filter(c => c.originalLength === 0);
if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
// Prefixes only have a single change.
return undefined;
}
}
for (const c of changes) {
const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
// No ghost text before cursor
return undefined;
}
if (c.originalLength > 0) {
const originalText = valueToBeReplaced.substr(c.originalStart, c.originalLength);
const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(lineNumber);
if (!(/^(\t| )*$/.test(originalText) && (firstNonWsCol === 0 || insertColumn <= firstNonWsCol))) {
return undefined;
}
}
if (c.modifiedLength === 0) {
continue;
}
const modifiedEnd = c.modifiedStart + c.modifiedLength;
const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInModified));
const nonPreviewText = inlineCompletion.text.substring(c.modifiedStart, nonPreviewTextEnd);
const italicText = inlineCompletion.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
if (nonPreviewText.length > 0) {
const lines = strings.splitLines(nonPreviewText);
parts.push(new GhostTextPart(insertColumn, lines, false));
}
if (italicText.length > 0) {
const lines = strings.splitLines(italicText);
parts.push(new GhostTextPart(insertColumn, lines, true));
}
}
return new GhostText(lineNumber, parts, 0);
}
let lastRequest: { originalValue: string; newValue: string; changes: readonly IDiffChange[]; } | undefined = undefined;
function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] {
if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) {
return lastRequest?.changes;
} else {
const changes = smartDiff(originalValue, newValue);
lastRequest = {
originalValue,
newValue,
changes
};
return changes;
}
}
/**
* When matching `if ()` with `if (f() = 1) { g(); }`,
* align it like this: `if ( )`
* Not like this: `if ( )`
* Also not like this: `if ( )`.
*
* The parenthesis are preprocessed to ensure that they match correctly.
*/
function smartDiff(originalValue: string, newValue: string): readonly IDiffChange[] {
function getMaxCharCode(val: string): number {
let maxCharCode = 0;
for (let i = 0, len = val.length; i < len; i++) {
const charCode = val.charCodeAt(i);
if (charCode > maxCharCode) {
maxCharCode = charCode;
}
}
return maxCharCode;
}
const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
function getUniqueCharCode(id: number): number {
if (id < 0) {
throw new Error('unexpected');
}
return maxCharCode + id + 1;
}
function getElements(source: string): Int32Array {
let level = 0;
let group = 0;
const characters = new Int32Array(source.length);
for (let i = 0, len = source.length; i < len; i++) {
const id = group * 100 + level;
// TODO support more brackets
if (source[i] === '(') {
characters[i] = getUniqueCharCode(2 * id);
level++;
} else if (source[i] === ')') {
characters[i] = getUniqueCharCode(2 * id + 1);
if (level === 1) {
group++;
}
level = Math.max(level - 1, 0);
} else {
characters[i] = source.charCodeAt(i);
}
}
return characters;
}
const elements1 = getElements(originalValue);
const elements2 = getElements(newValue);
return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
}

View file

@ -5,23 +5,23 @@
import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff';
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 * as strings 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';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
import { GhostTextWidgetModel, GhostText, BaseGhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText';
import { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText';
import { inlineSuggestCommitId } from './consts';
import { BaseGhostTextWidgetModel, GhostText, GhostTextPart, GhostTextWidgetModel } from './ghostText';
import { SharedInlineCompletionCache } from './ghostTextModel';
export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel {
protected readonly onDidChangeEmitter = new Emitter<void>();
@ -34,7 +34,8 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
constructor(
private readonly editor: IActiveCodeEditor,
@ICommandService private readonly commandService: ICommandService
private readonly cache: SharedInlineCompletionCache,
@ICommandService private readonly commandService: ICommandService,
) {
super();
@ -126,7 +127,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
if (this.completionSession.value) {
return;
}
this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService);
this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService, this.cache);
this.completionSession.value.takeOwnership(
this.completionSession.value.onDidChange(() => {
this.onDidChangeEmitter.fire();
@ -162,16 +163,15 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
public readonly minReservedLineCount = 0;
private readonly updateOperation = this._register(new MutableDisposable<UpdateOperation>());
private readonly cache = this._register(new MutableDisposable<SynchronizedInlineCompletionsCache>());
private readonly updateSoon = this._register(new RunOnceScheduler(() => this.update(InlineCompletionTriggerKind.Automatic), 50));
private readonly textModel = this.editor.getModel();
constructor(
editor: IActiveCodeEditor,
private readonly triggerPosition: Position,
private readonly shouldUpdate: () => boolean,
private readonly commandService: ICommandService,
private readonly cache: SharedInlineCompletionCache,
) {
super(editor);
@ -195,24 +195,6 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
}));
this._register(this.editor.onDidChangeModelContent((e) => {
if (this.cache.value) {
let hasChanged = false;
for (const c of this.cache.value.completions) {
const newRange = this.textModel.getDecorationRange(c.decorationId);
if (!newRange) {
onUnexpectedError(new Error('Decoration has no range'));
continue;
}
if (!c.synchronizedRange.equalsRange(newRange)) {
hasChanged = true;
c.synchronizedRange = newRange;
}
}
if (hasChanged) {
this.onDidChangeEmitter.fire();
}
}
this.scheduleAutomaticUpdate();
}));
@ -311,14 +293,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
if (!completion) {
return undefined;
}
return {
text: completion.inlineCompletion.text,
range: completion.synchronizedRange,
command: completion.inlineCompletion.command,
sourceProvider: completion.inlineCompletion.sourceProvider,
sourceInlineCompletions: completion.inlineCompletion.sourceInlineCompletions,
sourceInlineCompletion: completion.inlineCompletion.sourceInlineCompletion,
};
return completion.toLiveInlineCompletion();
}
get isValid(): boolean {
@ -344,7 +319,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
try {
result = await provideInlineCompletions(position,
this.editor.getModel(),
{ triggerKind },
{ triggerKind, selectedSuggestionInfo: undefined },
token
);
} catch (e) {
@ -356,10 +331,9 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
return;
}
this.cache.value = new SynchronizedInlineCompletionsCache(
this.cache.setValue(
this.editor,
result,
() => this.onDidChangeEmitter.fire(),
triggerKind
);
this.onDidChangeEmitter.fire();
@ -414,7 +388,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
}
}
class UpdateOperation implements IDisposable {
export class UpdateOperation implements IDisposable {
constructor(public readonly promise: CancelablePromise<void>, public readonly triggerKind: InlineCompletionTriggerKind) {
}
@ -427,7 +401,7 @@ class UpdateOperation implements IDisposable {
* The cache keeps itself in sync with the editor.
* It also owns the completions result and disposes it when the cache is diposed.
*/
class SynchronizedInlineCompletionsCache extends Disposable {
export class SynchronizedInlineCompletionsCache extends Disposable {
public readonly completions: readonly CachedInlineCompletion[];
constructor(
@ -483,6 +457,7 @@ class CachedInlineCompletion {
startColumn: this.inlineCompletion.range.startColumn,
command: this.inlineCompletion.command
});
/**
* The range, synchronized with text model changes.
*/
@ -494,135 +469,19 @@ class CachedInlineCompletion {
) {
this.synchronizedRange = inlineCompletion.range;
}
}
export interface NormalizedInlineCompletion extends InlineCompletion {
range: Range;
}
export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel, mode: 'prefix' | 'subword' | 'subwordSmart', cursorPosition?: Position): GhostText | undefined {
if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
// Only single line replacements are supported.
return undefined;
}
// This is a single line string
const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
const changes = cachingDiff(valueToBeReplaced, inlineCompletion.text);
const lineNumber = inlineCompletion.range.startLineNumber;
const parts = new Array<GhostTextPart>();
if (mode === 'prefix') {
const filteredChanges = changes.filter(c => c.originalLength === 0);
if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
// Prefixes only have a single change.
return undefined;
}
}
for (const c of changes) {
const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
// No ghost text before cursor
return undefined;
}
if (c.originalLength > 0) {
const originalText = valueToBeReplaced.substr(c.originalStart, c.originalLength);
const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(lineNumber);
if (!(/^(\t| )*$/.test(originalText) && (firstNonWsCol === 0 || insertColumn <= firstNonWsCol))) {
return undefined;
}
}
if (c.modifiedLength === 0) {
continue;
}
const text = inlineCompletion.text.substr(c.modifiedStart, c.modifiedLength);
const lines = strings.splitLines(text);
parts.push(new GhostTextPart(insertColumn, lines));
}
return new GhostText(lineNumber, parts, 0);
}
let lastRequest: { originalValue: string, newValue: string, changes: readonly IDiffChange[] } | undefined = undefined;
function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] {
if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) {
return lastRequest?.changes;
} else {
const changes = smartDiff(originalValue, newValue);
lastRequest = {
originalValue,
newValue,
changes
public toLiveInlineCompletion(): LiveInlineCompletion | undefined {
return {
text: this.inlineCompletion.text,
range: this.synchronizedRange,
command: this.inlineCompletion.command,
sourceProvider: this.inlineCompletion.sourceProvider,
sourceInlineCompletions: this.inlineCompletion.sourceInlineCompletions,
sourceInlineCompletion: this.inlineCompletion.sourceInlineCompletion,
};
return changes;
}
}
/**
* When matching `if ()` with `if (f() = 1) { g(); }`,
* align it like this: `if ( )`
* Not like this: `if ( )`
* Also not like this: `if ( )`.
*
* The parenthesis are preprocessed to ensure that they match correctly.
*/
function smartDiff(originalValue: string, newValue: string): readonly IDiffChange[] {
function getMaxCharCode(val: string): number {
let maxCharCode = 0;
for (let i = 0, len = val.length; i < len; i++) {
const charCode = val.charCodeAt(i);
if (charCode > maxCharCode) {
maxCharCode = charCode;
}
}
return maxCharCode;
}
const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
function getUniqueCharCode(id: number): number {
if (id < 0) {
throw new Error('unexpected');
}
return maxCharCode + id + 1;
}
function getElements(source: string): Int32Array {
let level = 0;
let group = 0;
const characters = new Int32Array(source.length);
for (let i = 0, len = source.length; i < len; i++) {
const id = group * 100 + level;
// TODO support more brackets
if (source[i] === '(') {
characters[i] = getUniqueCharCode(2 * id);
level++;
} else if (source[i] === ')') {
characters[i] = getUniqueCharCode(2 * id + 1);
if (level === 1) {
group++;
}
level = Math.max(level - 1, 0);
} else {
characters[i] = source.charCodeAt(i);
}
}
return characters;
}
const elements1 = getElements(originalValue);
const elements2 = getElements(newValue);
return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
}
export interface LiveInlineCompletion extends NormalizedInlineCompletion {
sourceProvider: InlineCompletionsProvider;
sourceInlineCompletion: InlineCompletion;
@ -646,7 +505,7 @@ function getDefaultRange(position: Position, model: ITextModel): Range {
: Range.fromPositions(position, position.with(undefined, maxColumn));
}
async function provideInlineCompletions(
export async function provideInlineCompletions(
position: Position,
model: ITextModel,
context: InlineCompletionContext,

View file

@ -1,231 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CompletionItemInsertTextRule } from 'vs/editor/common/modes';
import { BaseGhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostText';
import { inlineCompletionToGhostText, NormalizedInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
export class SuggestWidgetAdapterModel extends BaseGhostTextWidgetModel {
private isSuggestWidgetVisible: boolean = false;
private currentGhostText: GhostText | undefined = undefined;
private _isActive: boolean = false;
private isShiftKeyPressed = false;
private currentCompletion: NormalizedInlineCompletion | undefined;
public override minReservedLineCount: number = 0;
public get isActive() { return this._isActive; }
// This delay fixes an suggest widget issue when typing "." immediately restarts the suggestion session.
private setInactiveDelayed = this._register(new RunOnceScheduler(() => {
if (!this.isSuggestWidgetVisible) {
if (this.isActive) {
this._isActive = false;
this.onDidChangeEmitter.fire();
}
}
}, 100));
constructor(
editor: IActiveCodeEditor
) {
super(editor);
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
let isBoundToSuggestWidget = false;
const bindToSuggestWidget = () => {
if (isBoundToSuggestWidget) {
return;
}
isBoundToSuggestWidget = true;
this._register(suggestController.widget.value.onDidShow(() => {
this.isSuggestWidgetVisible = true;
this._isActive = true;
this.updateFromSuggestion();
}));
this._register(suggestController.widget.value.onDidHide(() => {
this.isSuggestWidgetVisible = false;
this.setInactiveDelayed.schedule();
this.minReservedLineCount = 0;
this.updateFromSuggestion();
}));
this._register(suggestController.widget.value.onDidFocus(() => {
this.isSuggestWidgetVisible = true;
this._isActive = true;
this.updateFromSuggestion();
}));
};
this._register(Event.once(suggestController.model.onDidTrigger)(e => {
bindToSuggestWidget();
}));
}
this.updateFromSuggestion();
this._register(this.editor.onDidChangeCursorPosition((e) => {
if (this.isSuggestionPreviewEnabled()) {
this.minReservedLineCount = 0;
this.update();
}
}));
this._register(toDisposable(() => {
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
suggestController.stopForceRenderingAbove();
}
}));
// See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab
this._register(editor.onKeyDown(e => {
if (e.shiftKey && !this.isShiftKeyPressed) {
this.isShiftKeyPressed = true;
this.updateFromSuggestion();
}
}));
this._register(editor.onKeyUp(e => {
if (e.shiftKey && this.isShiftKeyPressed) {
this.isShiftKeyPressed = false;
this.updateFromSuggestion();
}
}));
}
public override setExpanded(expanded: boolean): void {
super.setExpanded(expanded);
this.updateFromSuggestion();
}
private isSuggestionPreviewEnabled(): boolean {
const suggestOptions = this.editor.getOption(EditorOption.suggest);
return suggestOptions.preview;
}
private updateFromSuggestion(): void {
const suggestController = SuggestController.get(this.editor);
if (!suggestController) {
this.setCurrentInlineCompletion(undefined);
return;
}
if (!this.isSuggestWidgetVisible) {
this.setCurrentInlineCompletion(undefined);
return;
}
const focusedItem = suggestController.widget.value.getFocusedItem();
if (!focusedItem) {
this.setCurrentInlineCompletion(undefined);
return;
}
// TODO: item.isResolved
this.setCurrentInlineCompletion(
getInlineCompletion(
suggestController,
this.editor.getPosition(),
focusedItem,
this.isShiftKeyPressed
)
);
}
private setCurrentInlineCompletion(completion: NormalizedInlineCompletion | undefined): void {
this.currentCompletion = completion;
this.update();
}
private update(): void {
const completion = this.currentCompletion;
const mode = this.editor.getOptions().get(EditorOption.suggest).previewMode;
this.setGhostText(
completion
? (
inlineCompletionToGhostText(completion, this.editor.getModel(), mode, this.editor.getPosition()) ||
// Show an invisible ghost text to reserve space
new GhostText(completion.range.endLineNumber, [], this.minReservedLineCount)
)
: undefined
);
}
private setGhostText(newGhostText: GhostText | undefined): void {
if (GhostText.equals(this.currentGhostText, newGhostText)) {
return;
}
this.currentGhostText = newGhostText;
if (this.currentGhostText && this.expanded) {
function sum(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0);
}
this.minReservedLineCount = Math.max(this.minReservedLineCount, sum(this.currentGhostText.parts.map(p => p.lines.length - 1)));
}
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
if (this.minReservedLineCount >= 1 && this.isSuggestionPreviewEnabled()) {
suggestController.forceRenderingAbove();
} else {
suggestController.stopForceRenderingAbove();
}
}
this.onDidChangeEmitter.fire();
}
public override get ghostText(): GhostText | undefined {
return this.isSuggestionPreviewEnabled()
? this.currentGhostText
: undefined;
}
}
function getInlineCompletion(suggestController: SuggestController, position: Position, suggestion: ISelectedSuggestion, toggleMode: boolean): NormalizedInlineCompletion {
const item = suggestion.item;
if (Array.isArray(item.completion.additionalTextEdits)) {
// cannot represent additional text edits
return {
text: '',
range: Range.fromPositions(position, position),
};
}
let { insertText } = item.completion;
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
const snippet = new SnippetParser().parse(insertText);
const model = suggestController.editor.getModel()!;
SnippetSession.adjustWhitespace(
model, position, snippet,
true,
true
);
insertText = snippet.toString();
}
const info = suggestController.getOverwriteInfo(item, toggleMode);
return {
text: insertText,
range: Range.fromPositions(
position.delta(0, -info.overwriteBefore),
position.delta(0, Math.max(info.overwriteAfter, 0))
),
};
}

View file

@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
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 { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
import { NormalizedInlineCompletion } from './inlineCompletionToGhostText';
export interface SuggestWidgetState {
/**
* Represents the currently selected item in the suggest widget as inline completion, if possible.
*/
selectedItemAsInlineCompletion: NormalizedInlineCompletion | undefined;
}
export class SuggestWidgetInlineCompletionProvider extends Disposable {
private isSuggestWidgetVisible: boolean = false;
private isShiftKeyPressed = false;
private _isActive = false;
private _currentInlineCompletion: NormalizedInlineCompletion | undefined = undefined;
private readonly onDidChangeEmitter = new Emitter<void>();
public readonly onDidChange = this.onDidChangeEmitter.event;
// This delay fixes a suggest widget issue when typing "." immediately restarts the suggestion session.
private readonly setInactiveDelayed = this._register(new RunOnceScheduler(() => {
if (!this.isSuggestWidgetVisible) {
if (this._isActive) {
this._isActive = false;
this.onDidChangeEmitter.fire();
}
}
}, 100));
/**
* Returns undefined if the suggest widget is not active.
*/
get state(): SuggestWidgetState | undefined {
if (!this._isActive) {
return undefined;
}
return { selectedItemAsInlineCompletion: this._currentInlineCompletion };
}
constructor(private readonly editor: IActiveCodeEditor) {
super();
// See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab
this._register(editor.onKeyDown(e => {
if (e.shiftKey && !this.isShiftKeyPressed) {
this.isShiftKeyPressed = true;
this.update(this._isActive);
}
}));
this._register(editor.onKeyUp(e => {
if (e.shiftKey && this.isShiftKeyPressed) {
this.isShiftKeyPressed = false;
this.update(this._isActive);
}
}));
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
let isBoundToSuggestWidget = false;
const bindToSuggestWidget = () => {
if (isBoundToSuggestWidget) {
return;
}
isBoundToSuggestWidget = true;
this._register(suggestController.widget.value.onDidShow(() => {
this.isSuggestWidgetVisible = true;
this.update(true);
}));
this._register(suggestController.widget.value.onDidHide(() => {
this.isSuggestWidgetVisible = false;
this.setInactiveDelayed.schedule();
this.update(this._isActive);
}));
this._register(suggestController.widget.value.onDidFocus(() => {
this.isSuggestWidgetVisible = true;
this.update(true);
}));
};
this._register(Event.once(suggestController.model.onDidTrigger)(e => {
bindToSuggestWidget();
}));
}
this.update(this._isActive);
}
private update(newActive: boolean): void {
const newInlineCompletion = this.getInlineCompletion();
let shouldFire = false;
if (this._currentInlineCompletion !== newInlineCompletion) {
this._currentInlineCompletion = newInlineCompletion;
shouldFire = true;
}
if (this._isActive !== newActive) {
this._isActive = newActive;
shouldFire = true;
}
if (shouldFire) {
this.onDidChangeEmitter.fire();
}
}
private getInlineCompletion(): NormalizedInlineCompletion | undefined {
const suggestController = SuggestController.get(this.editor);
if (!suggestController) {
return undefined;
}
if (!this.isSuggestWidgetVisible) {
return undefined;
}
const focusedItem = suggestController.widget.value.getFocusedItem();
if (!focusedItem) {
return undefined;
}
// TODO: item.isResolved
return suggestionToInlineCompletion(
suggestController,
this.editor.getPosition(),
focusedItem,
this.isShiftKeyPressed
);
}
public stopForceRenderingAbove(): void {
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
suggestController.stopForceRenderingAbove();
}
}
public forceRenderingAbove(): void {
const suggestController = SuggestController.get(this.editor);
if (suggestController) {
suggestController.forceRenderingAbove();
}
}
}
function suggestionToInlineCompletion(suggestController: SuggestController, position: Position, suggestion: ISelectedSuggestion, toggleMode: boolean): NormalizedInlineCompletion {
const item = suggestion.item;
if (Array.isArray(item.completion.additionalTextEdits)) {
// cannot represent additional text edits
return {
text: '',
range: Range.fromPositions(position, position),
};
}
let { insertText } = item.completion;
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
const snippet = new SnippetParser().parse(insertText);
const model = suggestController.editor.getModel()!;
SnippetSession.adjustWhitespace(
model, position, snippet,
true,
true
);
insertText = snippet.toString();
}
const info = suggestController.getOverwriteInfo(item, toggleMode);
return {
text: insertText,
range: Range.fromPositions(
position.delta(0, -info.overwriteBefore),
position.delta(0, Math.max(info.overwriteAfter, 0))
),
};
}

View file

@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
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 { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText';
import { SuggestWidgetInlineCompletionProvider } from './suggestWidgetInlineCompletionProvider';
export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel {
private readonly suggestionInlineCompletionSource = this._register(new SuggestWidgetInlineCompletionProvider(this.editor));
private readonly updateOperation = this._register(new MutableDisposable<UpdateOperation>());
private readonly updateCacheSoon = this._register(new RunOnceScheduler(() => this.updateCache(), 50));
public override minReservedLineCount: number = 0;
public get isActive(): boolean {
return this.suggestionInlineCompletionSource.state !== undefined;
}
constructor(
editor: IActiveCodeEditor,
private readonly cache: SharedInlineCompletionCache,
) {
super(editor);
this._register(this.suggestionInlineCompletionSource.onDidChange(() => {
const suggestWidgetState = this.suggestionInlineCompletionSource.state;
if (!suggestWidgetState) {
this.minReservedLineCount = 0;
}
this.updateCacheSoon.schedule();
if (this.minReservedLineCount >= 1 && this.isSuggestionPreviewEnabled()) {
this.suggestionInlineCompletionSource.forceRenderingAbove();
} else {
this.suggestionInlineCompletionSource.stopForceRenderingAbove();
}
this.onDidChangeEmitter.fire();
}));
this._register(this.cache.onDidChange(() => {
this.onDidChangeEmitter.fire();
}));
this._register(this.editor.onDidChangeCursorPosition((e) => {
if (this.isSuggestionPreviewEnabled()) {
this.minReservedLineCount = 0;
this.updateCacheSoon.schedule();
this.onDidChangeEmitter.fire();
}
}));
this._register(toDisposable(() => this.suggestionInlineCompletionSource.stopForceRenderingAbove()));
}
private isSuggestionPreviewEnabled(): boolean {
const suggestOptions = this.editor.getOption(EditorOption.suggest);
return suggestOptions.preview;
}
private async updateCache() {
const state = this.suggestionInlineCompletionSource.state;
if (!state || !state.selectedItemAsInlineCompletion) {
return;
}
const info: SelectedSuggestionInfo = {
text: state.selectedItemAsInlineCompletion.text,
range: state.selectedItemAsInlineCompletion.range,
};
const position = this.editor.getPosition();
const promise = createCancelablePromise(async token => {
let result;
try {
result = await provideInlineCompletions(position,
this.editor.getModel(),
{ triggerKind: InlineCompletionTriggerKind.Automatic, selectedSuggestionInfo: info },
token
);
} catch (e) {
onUnexpectedError(e);
return;
}
if (token.isCancellationRequested) {
return;
}
this.cache.setValue(
this.editor,
result,
InlineCompletionTriggerKind.Automatic
);
this.onDidChangeEmitter.fire();
});
const operation = new UpdateOperation(promise, InlineCompletionTriggerKind.Automatic);
this.updateOperation.value = operation;
await promise;
if (this.updateOperation.value === operation) {
this.updateOperation.clear();
}
}
public override get ghostText(): GhostText | undefined {
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;
}
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 start = model.getPositionAt(model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLength);
const commonSuffixLength = lengthOfLongestCommonSuffix(valueToReplace, inlineCompletion.text);
const end = model.getPositionAt(model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLength);
return {
range: Range.fromPositions(start, end),
text: inlineCompletion.text.substr(commonPrefixLength, inlineCompletion.text.length - commonPrefixLength - commonSuffixLength),
};
}
const suggestWidgetState = this.suggestionInlineCompletionSource.state;
const originalInlineCompletion = minimizeInlineCompletion(this.editor.getModel()!, suggestWidgetState?.selectedItemAsInlineCompletion);
const augmentedCompletion = minimizeInlineCompletion(this.editor.getModel()!, this.cache.value?.completions[0]?.toLiveInlineCompletion());
const finalCompletion =
augmentedCompletion
&& originalInlineCompletion
&& augmentedCompletion.text.startsWith(originalInlineCompletion.text)
&& augmentedCompletion.range.equalsRange(originalInlineCompletion.range)
? augmentedCompletion : (originalInlineCompletion || augmentedCompletion);
const inlineCompletionPreviewLength = (finalCompletion?.text.length || 0) - (originalInlineCompletion?.text.length || 0);
const toGhostText = (completion: NormalizedInlineCompletion | undefined): GhostText | undefined => {
const mode = this.editor.getOptions().get(EditorOption.suggest).previewMode;
return completion
? (
inlineCompletionToGhostText(completion, this.editor.getModel(), mode, this.editor.getPosition(), inlineCompletionPreviewLength) ||
// Show an invisible ghost text to reserve space
new GhostText(completion.range.endLineNumber, [], this.minReservedLineCount)
)
: undefined;
};
const newGhostText = toGhostText(finalCompletion);
if (newGhostText) {
this.minReservedLineCount = Math.max(this.minReservedLineCount, sum(newGhostText.parts.map(p => p.lines.length - 1)));
}
return this.isSuggestionPreviewEnabled()
? newGhostText
: undefined;
}
}
function sum(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0);
}

View file

@ -9,12 +9,14 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { InlineCompletionsProvider, InlineCompletionsProviderRegistry } from 'vs/editor/common/modes';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { InlineCompletionsModel, inlineCompletionToGhostText } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { inlineCompletionToGhostText } from '../inlineCompletionToGhostText';
import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/utils';
import { ITestCodeEditor, TestCodeEditorCreationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel';
suite('Inline Completions', () => {
ensureNoDisposablesAreLeakedInTestSuite();
@ -84,7 +86,7 @@ suite('Inline Completions', () => {
});
});
test('Does trigger automatically if disabled', async function () {
test('Does not trigger automatically if disabled', async function () {
const provider = new MockInlineCompletionsProvider();
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, inlineSuggest: { enabled: false } },
@ -147,13 +149,14 @@ suite('Inline Completions', () => {
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 4) });
context.keyboardType('foo');
model.trigger();
await timeout(1000);
provider.setReturnValue({ text: 'foobizz', range: new Range(1, 1, 1, 6) });
context.keyboardType('bi');
context.keyboardType('b');
context.keyboardType('i');
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
@ -449,11 +452,6 @@ suite('Inline Completions', () => {
{ position: '(1,5)', text: 'foob', triggerKind: 0, }
]);
assert.deepStrictEqual(context.getAndClearViewStates(), [
/*
TODO: Remove this flickering. Fortunately, it is not visible.
It is caused by the text model updating before the cursor position.
*/
'foob',
'foob[ar]',
'foob[az]'
]);
@ -506,7 +504,8 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
let result: T;
await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {
const model = instantiationService.createInstance(InlineCompletionsModel, editor);
const cache = disposableStore.add(new SharedInlineCompletionCache());
const model = instantiationService.createInstance(InlineCompletionsModel, editor, cache);
const context = new GhostTextContext(model, editor);
result = await callback({ editor, editorViewModel, model, context });
context.dispose();

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel';
import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
@ -27,6 +27,7 @@ import assert = require('assert');
import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/utils';
import { Range } from 'vs/editor/common/core/range';
import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel';
suite('Suggest Widget Model', () => {
test('Active', async () => {
@ -69,11 +70,11 @@ suite('Suggest Widget Model', () => {
const suggestController = (editor.getContribution(SuggestController.ID) as SuggestController);
suggestController.triggerSuggest();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h[ello]']);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h', 'h[ello]']);
context.keyboardType('.');
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['hello', 'hello.[hello]']);
assert.deepStrictEqual(context.getAndClearViewStates(), ['hello', 'hello.', 'hello.[hello]']);
suggestController.cancelSuggestWidget();
@ -107,7 +108,7 @@ const provider: CompletionItemProvider = {
async function withAsyncTestCodeEditorAndInlineCompletionsModel(
text: string,
options: TestCodeEditorCreationOptions & { provider?: CompletionItemProvider, fakeClock?: boolean, serviceCollection?: never },
callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: SuggestWidgetAdapterModel, context: GhostTextContext }) => Promise<void>
callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: SuggestWidgetPreviewModel, context: GhostTextContext }) => Promise<void>
): Promise<void> {
await runWithFakedTimers({ useFakeTimers: options.fakeClock }, async () => {
const disposableStore = new DisposableStore();
@ -145,7 +146,8 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel(
await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => {
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController);
const model = instantiationService.createInstance(SuggestWidgetAdapterModel, editor);
const cache = disposableStore.add(new SharedInlineCompletionCache());
const model = instantiationService.createInstance(SuggestWidgetPreviewModel, editor, cache);
const context = new GhostTextContext(model, editor);
await callback({ editor, editorViewModel, model, context });
model.dispose();

6
src/vs/monaco.d.ts vendored
View file

@ -5936,6 +5936,12 @@ declare namespace monaco.languages {
* How the completion was triggered.
*/
readonly triggerKind: InlineCompletionTriggerKind;
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: IRange;
text: string;
}
export interface InlineCompletion {

View file

@ -2415,6 +2415,18 @@ declare module 'vscode' {
* How the completion was triggered.
*/
readonly triggerKind: InlineCompletionTriggerKind;
/**
* Provides information about the currently selected item in the suggest widget.
* If set, provided inline completions must extend the text of the selected item
* and use the same range, otherwise they are not shown as preview.
*/
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: Range;
text: string;
}
/**

View file

@ -1033,11 +1033,20 @@ class InlineCompletionAdapter {
private readonly _commands: CommandsConverter,
) { }
public async provideInlineCompletions(resource: URI, position: IPosition, context: vscode.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> {
public async provideInlineCompletions(resource: URI, position: IPosition, context: modes.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> {
const doc = this._documents.getDocument(resource);
const pos = typeConvert.Position.to(position);
const result = await this._provider.provideInlineCompletionItems(doc, pos, context, token);
const result = await this._provider.provideInlineCompletionItems(doc, pos, {
selectedSuggestionInfo:
context.selectedSuggestionInfo
? {
range: typeConvert.Range.to(context.selectedSuggestionInfo.range),
text: context.selectedSuggestionInfo.text
}
: undefined,
triggerKind: context.triggerKind
}, token);
if (!result) {
// undefined and null are valid results