Rewrites the inline completion feature.

Fixes #172162
Fixes #174329
Fixes #172153
Fixes #164716
Fixes #170527
This commit is contained in:
Henning Dieterichs
2023-04-17 11:52:32 +02:00
parent 5b5b1c3cc3
commit cb0e59c566
31 changed files with 2135 additions and 2709 deletions
+37 -13
View File
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import type { derived } from 'vs/base/common/observableImpl/derived';
import { getLogger } from 'vs/base/common/observableImpl/logging';
@@ -25,9 +26,9 @@ export interface IObservable<T, TChange = void> {
/**
* Subscribes the reader to this observable and returns the current value of this observable.
*/
read(reader: IReader): T;
read(reader: IReader | undefined): T;
map<TNew>(fn: (value: T) => TNew): IObservable<TNew>;
map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
readonly debugName: string;
}
@@ -100,19 +101,19 @@ export abstract class ConvenientObservable<T, TChange> implements IObservable<T,
public abstract removeObserver(observer: IObserver): void;
/** @sealed */
public read(reader: IReader): T {
reader.subscribeTo(this);
public read(reader: IReader | undefined): T {
reader?.subscribeTo(this);
return this.get();
}
/** @sealed */
public map<TNew>(fn: (value: T) => TNew): IObservable<TNew> {
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew> {
return _derived(
() => {
const name = getFunctionName(fn);
return name !== undefined ? name : `${this.debugName} (mapped)`;
},
(reader) => fn(this.read(reader))
(reader) => fn(this.read(reader), reader)
);
}
@@ -204,19 +205,19 @@ export class ObservableValue<T, TChange = void>
extends BaseObservable<T, TChange>
implements ISettableObservable<T, TChange>
{
private value: T;
protected _value: T;
constructor(public readonly debugName: string, initialValue: T) {
super();
this.value = initialValue;
this._value = initialValue;
}
public get(): T {
return this.value;
return this._value;
}
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
if (this.value === value) {
if (this._value === value) {
return;
}
@@ -227,8 +228,8 @@ export class ObservableValue<T, TChange = void>
return;
}
const oldValue = this.value;
this.value = value;
const oldValue = this._value;
this._setValue(value);
getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true });
for (const observer of this.observers) {
@@ -238,7 +239,30 @@ export class ObservableValue<T, TChange = void>
}
override toString(): string {
return `${this.debugName}: ${this.value}`;
return `${this.debugName}: ${this._value}`;
}
protected _setValue(newValue: T): void {
this._value = newValue;
}
}
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
return new DisposableObservableValue(name, initialValue);
}
export class DisposableObservableValue<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
protected override _setValue(newValue: T): void {
if (this._value === newValue) {
return;
}
if (this._value) {
this._value.dispose();
}
this._value = newValue;
}
public dispose(): void {
this._value?.dispose();
}
}
+16 -6
View File
@@ -621,19 +621,29 @@ export enum InlineCompletionTriggerKind {
}
export interface InlineCompletionContext {
/**
* How the completion was triggered.
*/
readonly triggerKind: InlineCompletionTriggerKind;
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: IRange;
text: string;
isSnippetText: boolean;
completionKind: CompletionItemKind;
export class SelectedSuggestionInfo {
constructor(
public readonly range: IRange,
public readonly text: string,
public readonly completionKind: CompletionItemKind,
public readonly isSnippetText: boolean,
) {
}
public equals(other: SelectedSuggestionInfo) {
return Range.lift(this.range).equalsRange(other.range)
&& this.text === other.text
&& this.completionKind === other.completionKind
&& this.isSnippetText === other.isSnippetText;
}
}
export interface InlineCompletion {
+1 -1
View File
@@ -28,7 +28,7 @@ import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverT
import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant';
import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant';
import 'vs/css!./hover';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineSuggestionHintsWidget';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver';
@@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { transaction } from 'vs/base/common/observable';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId, inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds';
import { InlineCompletionContextKeys, InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import * as nls from 'vs/nls';
import { MenuId, Action2 } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
export class ShowNextInlineSuggestionAction extends EditorAction {
public static ID = showNextInlineSuggestionActionId;
constructor() {
super({
id: ShowNextInlineSuggestionAction.ID,
label: nls.localize('action.inlineSuggest.showNext', "Show Next Inline Suggestion"),
alias: 'Show Next Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineSuggestionVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketRight,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
controller?.model.get()?.next();
}
}
export class ShowPreviousInlineSuggestionAction extends EditorAction {
public static ID = showPreviousInlineSuggestionActionId;
constructor() {
super({
id: ShowPreviousInlineSuggestionAction.ID,
label: nls.localize('action.inlineSuggest.showPrevious', "Show Previous Inline Suggestion"),
alias: 'Show Previous Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineSuggestionVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketLeft,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
controller?.model.get()?.previous();
}
}
export class TriggerInlineSuggestionAction extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.trigger',
label: nls.localize('action.inlineSuggest.trigger', "Trigger Inline Suggestion"),
alias: 'Trigger Inline Suggestion',
precondition: EditorContextKeys.writable
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
controller?.model.get()?.trigger(undefined);
}
}
export class AcceptNextWordOfInlineCompletion extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.acceptNextWord',
label: nls.localize('action.inlineSuggest.acceptNextWord', "Accept Next Word Of Inline Suggestion"),
alias: 'Accept Next Word Of Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineSuggestionVisible),
kbOpts: {
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.RightArrow,
},
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('acceptWord', 'Accept Word'),
group: 'primary',
order: 2,
}],
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
controller?.model.get()?.acceptNextWord(controller.editor);
}
}
export class AcceptInlineCompletion extends EditorAction {
constructor() {
super({
id: inlineSuggestCommitId,
label: nls.localize('action.inlineSuggest.accept', "Accept Inline Suggestion"),
alias: 'Accept Inline Suggestion',
precondition: InlineCompletionContextKeys.inlineSuggestionVisible,
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('accept', "Accept"),
group: 'primary',
order: 1,
}],
kbOpts: {
primary: KeyCode.Tab,
weight: 200,
kbExpr: ContextKeyExpr.and(
InlineCompletionContextKeys.inlineSuggestionVisible,
EditorContextKeys.tabMovesFocus.toNegated(),
InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize
),
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
if (controller) {
controller.model.get()?.accept(controller.editor);
controller.editor.focus();
}
}
}
export class HideInlineCompletion extends EditorAction {
public static ID = 'editor.action.inlineSuggest.hide';
constructor() {
super({
id: HideInlineCompletion.ID,
label: nls.localize('action.inlineSuggest.hide', "Hide Inline Suggestion"),
alias: 'Hide Inline Suggestion',
precondition: InlineCompletionContextKeys.inlineSuggestionVisible,
kbOpts: {
weight: 100,
primary: KeyCode.Escape,
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineCompletionsController.get(editor);
transaction(tx => {
controller?.model.get()?.stop(tx);
});
}
}
export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 {
public static ID = 'editor.action.inlineSuggest.toggleAlwaysShowToolbar';
constructor() {
super({
id: ToggleAlwaysShowInlineSuggestionToolbar.ID,
title: nls.localize('action.inlineSuggest.alwaysShowToolbar', "Always Show Toolbar"),
f1: false,
precondition: undefined,
menu: [{
id: MenuId.InlineSuggestionToolbar,
group: 'secondary',
order: 10,
}],
toggled: InlineCompletionContextKeys.alwaysShowInlineSuggestionToolbar,
});
}
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
const configService = accessor.get(IConfigurationService);
const currentValue = configService.getValue<'always' | 'onHover'>('editor.inlineSuggest.showToolbar');
const newValue = currentValue === 'always' ? 'onHover' : 'always';
configService.updateValue('editor.inlineSuggest.showToolbar', newValue);
}
}
@@ -24,11 +24,7 @@
font-size: 0;
}
.monaco-editor .ghost-text-decoration {
font-style: italic;
}
.monaco-editor .suggest-preview-text {
.monaco-editor .ghost-text-decoration, .monaco-editor .suggest-preview-text .ghost-text {
font-style: italic;
}
@@ -3,17 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { applyEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { Range } from 'vs/editor/common/core/range';
import { ColumnRange, applyEdits } from 'vs/editor/contrib/inlineCompletions/browser/utils';
export class GhostText {
public static equals(a: GhostText | undefined, b: GhostText | undefined): boolean {
return a === b || (!!a && !!b && a.equals(b));
}
constructor(
public readonly lineNumber: number,
public readonly parts: GhostTextPart[],
@@ -32,14 +25,12 @@ export class GhostText {
*/
render(documentText: string, debug: boolean = false): string {
const l = this.lineNumber;
return applyEdits(documentText,
[
...this.parts.map(p => ({
range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column },
text: debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n')
})),
]
);
return applyEdits(documentText, [
...this.parts.map(p => ({
range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column },
text: debug ? `[${p.lines.join('\n')}]` : p.lines.join('\n')
})),
]);
}
renderForScreenReader(lineText: string): string {
@@ -62,6 +53,10 @@ export class GhostText {
isEmpty(): boolean {
return this.parts.every(p => p.lines.length === 0);
}
get lineCount(): number {
return 1 + this.parts.reduce((r, p) => r + p.lines.length - 1, 0);
}
}
export class GhostTextPart {
@@ -83,96 +78,43 @@ export class GhostTextPart {
}
export class GhostTextReplacement {
constructor(
readonly lineNumber: number,
readonly columnStart: number,
readonly length: number,
readonly newLines: readonly string[],
public readonly additionalReservedLineCount: number = 0,
) { }
public readonly parts: ReadonlyArray<GhostTextPart> = [
new GhostTextPart(
this.columnStart + this.length,
this.columnRange.endColumnExclusive,
this.newLines,
false
),
];
constructor(
readonly lineNumber: number,
readonly columnRange: ColumnRange,
readonly newLines: readonly string[],
public readonly additionalReservedLineCount: number = 0,
) { }
renderForScreenReader(_lineText: string): string {
return this.newLines.join('\n');
}
render(documentText: string, debug: boolean = false): string {
const startLineNumber = this.lineNumber;
const endLineNumber = this.lineNumber;
const replaceRange = this.columnRange.toRange(this.lineNumber);
if (debug) {
return applyEdits(documentText,
[
{
range: { startLineNumber, endLineNumber, startColumn: this.columnStart, endColumn: this.columnStart },
text: `(`
},
{
range: { startLineNumber, endLineNumber, startColumn: this.columnStart + this.length, endColumn: this.columnStart + this.length },
text: `)[${this.newLines.join('\n')}]`
}
]
);
return applyEdits(documentText, [
{ range: Range.fromPositions(replaceRange.getStartPosition()), text: `(` },
{ range: Range.fromPositions(replaceRange.getEndPosition()), text: `)[${this.newLines.join('\n')}]` }
]);
} else {
return applyEdits(documentText,
[
{
range: { startLineNumber, endLineNumber, startColumn: this.columnStart, endColumn: this.columnStart + this.length },
text: this.newLines.join('\n')
}
]
);
return applyEdits(documentText, [
{ range: replaceRange, text: this.newLines.join('\n') }
]);
}
}
}
export interface GhostTextWidgetModel {
readonly onDidChange: Event<void>;
readonly ghostText: GhostText | GhostTextReplacement | undefined;
setExpanded(expanded: boolean): void;
readonly expanded: boolean;
readonly minReservedLineCount: number;
}
export abstract class BaseGhostTextWidgetModel extends Disposable implements GhostTextWidgetModel {
public abstract readonly ghostText: GhostText | GhostTextReplacement | undefined;
private _expanded: boolean | undefined = undefined;
protected readonly onDidChangeEmitter = new Emitter<void>();
public readonly onDidChange = this.onDidChangeEmitter.event;
public abstract readonly minReservedLineCount: number;
public get expanded() {
if (this._expanded === undefined) {
// TODO this should use a global hidden setting.
// See https://github.com/microsoft/vscode/issues/125037.
return true;
}
return this._expanded;
}
constructor(protected readonly editor: IActiveCodeEditor) {
super();
this._register(editor.onDidChangeConfiguration((e) => {
if (e.hasChanged(EditorOption.suggest) && this._expanded === undefined) {
this.onDidChangeEmitter.fire();
}
}));
}
public setExpanded(expanded: boolean): void {
this._expanded = true;
this.onDidChangeEmitter.fire();
get lineCount(): number {
return this.newLines.length;
}
}
export type GhostTextOrReplacement = GhostText | GhostTextReplacement;
@@ -1,437 +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 { Emitter } from 'vs/base/common/event';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { firstNonWhitespaceIndex } from 'vs/base/common/strings';
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
import { GhostTextModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel';
import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget';
import { InlineSuggestionHintsWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineSuggestionHintsWidget';
import * as nls from 'vs/nls';
import { Action2, MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
export class GhostTextController extends Disposable {
public static readonly inlineSuggestionVisible = new RawContextKey<boolean>('inlineSuggestionVisible', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible"));
public static readonly inlineSuggestionHasIndentation = new RawContextKey<boolean>('inlineSuggestionHasIndentation', false, nls.localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace"));
public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey<boolean>('inlineSuggestionHasIndentationLessThanTabSize', true, nls.localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab"));
/**
* Enables to use Ctrl+Left to undo partially accepted inline completions.
*/
public static readonly canUndoInlineSuggestion = new RawContextKey<boolean>('canUndoInlineSuggestion', false, nls.localize('canUndoInlineSuggestion', "Whether undo would undo an inline suggestion"));
public static readonly alwaysShowInlineSuggestionToolbar = new RawContextKey<boolean>('alwaysShowInlineSuggestionToolbar', false, nls.localize('alwaysShowInlineSuggestionToolbar', "Whether the inline suggestion toolbar should always be visible"));
static ID = 'editor.contrib.ghostTextController';
public static get(editor: ICodeEditor): GhostTextController | null {
return editor.getContribution<GhostTextController>(GhostTextController.ID);
}
private triggeredExplicitly = false;
protected readonly activeController = this._register(new MutableDisposable<ActiveGhostTextController>());
public get activeModel(): GhostTextModel | undefined {
return this.activeController.value?.model;
}
private readonly activeModelDidChangeEmitter = this._register(new Emitter<void>());
public readonly onActiveModelDidChange = this.activeModelDidChangeEmitter.event;
/**
* Tracks the first alternative version id until which only partial inline suggestions can be undone.
* Any other content change will invalidate this.
* This field is used to set the corresponding context key.
*/
private firstUndoableVersionId: number | undefined = undefined;
public readonly alwaysShowInlineSuggestionToolbar = GhostTextController.alwaysShowInlineSuggestionToolbar.bindTo(this.contextKeyService);
constructor(
public readonly editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
this._register(this.editor.onDidChangeModelContent((e) => {
if (!e.isUndoing || this.firstUndoableVersionId && this.editor.getModel()!.getAlternativeVersionId() < this.firstUndoableVersionId) {
this.activeController.value?.contextKeys.canUndoInlineSuggestion.reset();
this.firstUndoableVersionId = undefined; // Will be set again if this change was caused by an inline suggestion.
}
}));
this._register(this.editor.onDidChangeCursorPosition((e) => {
if (e.reason === CursorChangeReason.Explicit) {
this.activeController.value?.contextKeys.canUndoInlineSuggestion.reset();
this.firstUndoableVersionId = undefined;
}
}));
this._register(this.editor.onDidChangeModel(() => {
this.update();
}));
this._register(this.editor.onDidChangeConfiguration((e) => {
if (e.hasChanged(EditorOption.suggest) || e.hasChanged(EditorOption.inlineSuggest)) {
this.update();
}
}));
this.update();
}
// Don't call this method when not necessary. It will recreate the activeController.
private update(): void {
const suggestOptions = this.editor.getOption(EditorOption.suggest);
const inlineSuggestOptions = this.editor.getOption(EditorOption.inlineSuggest);
this.alwaysShowInlineSuggestionToolbar.set(inlineSuggestOptions.showToolbar === 'always');
const shouldCreate = this.editor.hasModel() && (suggestOptions.preview || inlineSuggestOptions.enabled || this.triggeredExplicitly);
if (shouldCreate !== !!this.activeController.value) {
this.activeController.value = undefined;
// ActiveGhostTextController is only created if one of those settings is set or if the inline completions are triggered explicitly.
this.activeController.value =
shouldCreate ? this.instantiationService.createInstance(
ActiveGhostTextController,
this.editor as IActiveCodeEditor
)
: undefined;
this.activeModelDidChangeEmitter.fire();
}
}
public shouldShowHoverAt(hoverRange: Range): boolean {
return this.activeModel?.shouldShowHoverAt(hoverRange) || false;
}
public shouldShowHoverAtViewZone(viewZoneId: string): boolean {
return this.activeController.value?.widget?.shouldShowHoverAtViewZone(viewZoneId) || false;
}
public trigger(): void {
this.triggeredExplicitly = true;
if (!this.activeController.value) {
this.update();
}
this.activeModel?.triggerInlineCompletion();
}
public commitPartially(): void {
const nextVersion = this.firstUndoableVersionId; // Read this before committing, as it will be reset.
this.activeModel?.commitInlineCompletionPartially();
this.activeController?.value?.contextKeys.canUndoInlineSuggestion.set(true);
// Don't override this field if the previous command already accepted some inline suggestion.
this.firstUndoableVersionId = nextVersion ?? this.editor.getModel()!.getAlternativeVersionId();
}
public commit(): void {
this.activeModel?.commitInlineCompletion();
}
public hide(): void {
this.activeModel?.hideInlineCompletion();
}
public showNextInlineCompletion(): void {
this.activeModel?.showNextInlineCompletion();
}
public showPreviousInlineCompletion(): void {
this.activeModel?.showPreviousInlineCompletion();
}
public async getInlineCompletionsCount(): Promise<number> {
const result = await this.activeModel?.getInlineCompletionsCount();
return result ?? 0;
}
}
class GhostTextContextKeys {
public readonly inlineCompletionVisible = GhostTextController.inlineSuggestionVisible.bindTo(this.contextKeyService);
public readonly inlineCompletionSuggestsIndentation = GhostTextController.inlineSuggestionHasIndentation.bindTo(this.contextKeyService);
public readonly inlineCompletionSuggestsIndentationLessThanTabSize = GhostTextController.inlineSuggestionHasIndentationLessThanTabSize.bindTo(this.contextKeyService);
public readonly canUndoInlineSuggestion = GhostTextController.canUndoInlineSuggestion.bindTo(this.contextKeyService);
constructor(private readonly contextKeyService: IContextKeyService) {
}
}
/**
* The controller for a text editor with an initialized text model.
* Must be disposed as soon as the model detaches from the editor.
*/
export class ActiveGhostTextController extends Disposable {
public readonly contextKeys = new GhostTextContextKeys(this.contextKeyService);
public readonly model = this._register(this.instantiationService.createInstance(GhostTextModel, this.editor));
public readonly widget = this._register(this.instantiationService.createInstance(GhostTextWidget, this.editor, this.model));
public readonly hintsWidget = this._register(this.instantiationService.createInstance(InlineSuggestionHintsWidget, this.editor, this.model.inlineCompletionsModel));
constructor(
private readonly editor: IActiveCodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super();
this._register(toDisposable(() => {
this.contextKeys.inlineCompletionVisible.set(false);
this.contextKeys.inlineCompletionSuggestsIndentation.set(false);
this.contextKeys.inlineCompletionSuggestsIndentationLessThanTabSize.set(true);
}));
this._register(this.model.onDidChange(() => {
this.updateContextKeys();
}));
this.updateContextKeys();
}
private updateContextKeys(): void {
this.contextKeys.inlineCompletionVisible.set(
this.model.activeInlineCompletionsModel?.ghostText !== undefined
);
let startsWithIndentation = false;
let startsWithIndentationLessThanTabSize = true;
const ghostText = this.model.inlineCompletionsModel.ghostText;
if (!!this.model.activeInlineCompletionsModel && ghostText && ghostText.parts.length > 0) {
const { column, lines } = ghostText.parts[0];
const firstLine = lines[0];
const indentationEndColumn = this.editor.getModel().getLineIndentColumn(ghostText.lineNumber);
const inIndentation = column <= indentationEndColumn;
if (inIndentation) {
let firstNonWsIdx = firstNonWhitespaceIndex(firstLine);
if (firstNonWsIdx === -1) {
firstNonWsIdx = firstLine.length - 1;
}
startsWithIndentation = firstNonWsIdx > 0;
const tabSize = this.editor.getModel().getOptions().tabSize;
const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize);
startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize;
}
}
this.contextKeys.inlineCompletionSuggestsIndentation.set(startsWithIndentation);
this.contextKeys.inlineCompletionSuggestsIndentationLessThanTabSize.set(startsWithIndentationLessThanTabSize);
}
}
export class ShowNextInlineSuggestionAction extends EditorAction {
public static ID = showNextInlineSuggestionActionId;
constructor() {
super({
id: ShowNextInlineSuggestionAction.ID,
label: nls.localize('action.inlineSuggest.showNext', "Show Next Inline Suggestion"),
alias: 'Show Next Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.inlineSuggestionVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketRight,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.showNextInlineCompletion();
}
}
}
export class ShowPreviousInlineSuggestionAction extends EditorAction {
public static ID = showPreviousInlineSuggestionActionId;
constructor() {
super({
id: ShowPreviousInlineSuggestionAction.ID,
label: nls.localize('action.inlineSuggest.showPrevious', "Show Previous Inline Suggestion"),
alias: 'Show Previous Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.inlineSuggestionVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketLeft,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.showPreviousInlineCompletion();
}
}
}
export class TriggerInlineSuggestionAction extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.trigger',
label: nls.localize('action.inlineSuggest.trigger', "Trigger Inline Suggestion"),
alias: 'Trigger Inline Suggestion',
precondition: EditorContextKeys.writable
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
controller?.trigger();
}
}
export class AcceptNextWordOfInlineCompletion extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.acceptNextWord',
label: nls.localize('action.inlineSuggest.acceptNextWord', "Accept Next Word Of Inline Suggestion"),
alias: 'Accept Next Word Of Inline Suggestion',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.inlineSuggestionVisible),
kbOpts: {
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.RightArrow,
},
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('acceptWord', 'Accept Word'),
group: 'primary',
order: 2,
}],
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.commitPartially();
}
}
}
export class AcceptInlineCompletion extends EditorAction {
constructor() {
super({
id: inlineSuggestCommitId,
label: nls.localize('action.inlineSuggest.accept', "Accept Inline Suggestion"),
alias: 'Accept Inline Suggestion',
precondition: GhostTextController.inlineSuggestionVisible,
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('accept', "Accept"),
group: 'primary',
order: 1,
}],
kbOpts: {
primary: KeyCode.Tab,
weight: 200,
kbExpr: ContextKeyExpr.and(
GhostTextController.inlineSuggestionVisible,
EditorContextKeys.tabMovesFocus.toNegated(),
GhostTextController.inlineSuggestionHasIndentationLessThanTabSize
),
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.commit();
controller.editor.focus();
}
}
}
export class HideInlineCompletion extends EditorAction {
public static ID = 'editor.action.inlineSuggest.hide';
constructor() {
super({
id: HideInlineCompletion.ID,
label: nls.localize('action.inlineSuggest.hide', "Hide Inline Suggestion"),
alias: 'Hide Inline Suggestion',
precondition: GhostTextController.inlineSuggestionVisible,
kbOpts: {
weight: 100,
primary: KeyCode.Escape,
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.hide();
}
}
}
export class ToggleAlwaysShowInlineSuggestionToolbar extends Action2 {
public static ID = 'editor.action.inlineSuggest.toggleAlwaysShowToolbar';
constructor() {
super({
id: ToggleAlwaysShowInlineSuggestionToolbar.ID,
title: nls.localize('action.inlineSuggest.alwaysShowToolbar', "Always Show Toolbar"),
f1: false,
precondition: undefined,
menu: [{
id: MenuId.InlineSuggestionToolbar,
group: 'secondary',
order: 10,
}],
toggled: GhostTextController.alwaysShowInlineSuggestionToolbar,
});
}
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
const configService = accessor.get(IConfigurationService);
const currentValue = configService.getValue<'always' | 'onHover'>('editor.inlineSuggest.showToolbar');
const newValue = currentValue === 'always' ? 'onHover' : 'always';
configService.updateValue('editor.inlineSuggest.showToolbar', newValue);
}
}
export class UndoAcceptPart extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineSuggest.undo',
label: nls.localize('action.inlineSuggest.undo', "Undo Accept Word"),
alias: 'Undo Accept Word',
precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.canUndoInlineSuggestion),
kbOpts: {
weight: KeybindingWeight.EditorContrib + 1,
primary: KeyMod.CtrlCmd | KeyCode.LeftArrow,
kbExpr: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.canUndoInlineSuggestion),
},
menuOpts: [{
menuId: MenuId.InlineSuggestionToolbar,
title: nls.localize('undoAcceptWord', 'Undo Accept Word'),
group: 'secondary',
order: 3,
}],
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
editor.getModel()?.undo();
}
}
@@ -1,168 +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 { Emitter } from 'vs/base/common/event';
import { Disposable, IReference, MutableDisposable } 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 { InlineCompletionTriggerKind } from 'vs/editor/common/languages';
import { GhostText, GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { InlineCompletionsModel, SynchronizedInlineCompletionsCache, TrackedInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel';
import { createDisposableRef } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export abstract class DelegatingModel extends Disposable implements GhostTextWidgetModel {
private readonly onDidChangeEmitter = new Emitter<void>();
public readonly onDidChange = this.onDidChangeEmitter.event;
private hasCachedGhostText = false;
private cachedGhostText: GhostText | GhostTextReplacement | undefined;
private readonly currentModelRef = this._register(new MutableDisposable<IReference<GhostTextWidgetModel>>());
protected get targetModel(): GhostTextWidgetModel | undefined {
return this.currentModelRef.value?.object;
}
protected setTargetModel(model: GhostTextWidgetModel | undefined): void {
if (this.currentModelRef.value?.object === model) {
return;
}
this.currentModelRef.clear();
this.currentModelRef.value = model ? createDisposableRef(model, model.onDidChange(() => {
this.hasCachedGhostText = false;
this.onDidChangeEmitter.fire();
})) : undefined;
this.hasCachedGhostText = false;
this.onDidChangeEmitter.fire();
}
public get ghostText(): GhostText | GhostTextReplacement | undefined {
if (!this.hasCachedGhostText) {
this.cachedGhostText = this.currentModelRef.value?.object?.ghostText;
this.hasCachedGhostText = true;
}
return this.cachedGhostText;
}
public setExpanded(expanded: boolean): void {
this.targetModel?.setExpanded(expanded);
}
public get expanded(): boolean {
return this.targetModel ? this.targetModel.expanded : false;
}
public get minReservedLineCount(): number {
return this.targetModel ? this.targetModel.minReservedLineCount : 0;
}
}
/**
* A ghost text model that is both driven by inline completions and the suggest widget.
*/
export class GhostTextModel extends DelegatingModel implements GhostTextWidgetModel {
public readonly sharedCache = this._register(new SharedInlineCompletionCache());
public readonly suggestWidgetAdapterModel = this._register(this.instantiationService.createInstance(SuggestWidgetPreviewModel, this.editor, this.sharedCache));
public readonly inlineCompletionsModel = this._register(this.instantiationService.createInstance(InlineCompletionsModel, this.editor, this.sharedCache));
public get activeInlineCompletionsModel(): InlineCompletionsModel | undefined {
if (this.targetModel === this.inlineCompletionsModel) {
return this.inlineCompletionsModel;
}
return undefined;
}
constructor(
private readonly editor: IActiveCodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this._register(this.suggestWidgetAdapterModel.onDidChange(() => {
this.updateModel();
}));
this.updateModel();
}
private updateModel(): void {
this.setTargetModel(
this.suggestWidgetAdapterModel.isActive
? this.suggestWidgetAdapterModel
: this.inlineCompletionsModel
);
this.inlineCompletionsModel.setActive(this.targetModel === this.inlineCompletionsModel);
}
public shouldShowHoverAt(hoverRange: Range): boolean {
const ghostText = this.activeInlineCompletionsModel?.ghostText;
if (ghostText) {
return ghostText.parts.some(p => hoverRange.containsPosition(new Position(ghostText.lineNumber, p.column)));
}
return false;
}
public triggerInlineCompletion(): void {
this.activeInlineCompletionsModel?.trigger(InlineCompletionTriggerKind.Explicit);
}
public commitInlineCompletion(): void {
this.activeInlineCompletionsModel?.commitCurrentSuggestion();
}
public commitInlineCompletionPartially(): void {
this.activeInlineCompletionsModel?.commitCurrentSuggestionPartially();
}
public hideInlineCompletion(): void {
this.activeInlineCompletionsModel?.hide();
}
public showNextInlineCompletion(): void {
this.activeInlineCompletionsModel?.showNext();
}
public showPreviousInlineCompletion(): void {
this.activeInlineCompletionsModel?.showPrevious();
}
public async getInlineCompletionsCount(): Promise<number> {
const result = await this.activeInlineCompletionsModel?.getInlineCompletionsCount();
return result ?? 0;
}
}
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: TrackedInlineCompletions,
triggerKind: InlineCompletionTriggerKind
) {
this.cache.value = new SynchronizedInlineCompletionsCache(
completionsSource,
editor,
() => this.onDidChangeEmitter.fire(),
triggerKind
);
}
public clearAndLeak(): SynchronizedInlineCompletionsCache | undefined {
return this.cache.clearAndLeak();
}
public clear() {
this.cache.clear();
}
}
@@ -3,90 +3,65 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, derived, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable';
import * as strings from 'vs/base/common/strings';
import 'vs/css!./ghostText';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { StringBuilder } from 'vs/editor/common/core/stringBuilder';
import { IModelDeltaDecoration, InjectedTextCursorStops, PositionAffinity } from 'vs/editor/common/model';
import { ILanguageIdCodec } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAffinity } from 'vs/editor/common/model';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { InlineDecorationType } from 'vs/editor/common/viewModel';
import { GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { GhostText, GhostTextReplacement } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { ColumnRange, applyObservableDecorations } from 'vs/editor/contrib/inlineCompletions/browser/utils';
const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value });
export interface IGhostTextWidgetModel {
readonly targetTextModel: IObservable<ITextModel | undefined>;
readonly ghostText: IObservable<GhostText | GhostTextReplacement | undefined>;
readonly minReservedLineCount: IObservable<number>;
}
export class GhostTextWidget extends Disposable {
private disposed = false;
private readonly partsWidget = this._register(this.instantiationService.createInstance(DecorationsWidget, this.editor));
private readonly additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor, this.languageService.languageIdCodec));
private viewMoreContentWidget: ViewMoreLinesContentWidget | undefined = undefined;
private _lastSuggestion: string | undefined = undefined;
private readonly isDisposed = observableValue('isDisposed', false);
private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel());
constructor(
private readonly editor: ICodeEditor,
private readonly model: GhostTextWidgetModel,
@IInstantiationService private readonly instantiationService: IInstantiationService,
private readonly model: IGhostTextWidgetModel,
@ILanguageService private readonly languageService: ILanguageService,
@IAudioCueService private readonly audioCueService: IAudioCueService
) {
super();
this._register(this.editor.onDidChangeConfiguration((e) => {
if (
e.hasChanged(EditorOption.disableMonospaceOptimizations)
|| e.hasChanged(EditorOption.stopRenderingLineAfter)
|| e.hasChanged(EditorOption.renderWhitespace)
|| e.hasChanged(EditorOption.renderControlCharacters)
|| e.hasChanged(EditorOption.fontLigatures)
|| e.hasChanged(EditorOption.fontInfo)
|| e.hasChanged(EditorOption.lineHeight)
) {
this.update();
}
}));
this._register(toDisposable(() => {
this.disposed = true;
this.update();
this.viewMoreContentWidget?.dispose();
this.viewMoreContentWidget = undefined;
}));
this._register(model.onDidChange(() => this.update(true)));
this.update(true);
this._register(toDisposable(() => { this.isDisposed.set(true, undefined); }));
this._register(applyObservableDecorations(this.editor, this.decorations));
}
public shouldShowHoverAtViewZone(viewZoneId: string): boolean {
return (this.additionalLinesWidget.viewZoneId === viewZoneId);
}
private readonly replacementDecoration = this._register(new DisposableDecorations(this.editor));
private update(notifyUser?: boolean): void {
const ghostText = this.model.ghostText;
if (!this.editor.hasModel() || !ghostText || this.disposed) {
this.partsWidget.clear();
this.additionalLinesWidget.clear();
this.replacementDecoration.clear();
return;
private readonly uiState = derived('uiState', reader => {
if (this.isDisposed.read(reader)) {
return undefined;
}
const textModel = this.currentTextModel.read(reader);
if (textModel !== this.model.targetTextModel.read(reader)) {
return undefined;
}
const ghostText = this.model.ghostText.read(reader);
if (!ghostText) {
return undefined;
}
const inlineTexts = new Array<InsertedInlineText>();
const additionalLines = new Array<LineData>();
const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined;
const inlineTexts: { column: number; text: string; preview: boolean }[] = [];
const additionalLines: LineData[] = [];
function addToAdditionalLines(lines: readonly string[], className: string | undefined) {
if (additionalLines.length > 0) {
@@ -106,26 +81,7 @@ export class GhostTextWidget extends Disposable {
}
}
if (ghostText instanceof GhostTextReplacement) {
this.replacementDecoration.setDecorations([
{
range: new Range(
ghostText.lineNumber,
ghostText.columnStart,
ghostText.lineNumber,
ghostText.columnStart + ghostText.length
),
options: {
inlineClassName: 'inline-completion-text-to-replace',
description: 'GhostTextReplacement'
}
},
]);
} else {
this.replacementDecoration.setDecorations([]);
}
const textBufferLine = this.editor.getModel().getLineContent(ghostText.lineNumber);
const textBufferLine = textModel.getLineContent(ghostText.lineNumber);
let hiddenTextStartColumn: number | undefined = undefined;
let lastIdx = 0;
@@ -155,165 +111,116 @@ export class GhostTextWidget extends Disposable {
addToAdditionalLines([textBufferLine.substring(lastIdx)], undefined);
}
this.partsWidget.setParts(ghostText.lineNumber, inlineTexts,
hiddenTextStartColumn !== undefined ? { column: hiddenTextStartColumn, length: textBufferLine.length + 1 - hiddenTextStartColumn } : undefined);
this.additionalLinesWidget.updateLines(ghostText.lineNumber, additionalLines, ghostText.additionalReservedLineCount);
const hiddenRange = hiddenTextStartColumn !== undefined ? new ColumnRange(hiddenTextStartColumn, textBufferLine.length + 1) : undefined;
if (notifyUser) {
const lineText = this.editor.getModel()?.getLineContent(ghostText.lineNumber);
if (lineText && this._lastSuggestion !== lineText) {
this._lastSuggestion = lineText;
this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => {
if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) {
alert(ghostText.renderForScreenReader(lineText));
}
});
}
}
if (0 < 0) {
// Not supported at the moment, condition is always false.
this.viewMoreContentWidget = this.renderViewMoreLines(
new Position(ghostText.lineNumber, this.editor.getModel()!.getLineMaxColumn(ghostText.lineNumber)),
'', 0
);
} else {
this.viewMoreContentWidget?.dispose();
this.viewMoreContentWidget = undefined;
}
}
return {
replacedRange,
inlineTexts,
additionalLines,
hiddenRange,
lineNumber: ghostText.lineNumber,
additionalReservedLineCount: ghostText.additionalReservedLineCount,
targetTextModel: textModel,
};
});
private renderViewMoreLines(position: Position, firstLineText: string, remainingLinesLength: number): ViewMoreLinesContentWidget {
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
const domNode = document.createElement('div');
domNode.className = 'suggest-preview-additional-widget';
applyFontInfo(domNode, fontInfo);
const spacer = document.createElement('span');
spacer.className = 'content-spacer';
spacer.append(firstLineText);
domNode.append(spacer);
const newline = document.createElement('span');
newline.className = 'content-newline suggest-preview-text';
newline.append('⏎ ');
domNode.append(newline);
const disposableStore = new DisposableStore();
const button = document.createElement('div');
button.className = 'button suggest-preview-text';
button.append(`+${remainingLinesLength} lines…`);
disposableStore.add(dom.addStandardDisposableListener(button, 'mousedown', (e) => {
this.model?.setExpanded(true);
e.preventDefault();
this.editor.focus();
}));
domNode.append(button);
return new ViewMoreLinesContentWidget(this.editor, position, domNode, disposableStore);
}
}
class DisposableDecorations {
private decorationIds: string[] = [];
constructor(private readonly editor: ICodeEditor) {
}
public setDecorations(decorations: IModelDeltaDecoration[]): void {
// Using change decorations ensures that we update the id's before some event handler is called.
this.editor.changeDecorations(accessor => {
this.decorationIds = accessor.deltaDecorations(this.decorationIds, decorations);
});
}
public clear(): void {
this.setDecorations([]);
}
public dispose(): void {
this.clear();
}
}
interface HiddenText {
column: number;
length: number;
}
interface InsertedInlineText {
column: number;
text: string;
preview: boolean;
}
class DecorationsWidget implements IDisposable {
private decorationIds: string[] = [];
constructor(
private readonly editor: ICodeEditor
) {
}
public dispose(): void {
this.clear();
}
public clear(): void {
// Using change decorations ensures that we update the id's before some event handler is called.
this.editor.changeDecorations(accessor => {
this.decorationIds = accessor.deltaDecorations(this.decorationIds, []);
});
}
public setParts(lineNumber: number, parts: InsertedInlineText[], hiddenText?: HiddenText): void {
const textModel = this.editor.getModel();
if (!textModel) {
return;
private readonly decorations = derived('decorations', reader => {
const uiState = this.uiState.read(reader);
if (!uiState) {
return [];
}
const hiddenTextDecorations = new Array<IModelDeltaDecoration>();
if (hiddenText) {
hiddenTextDecorations.push({
range: Range.fromPositions(new Position(lineNumber, hiddenText.column), new Position(lineNumber, hiddenText.column + hiddenText.length)),
const decorations: IModelDeltaDecoration[] = [];
if (uiState.replacedRange) {
decorations.push({
range: uiState.replacedRange.toRange(uiState.lineNumber),
options: { inlineClassName: 'inline-completion-text-to-replace', description: 'GhostTextReplacement' }
});
}
if (uiState.hiddenRange) {
decorations.push({
range: uiState.hiddenRange.toRange(uiState.lineNumber),
options: { inlineClassName: 'ghost-text-hidden', description: 'ghost-text-hidden', }
});
}
for (const p of uiState.inlineTexts) {
decorations.push({
range: Range.fromPositions(new Position(uiState.lineNumber, p.column)),
options: {
inlineClassName: 'ghost-text-hidden',
description: 'ghost-text-hidden',
description: 'ghost-text',
after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration', cursorStops: InjectedTextCursorStops.Left },
showIfCollapsed: true,
}
});
}
// Using change decorations ensures that we update the id's before some event handler is called.
this.editor.changeDecorations(accessor => {
this.decorationIds = accessor.deltaDecorations(this.decorationIds, parts.map<IModelDeltaDecoration>(p => {
return ({
range: Range.fromPositions(new Position(lineNumber, p.column)),
options: {
description: 'ghost-text',
after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration', cursorStops: InjectedTextCursorStops.Left },
showIfCollapsed: true,
}
});
}).concat(hiddenTextDecorations));
});
return decorations;
});
private readonly additionalLinesWidget = this._register(
new AdditionalLinesWidget(
this.editor,
this.languageService.languageIdCodec,
derived('lines', (reader) => {
const uiState = this.uiState.read(reader);
return uiState ? {
lineNumber: uiState.lineNumber,
additionalLines: uiState.additionalLines,
minReservedLineCount: uiState.additionalReservedLineCount,
targetTextModel: uiState.targetTextModel,
} : undefined;
})
)
);
public ownsViewZone(viewZoneId: string): boolean {
return this.additionalLinesWidget.viewZoneId === viewZoneId;
}
}
class AdditionalLinesWidget implements IDisposable {
class AdditionalLinesWidget extends Disposable {
private _viewZoneId: string | undefined = undefined;
public get viewZoneId(): string | undefined { return this._viewZoneId; }
private readonly editorOptionsChanged = observableSignalFromEvent('editorOptionChanged', Event.filter(
this.editor.onDidChangeConfiguration,
e => e.hasChanged(EditorOption.disableMonospaceOptimizations)
|| e.hasChanged(EditorOption.stopRenderingLineAfter)
|| e.hasChanged(EditorOption.renderWhitespace)
|| e.hasChanged(EditorOption.renderControlCharacters)
|| e.hasChanged(EditorOption.fontLigatures)
|| e.hasChanged(EditorOption.fontInfo)
|| e.hasChanged(EditorOption.lineHeight)
));
constructor(
private readonly editor: ICodeEditor,
private readonly languageIdCodec: ILanguageIdCodec
) { }
private readonly languageIdCodec: ILanguageIdCodec,
private readonly lines: IObservable<{ targetTextModel: ITextModel; lineNumber: number; additionalLines: LineData[]; minReservedLineCount: number } | undefined>
) {
super();
public dispose(): void {
this._register(autorun('update view zone', reader => {
const lines = this.lines.read(reader);
this.editorOptionsChanged.read(reader);
if (lines) {
this.updateLines(lines.lineNumber, lines.additionalLines, lines.minReservedLineCount);
} else {
this.clear();
}
}));
}
public override dispose(): void {
super.dispose();
this.clear();
}
public clear(): void {
private clear(): void {
this.editor.changeViewZones((changeAccessor) => {
if (this._viewZoneId) {
changeAccessor.removeZone(this._viewZoneId);
@@ -322,7 +229,7 @@ class AdditionalLinesWidget implements IDisposable {
});
}
public updateLines(lineNumber: number, additionalLines: LineData[], minReservedLineCount: number): void {
private updateLines(lineNumber: number, additionalLines: LineData[], minReservedLineCount: number): void {
const textModel = this.editor.getModel();
if (!textModel) {
return;
@@ -353,7 +260,7 @@ class AdditionalLinesWidget implements IDisposable {
}
interface LineData {
content: string;
content: string; // Must not contain a linebreak!
decorations: LineDecoration[];
}
@@ -414,37 +321,4 @@ function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], o
domNode.innerHTML = trustedhtml as string;
}
class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
readonly allowEditorOverflow = false;
readonly suppressMouseDown = false;
constructor(
private editor: ICodeEditor,
private position: Position,
private domNode: HTMLElement,
disposableStore: DisposableStore
) {
super();
this._register(disposableStore);
this._register(toDisposable(() => {
this.editor.removeContentWidget(this);
}));
this.editor.addContentWidget(this);
}
getId(): string {
return 'editor.widget.viewMoreLinesWidget';
}
getDomNode(): HTMLElement {
return this.domNode;
}
getPosition(): IContentWidgetPosition | null {
return {
position: this.position,
preference: [ContentWidgetPositionPreference.EXACT]
};
}
}
const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value });
@@ -4,18 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { autorun, constObservable } from 'vs/base/common/observable';
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { Command } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { IModelDecoration } from 'vs/editor/common/model';
import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes';
import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineSuggestionHintsWidget';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
import * as nls from 'vs/nls';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
@@ -27,7 +26,7 @@ export class InlineCompletionsHover implements IHoverPart {
constructor(
public readonly owner: IEditorHoverParticipant<InlineCompletionsHover>,
public readonly range: Range,
public readonly controller: GhostTextController
public readonly controller: InlineCompletionsController
) { }
public isValidForHoverAnchor(anchor: HoverAnchor): boolean {
@@ -37,31 +36,6 @@ export class InlineCompletionsHover implements IHoverPart {
&& this.range.endColumn >= anchor.range.endColumn
);
}
public requestExplicitContext(): void {
this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.ensureUpdateWithExplicitContext();
}
public getInlineCompletionsCount(): number | undefined {
const session = this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value;
if (!session?.hasBeenTriggeredExplicitly) {
return undefined;
}
return session?.getInlineCompletionsCountSync();
}
public getInlineCompletionIndex(): number | undefined {
return this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.currentlySelectedIndex;
}
public onDidChange(handler: () => void): IDisposable {
const d = this.controller.activeModel?.activeInlineCompletionsModel?.onDidChange(handler);
return d || Disposable.None;
}
public get commands(): Command[] {
return this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.commands || [];
}
}
export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant<InlineCompletionsHover> {
@@ -79,7 +53,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
}
suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null {
const controller = GhostTextController.get(this._editor);
const controller = InlineCompletionsController.get(this._editor);
if (!controller) {
return null;
}
@@ -113,7 +87,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
return [];
}
const controller = GhostTextController.get(this._editor);
const controller = InlineCompletionsController.get(this._editor);
if (controller && controller.shouldShowHoverAt(anchor.range)) {
return [new InlineCompletionsHover(this, anchor.range, controller)];
}
@@ -133,15 +107,18 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
this.renderScreenReaderText(context, part, disposableStore);
}
const w = this._instantiationService.createInstance(InlineSuggestionHintsContentWidget, this._editor, false);
const model = part.controller.model.get()!;
const w = this._instantiationService.createInstance(InlineSuggestionHintsContentWidget, this._editor, false,
constObservable(null),
model.currentInlineCompletionIndex,
model.inlineCompletionsCount,
model.currentInlineCompletion.map(v => v?.inlineCompletion.source.inlineCompletions.commands ?? []),);
context.fragment.appendChild(w.getDomNode());
w.update(null, part.getInlineCompletionIndex() || 0, part.getInlineCompletionsCount(), part.commands);
part.requestExplicitContext();
model.triggerExplicitly();
disposableStore.add(part.onDidChange(() => {
w.update(null, part.getInlineCompletionIndex() || 0, part.getInlineCompletionsCount(), part.commands);
}));
disposableStore.add(w);
return disposableStore;
}
@@ -162,8 +139,8 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
hoverContentsElement.replaceChildren(renderedContents.element);
};
disposableStore.add(Event.runAndSubscribe<void>(e => part.onDidChange(e), () => {
const ghostText = part.controller.activeModel?.inlineCompletionsModel?.ghostText;
disposableStore.add(autorun('update hover', (reader) => {
const ghostText = part.controller.model.read(reader)?.ghostText.read(reader);
if (ghostText) {
const lineText = this._editor.getModel()!.getLineContent(ghostText.lineNumber);
render(ghostText.renderForScreenReader(lineText));
@@ -172,7 +149,6 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
}
}));
context.fragment.appendChild(markdownHoverElement);
}
}
@@ -1,281 +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 { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff';
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 { Command } from 'vs/editor/common/languages';
import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
/**
* A normalized inline completion is an inline completion with a defined range.
*/
export interface NormalizedInlineCompletion {
readonly filterText: string;
readonly command?: Command;
readonly range: Range;
readonly insertText: string;
readonly snippetInfo:
| {
snippet: string;
/* Could be different than the main range */
range: Range;
}
| undefined;
readonly additionalTextEdits: readonly ISingleEditOperation[];
}
/**
* Shrinks the range if the text has a suffix/prefix that agrees with the text buffer.
* E.g. text buffer: `ab[cdef]ghi`, [...] is the replace range, `cxyzf` is the new text.
* Then the minimized inline completion has range `abc[de]fghi` and text `xyz`.
*/
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 = strings.commonPrefixLength(valueToReplace, inlineCompletion.insertText);
const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen;
const start = model.getPositionAt(startOffset);
const remainingValueToReplace = valueToReplace.substr(commonPrefixLen);
const commonSuffixLen = strings.commonSuffixLength(remainingValueToReplace, inlineCompletion.insertText);
const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen));
return {
range: Range.fromPositions(start, end),
insertText: inlineCompletion.insertText.substr(commonPrefixLen, inlineCompletion.insertText.length - commonPrefixLen - commonSuffixLen),
snippetInfo: inlineCompletion.snippetInfo,
filterText: inlineCompletion.filterText,
additionalTextEdits: inlineCompletion.additionalTextEdits,
};
}
export function normalizedInlineCompletionsEquals(a: NormalizedInlineCompletion | undefined, b: NormalizedInlineCompletion | undefined): boolean {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return a.range.equalsRange(b.range) && a.insertText === b.insertText && a.command === b.command;
}
/**
* @param previewSuffixLength Sets where to split `inlineCompletion.text`.
* If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
*/
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 sourceLine = textModel.getLineContent(inlineCompletion.range.startLineNumber);
const sourceIndentationLength = strings.getLeadingWhitespace(sourceLine).length;
const suggestionTouchesIndentation = inlineCompletion.range.startColumn - 1 <= sourceIndentationLength;
if (suggestionTouchesIndentation) {
// source: ··········[······abc]
// ^^^^^^^^^ inlineCompletion.range
// ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
// ^^^^^^ replacedIndentation.length
// ^^^ rangeThatDoesNotReplaceIndentation
// inlineCompletion.text: '··foo'
// ^^ suggestionAddedIndentationLength
const suggestionAddedIndentationLength = strings.getLeadingWhitespace(inlineCompletion.insertText).length;
const replacedIndentation = sourceLine.substring(inlineCompletion.range.startColumn - 1, sourceIndentationLength);
const rangeThatDoesNotReplaceIndentation = Range.fromPositions(
inlineCompletion.range.getStartPosition().delta(0, replacedIndentation.length),
inlineCompletion.range.getEndPosition()
);
const suggestionWithoutIndentationChange =
inlineCompletion.insertText.startsWith(replacedIndentation)
// Adds more indentation without changing existing indentation: We can add ghost text for this
? inlineCompletion.insertText.substring(replacedIndentation.length)
// Changes or removes existing indentation. Only add ghost text for the non-indentation part.
: inlineCompletion.insertText.substring(suggestionAddedIndentationLength);
inlineCompletion = {
range: rangeThatDoesNotReplaceIndentation,
insertText: suggestionWithoutIndentationChange,
command: inlineCompletion.command,
snippetInfo: undefined,
filterText: inlineCompletion.filterText,
additionalTextEdits: inlineCompletion.additionalTextEdits,
};
}
// This is a single line string
const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
const changes = cachingDiff(valueToBeReplaced, inlineCompletion.insertText);
if (!changes) {
// No ghost text in case the diff would be too slow to compute
return undefined;
}
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;
}
}
const previewStartInCompletionText = inlineCompletion.insertText.length - previewSuffixLength;
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) {
return undefined;
}
if (c.modifiedLength === 0) {
continue;
}
const modifiedEnd = c.modifiedStart + c.modifiedLength;
const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
const nonPreviewText = inlineCompletion.insertText.substring(c.modifiedStart, nonPreviewTextEnd);
const italicText = inlineCompletion.insertText.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 = undefined;
function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] | undefined {
if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) {
return lastRequest?.changes;
} else {
let changes = smartDiff(originalValue, newValue, true);
if (changes) {
const deletedChars = deletedCharacters(changes);
if (deletedChars > 0) {
// For performance reasons, don't compute diff if there is nothing to improve
const newChanges = smartDiff(originalValue, newValue, false);
if (newChanges && deletedCharacters(newChanges) < deletedChars) {
// Disabling smartness seems to be better here
changes = newChanges;
}
}
}
lastRequest = {
originalValue,
newValue,
changes
};
return changes;
}
}
function deletedCharacters(changes: readonly IDiffChange[]): number {
let sum = 0;
for (const c of changes) {
sum += c.originalLength;
}
return sum;
}
/**
* 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, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined {
if (originalValue.length > 5000 || newValue.length > 5000) {
// We don't want to work on strings that are too big
return undefined;
}
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++) {
// TODO support more brackets
if (smartBracketMatching && source[i] === '(') {
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id);
level++;
} else if (smartBracketMatching && source[i] === ')') {
level = Math.max(level - 1, 0);
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id + 1);
if (level === 0) {
group++;
}
} 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;
}
@@ -5,18 +5,19 @@
import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes';
import { AcceptInlineCompletion, AcceptNextWordOfInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar, GhostTextController, HideInlineCompletion, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, TriggerInlineSuggestionAction, UndoAcceptPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant';
import { TriggerInlineSuggestionAction, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, AcceptNextWordOfInlineCompletion, AcceptInlineCompletion, HideInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar } from 'vs/editor/contrib/inlineCompletions/browser/commands';
import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/hoverParticipant';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { registerAction2 } from 'vs/platform/actions/common/actions';
registerEditorContribution(GhostTextController.ID, GhostTextController, EditorContributionInstantiation.Eventually);
registerEditorContribution(InlineCompletionsController.ID, InlineCompletionsController, EditorContributionInstantiation.Eventually);
registerEditorAction(TriggerInlineSuggestionAction);
registerEditorAction(ShowNextInlineSuggestionAction);
registerEditorAction(ShowPreviousInlineSuggestionAction);
registerEditorAction(AcceptNextWordOfInlineCompletion);
registerEditorAction(AcceptInlineCompletion);
registerEditorAction(HideInlineCompletion);
registerEditorAction(UndoAcceptPart);
registerAction2(ToggleAlwaysShowInlineSuggestionToolbar);
HoverParticipantRegistry.register(InlineCompletionsHoverParticipant);
@@ -0,0 +1,266 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun, constObservable, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { IObservable, ITransaction, disposableObservableValue, transaction } from 'vs/base/common/observableImpl/base';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget';
import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as nls from 'vs/nls';
import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
import { firstNonWhitespaceIndex } from 'vs/base/common/strings';
import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CoreEditingCommands } from 'vs/editor/browser/coreCommands';
import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds';
import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { alert } from 'vs/base/browser/ui/aria/aria';
export class InlineCompletionsController extends Disposable {
static ID = 'editor.contrib.inlineCompletionsController';
public static get(editor: ICodeEditor): InlineCompletionsController | null {
return editor.getContribution<InlineCompletionsController>(InlineCompletionsController.ID);
}
private readonly suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor(
this.editor,
() => this.model.get()?.currentInlineCompletion.get()?.toSingleTextEdit(),
(tx) => this.updateObservables(tx, VersionIdChangeReason.Other)
));
private readonly textModelVersionId = observableValue<number, VersionIdChangeReason>('textModelVersionId', -1);
private readonly cursorPosition = observableValue<Position>('cursorPosition', new Position(1, 1));
public readonly model = disposableObservableValue<InlineCompletionsModel | undefined>('textModelVersionId', undefined);
private ghostTextWidget = this._register(this.instantiationService.createInstance(GhostTextWidget, this.editor, {
ghostText: this.model.map((v, reader) => v?.ghostText.read(reader)),
minReservedLineCount: constObservable(0),
targetTextModel: this.model.map(v => v?.textModel),
}));
private readonly _debounceValue = this.debounceService.for(
this.languageFeaturesService.inlineCompletionsProvider,
'InlineCompletionsDebounce',
{ min: 50, max: 50 }
);
constructor(
public readonly editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ICommandService private readonly commandService: ICommandService,
@ILanguageFeatureDebounceService private readonly debounceService: ILanguageFeatureDebounceService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IAudioCueService private readonly audioCueService: IAudioCueService,
) {
super();
this._register(new InlineCompletionContextKeys(this.contextKeyService, this.model));
const enabled = observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.inlineSuggest).enabled);
this._register(Event.runAndSubscribe(editor.onDidChangeModel, () => {
this.model.set(undefined, undefined); // This disposes the model (do this outside of the transaction to dispose autoruns)
transaction(tx => {
this.updateObservables(tx, VersionIdChangeReason.Other);
const textModel = editor.getModel();
if (textModel) {
const model = instantiationService.createInstance(
InlineCompletionsModel,
textModel,
this.suggestWidgetAdaptor.selectedItem,
this.cursorPosition,
this.textModelVersionId,
this._debounceValue,
observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview),
observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).previewMode),
observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.inlineSuggest).mode),
enabled
);
this.model.set(model, tx);
}
});
}));
this._register(editor.onDidChangeModelContent((e) => transaction(tx =>
this.updateObservables(tx,
e.isUndoing ? VersionIdChangeReason.Undo
: e.isRedoing ? VersionIdChangeReason.Redo
: this.model.get()?.isAcceptingPartialWord ? VersionIdChangeReason.AcceptWord
: VersionIdChangeReason.Other
)
)));
this._register(editor.onDidChangeCursorPosition(e => transaction(tx => {
this.updateObservables(tx, VersionIdChangeReason.Other);
if (e.reason === CursorChangeReason.Explicit) {
this.model.get()?.stop(tx);
}
})));
this._register(editor.onDidType(() => transaction(tx => {
this.updateObservables(tx, VersionIdChangeReason.Other);
if (enabled.get()) {
this.model.get()?.trigger(tx);
}
})));
this._register(
this.commandService.onDidExecuteCommand((e) => {
// These commands don't trigger onDidType.
const commands = new Set([
CoreEditingCommands.Tab.id,
CoreEditingCommands.DeleteLeft.id,
CoreEditingCommands.DeleteRight.id,
inlineSuggestCommitId,
'acceptSelectedSuggestion',
]);
if (commands.has(e.commandId) && editor.hasTextFocus()) {
transaction(tx => {
this.model.get()?.trigger(tx);
});
}
})
);
this._register(this.editor.onDidBlurEditorWidget(() => {
// This is a hidden setting very useful for debugging
if (this.configurationService.getValue('editor.inlineSuggest.keepOnBlur')) {
return;
}
if (InlineSuggestionHintsContentWidget.dropDownVisible) {
return;
}
transaction(tx => {
this.model.get()?.stop(tx);
});
}));
this._register(autorun('forceRenderingAbove', reader => {
const model = this.model.read(reader);
const ghostText = model?.ghostText.read(reader);
const selectedSuggestItem = this.suggestWidgetAdaptor.selectedItem.read(reader);
if (selectedSuggestItem) {
if (ghostText && ghostText.lineCount >= 2) {
this.suggestWidgetAdaptor.forceRenderingAbove();
}
} else {
this.suggestWidgetAdaptor.stopForceRenderingAbove();
}
}));
this._register(toDisposable(() => {
this.suggestWidgetAdaptor.stopForceRenderingAbove();
}));
let lastEditorLine: string | undefined = undefined;
this._register(autorun('play audio cue & read suggestion', reader => {
const model = this.model.read(reader);
const ghostText = model?.ghostText.read(reader);
if (!model || !ghostText) {
return;
}
const lineText = model.textModel.getLineContent(ghostText.lineNumber);
if (lastEditorLine !== lineText) {
lastEditorLine = lineText;
this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => {
if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) {
alert(ghostText.renderForScreenReader(lineText));
}
});
}
}));
}
private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void {
const newModel = this.editor.getModel();
this.textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason);
this.cursorPosition.set(this.editor.getPosition() ?? new Position(1, 1), tx);
}
shouldShowHoverAt(range: Range) {
const ghostText = this.model.get()?.ghostText.get();
if (ghostText) {
return ghostText.parts.some(p => range.containsPosition(new Position(ghostText.lineNumber, p.column)));
}
return false;
}
public shouldShowHoverAtViewZone(viewZoneId: string): boolean {
return this.ghostTextWidget.ownsViewZone(viewZoneId);
}
}
export class InlineCompletionContextKeys extends Disposable {
public static readonly inlineSuggestionVisible = new RawContextKey<boolean>('inlineSuggestionVisible', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible"));
public static readonly inlineSuggestionHasIndentation = new RawContextKey<boolean>('inlineSuggestionHasIndentation', false, nls.localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace"));
public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey<boolean>('inlineSuggestionHasIndentationLessThanTabSize', true, nls.localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab"));
public static readonly alwaysShowInlineSuggestionToolbar = new RawContextKey<boolean>('alwaysShowInlineSuggestionToolbar', false, nls.localize('alwaysShowInlineSuggestionToolbar', "Whether the inline suggestion toolbar should always be visible"));
public readonly inlineCompletionVisible = InlineCompletionContextKeys.inlineSuggestionVisible.bindTo(this.contextKeyService);
public readonly inlineCompletionSuggestsIndentation = InlineCompletionContextKeys.inlineSuggestionHasIndentation.bindTo(this.contextKeyService);
public readonly inlineCompletionSuggestsIndentationLessThanTabSize = InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize.bindTo(this.contextKeyService);
constructor(
private readonly contextKeyService: IContextKeyService,
private readonly model: IObservable<InlineCompletionsModel | undefined>,
) {
super();
this._register(autorun('update context key: inlineCompletionVisible', (reader) => {
const model = this.model.read(reader);
const ghostText = model?.ghostText.read(reader);
const selectedSuggestItem = model?.selectedSuggestItem.read(reader);
this.inlineCompletionVisible.set(selectedSuggestItem === undefined && ghostText !== undefined);
}));
this._register(autorun('update context key: inlineCompletionSuggestsIndentation, inlineCompletionSuggestsIndentationLessThanTabSize', (reader) => {
const model = this.model.read(reader);
let startsWithIndentation = false;
let startsWithIndentationLessThanTabSize = true;
const ghostText = model?.ghostText.read(reader);
if (!!model?.selectedSuggestItem && ghostText && ghostText.parts.length > 0) {
const { column, lines } = ghostText.parts[0];
const firstLine = lines[0];
const indentationEndColumn = model.textModel.getLineIndentColumn(ghostText.lineNumber);
const inIndentation = column <= indentationEndColumn;
if (inIndentation) {
let firstNonWsIdx = firstNonWhitespaceIndex(firstLine);
if (firstNonWsIdx === -1) {
firstNonWsIdx = firstLine.length - 1;
}
startsWithIndentation = firstNonWsIdx > 0;
const tabSize = model.textModel.getOptions().tabSize;
const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize);
startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize;
}
}
this.inlineCompletionSuggestsIndentation.set(startsWithIndentation);
this.inlineCompletionSuggestsIndentationLessThanTabSize.set(startsWithIndentationLessThanTabSize);
}));
}
}
@@ -11,15 +11,16 @@ import { equals } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, derived, observableFromEvent } from 'vs/base/common/observable';
import { OS } from 'vs/base/common/platform';
import { ThemeIcon } from 'vs/base/common/themables';
import 'vs/css!./inlineSuggestionHintsWidget';
import 'vs/css!./inlineCompletionsHintsWidget';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Command } from 'vs/editor/common/languages';
import { Command, InlineCompletionTriggerKind } from 'vs/editor/common/languages';
import { PositionAffinity } from 'vs/editor/common/model';
import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
import { showPreviousInlineSuggestionActionId, showNextInlineSuggestionActionId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { localize } from 'vs/nls';
import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
@@ -33,11 +34,37 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
export class InlineSuggestionHintsWidget extends Disposable {
private readonly widget = this._register(this.instantiationService.createInstance(InlineSuggestionHintsContentWidget, this.editor, true));
export class InlineCompletionsHintsWidget extends Disposable {
private readonly showToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar);
private sessionPosition: Position | undefined = undefined;
private isDisposed = false;
private readonly position = derived('position', reader => {
const ghostText = this.model.ghostText.read(reader);
if (this.showToolbar.read(reader) !== 'always' || !ghostText) {
this.sessionPosition = undefined;
return null;
}
const firstColumn = ghostText.parts[0].column;
if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) {
this.sessionPosition = undefined;
}
const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));
return position;
});
private readonly contentWidget = this._register(this.instantiationService.createInstance(
InlineSuggestionHintsContentWidget,
this.editor,
true,
this.position,
this.model.currentInlineCompletionIndex,
this.model.inlineCompletionsCount,
this.model.currentInlineCompletion.map(v => v?.inlineCompletion.source.inlineCompletions.commands ?? []),
));
constructor(
private readonly editor: ICodeEditor,
@@ -46,54 +73,18 @@ export class InlineSuggestionHintsWidget extends Disposable {
) {
super();
editor.addContentWidget(this.widget);
this._register(toDisposable(() => editor.removeContentWidget(this.widget)));
this._register(model.onDidChange(() => this.update()));
this._register(editor.onDidChangeConfiguration(() => this.update()));
this.update();
}
editor.addContentWidget(this.contentWidget);
this._register(toDisposable(() => editor.removeContentWidget(this.contentWidget)));
override dispose(): void {
this.isDisposed = true;
super.dispose();
}
private update(): void {
if (this.isDisposed) {
return;
}
const options = this.editor.getOption(EditorOption.inlineSuggest);
if (options.showToolbar !== 'always' || !this.model.ghostText) {
this.widget.update(null, 0, undefined, []);
this.sessionPosition = undefined;
return;
}
if (!this.model.completionSession.value) {
return;
}
if (!this.model.completionSession.value.hasBeenTriggeredExplicitly) {
this.model.completionSession.value.ensureUpdateWithExplicitContext();
}
const ghostText = this.model.ghostText;
const firstColumn = ghostText.parts[0].column;
if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) {
this.sessionPosition = undefined;
}
const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));
this.sessionPosition = position;
this.widget.update(
this.sessionPosition,
this.model.completionSession.value.currentlySelectedIndex,
this.model.completionSession.value.hasBeenTriggeredExplicitly ? this.model.completionSession.value.getInlineCompletionsCountSync() : undefined,
this.model.completionSession.value.commands,
);
this._register(autorun('request explicit', reader => {
const position = this.position.read(reader);
if (!position) {
return;
}
if (this.model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) {
this.model.triggerExplicitly();
}
}));
}
}
@@ -116,7 +107,6 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC
h('div@toolBar'),
])
]);
private position: Position | null = null;
private createCommandAction(commandId: string, label: string, iconClassName: string): Action {
const action = new Action(
@@ -155,13 +145,16 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC
this.previousAction.enabled = this.nextAction.enabled = false;
}, 100));
private lastCurrentSuggestionIdx = -1;
private lastSuggestionCount = -1;
private lastCommands: Command[] = [];
constructor(
private readonly editor: ICodeEditor,
private readonly withBorder: boolean,
private readonly _position: IObservable<Position | null>,
private readonly _currentSuggestionIdx: IObservable<number>,
private readonly _suggestionCount: IObservable<number | undefined>,
private readonly _extraCommands: IObservable<Command[]>,
@ICommandService private readonly _commandService: ICommandService,
@IInstantiationService instantiationService: IInstantiationService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@@ -188,62 +181,65 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC
this._register(this.toolBar.onDidChangeDropdownVisibility(e => {
InlineSuggestionHintsContentWidget._dropDownVisible = e;
}));
}
public update(position: Position | null, currentSuggestionIdx: number, suggestionCount: number | undefined, extraCommands: Command[]): void {
if (this.position === position
&& this.lastCurrentSuggestionIdx === currentSuggestionIdx
&& this.lastSuggestionCount === suggestionCount
&& equals(this.lastCommands, extraCommands)) {
// nothing to update
return;
}
this.position = position;
this.lastCurrentSuggestionIdx = currentSuggestionIdx;
this.lastSuggestionCount = suggestionCount ?? -1;
this.lastCommands = extraCommands;
if (suggestionCount !== undefined && suggestionCount > 1) {
this.disableButtonsDebounced.cancel();
this.previousAction.enabled = this.nextAction.enabled = true;
} else {
this.disableButtonsDebounced.schedule();
}
if (suggestionCount !== undefined) {
this.clearAvailableSuggestionCountLabelDebounced.cancel();
this.availableSuggestionCountAction.label = `${currentSuggestionIdx + 1}/${suggestionCount}`;
} else {
this.clearAvailableSuggestionCountLabelDebounced.schedule();
}
this.editor.layoutContentWidget(this);
const extraActions = extraCommands.map<IAction>(c => ({
class: undefined,
id: c.id,
enabled: true,
tooltip: c.tooltip || '',
label: c.title,
run: (event) => {
return this._commandService.executeCommand(c.id);
},
this._register(autorun('update position', (reader) => {
this._position.read(reader);
this.editor.layoutContentWidget(this);
}));
for (const [_, group] of this.inlineCompletionsActionsMenus.getActions()) {
for (const action of group) {
if (action instanceof MenuItemAction) {
extraActions.push(action);
this._register(autorun('counts', (reader) => {
const suggestionCount = this._suggestionCount.read(reader);
const currentSuggestionIdx = this._currentSuggestionIdx.read(reader);
if (suggestionCount !== undefined) {
this.clearAvailableSuggestionCountLabelDebounced.cancel();
this.availableSuggestionCountAction.label = `${currentSuggestionIdx + 1}/${suggestionCount}`;
} else {
this.clearAvailableSuggestionCountLabelDebounced.schedule();
}
if (suggestionCount !== undefined && suggestionCount > 1) {
this.disableButtonsDebounced.cancel();
this.previousAction.enabled = this.nextAction.enabled = true;
} else {
this.disableButtonsDebounced.schedule();
}
}));
this._register(autorun('extra commands', (reader) => {
const extraCommands = this._extraCommands.read(reader);
if (equals(this.lastCommands, extraCommands)) {
// nothing to update
return;
}
this.lastCommands = extraCommands;
const extraActions = extraCommands.map<IAction>(c => ({
class: undefined,
id: c.id,
enabled: true,
tooltip: c.tooltip || '',
label: c.title,
run: (event) => {
return this._commandService.executeCommand(c.id);
},
}));
for (const [_, group] of this.inlineCompletionsActionsMenus.getActions()) {
for (const action of group) {
if (action instanceof MenuItemAction) {
extraActions.push(action);
}
}
}
}
if (extraActions.length > 0) {
extraActions.unshift(new Separator());
}
if (extraActions.length > 0) {
extraActions.unshift(new Separator());
}
this.toolBar.setAdditionalSecondaryActions(extraActions);
this.toolBar.setAdditionalSecondaryActions(extraActions);
}));
}
getId(): string { return this.id; }
@@ -254,7 +250,7 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC
getPosition(): IContentWidgetPosition | null {
return {
position: this.position,
position: this._position.get(),
preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW],
positionAffinity: PositionAffinity.LeftOfInjectedText,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,330 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { BugIndicatingError } from 'vs/base/common/errors';
import { matchesSubString } from 'vs/base/common/filters';
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ITransaction } from 'vs/base/common/observable';
import { disposableObservableValue, transaction } from 'vs/base/common/observableImpl/base';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions';
export class InlineCompletionsSource extends Disposable {
private readonly updateOperation = this._register(new MutableDisposable<UpdateOperation>());
public readonly inlineCompletions = disposableObservableValue<UpToDateInlineCompletions | undefined>('inlineCompletions', undefined);
public readonly suggestWidgetInlineCompletions = disposableObservableValue<UpToDateInlineCompletions | undefined>('suggestWidgetInlineCompletions', undefined);
constructor(
private readonly textModel: ITextModel,
private readonly _debounceValue: IFeatureDebounceInformation,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService,
) {
super();
this._register(this.textModel.onDidChangeContent(() => {
this.updateOperation.clear();
}));
}
public clear(tx: ITransaction): void {
this.updateOperation.clear();
this.inlineCompletions.set(undefined, tx);
this.suggestWidgetInlineCompletions.set(undefined, tx);
}
public clearSuggestWidgetInlineCompletions(): void {
if (this.updateOperation.value?.request.context.selectedSuggestionInfo) {
this.updateOperation.clear();
}
this.suggestWidgetInlineCompletions.set(undefined, undefined);
}
public update(position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineCompletionWithUpdatedRange | undefined): Promise<boolean> {
const request = new UpdateRequest(position, context, this.textModel.getVersionId());
const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions : this.inlineCompletions;
if (this.updateOperation.value?.request.satisfies(request)) {
return this.updateOperation.value.promise;
} else if (target.get()?.request.satisfies(request)) {
return Promise.resolve(true);
}
const updateOngoing = !!this.updateOperation.value;
this.updateOperation.clear();
const source = new CancellationTokenSource();
const promise = (async () => {
const shouldDebounce = updateOngoing || context.triggerKind === InlineCompletionTriggerKind.Automatic;
if (shouldDebounce) {
// This debounces the operation
await wait(this._debounceValue.get(this.textModel));
}
if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
return false;
}
const startTime = new Date();
const updatedCompletions = await provideInlineCompletions(
this.languageFeaturesService.inlineCompletionsProvider,
position,
this.textModel,
context,
source.token,
this.languageConfigurationService
);
if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) {
return false;
}
const endTime = new Date();
this._debounceValue.update(this.textModel, endTime.getTime() - startTime.getTime());
const completions = new UpToDateInlineCompletions(updatedCompletions, request, this.textModel);
if (
activeInlineCompletion && activeInlineCompletion.updatedRange.containsPosition(position)
&& activeInlineCompletion.isVisible(this.textModel, position)
&& !activeInlineCompletion.isSmallerThanOriginal()
&& !updatedCompletions.has(activeInlineCompletion.toInlineCompletion())
) {
completions.prepend(activeInlineCompletion.inlineCompletion, activeInlineCompletion.updatedRange, true);
}
transaction(tx => {
target.set(completions, tx);
});
this.updateOperation.clear();
return true;
})();
const updateOperation = new UpdateOperation(request, source, promise);
this.updateOperation.value = updateOperation;
return promise;
}
}
function wait(ms: number, cancellationToken?: CancellationToken): Promise<void> {
return new Promise(resolve => {
let d: IDisposable | undefined = undefined;
const handle = setTimeout(() => {
if (d) { d.dispose(); }
resolve();
}, ms);
if (cancellationToken) {
d = cancellationToken.onCancellationRequested(() => {
clearTimeout(handle);
if (d) { d.dispose(); }
resolve();
});
}
});
}
class UpdateRequest {
constructor(
public readonly position: Position,
public readonly context: InlineCompletionContext,
public readonly versionId: number,
) {
}
public satisfies(other: UpdateRequest): boolean {
return this.position.equals(other.position)
&& equals(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, (v1, v2) => v1.equals(v2))
&& (other.context.triggerKind === InlineCompletionTriggerKind.Automatic
|| this.context.triggerKind === InlineCompletionTriggerKind.Explicit)
&& this.versionId === other.versionId;
}
}
function equals<T>(v1: T | undefined, v2: T | undefined, equals: (v1: T, v2: T) => boolean): boolean {
if (!v1 || !v2) {
return v1 === v2;
}
return equals(v1, v2);
}
class UpdateOperation implements IDisposable {
constructor(
public readonly request: UpdateRequest,
public readonly cancellationTokenSource: CancellationTokenSource,
public readonly promise: Promise<boolean>,
) {
}
dispose() {
this.cancellationTokenSource.cancel();
}
}
export class UpToDateInlineCompletions implements IDisposable {
private lastVersionId: number = -1;
private readonly inlineCompletions: InlineCompletionWithUpdatedRange[];
private refCount = 1;
private readonly prependedInlineCompletionItems: InlineCompletionItem[] = [];
constructor(
private readonly inlineCompletionProviderResult: InlineCompletionProviderResult,
public readonly request: UpdateRequest,
private readonly textModel: ITextModel
) {
const ids = textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({
range: i.range,
options: {
description: 'inline-completion-tracking-range'
},
})));
this.inlineCompletions = inlineCompletionProviderResult.completions.map(
(i, index) => new InlineCompletionWithUpdatedRange(i, ids[index])
);
}
public prepend(inlineCompletion: InlineCompletionItem, range: Range, addRefToSource: boolean): void {
if (addRefToSource) {
inlineCompletion.source.addRef();
}
const id = this.textModel.deltaDecorations([], [{
range,
options: {
description: 'inline-completion-tracking-range'
},
}])[0];
this.inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, range));
this.prependedInlineCompletionItems.push(inlineCompletion);
}
public clone(): this {
this.refCount++;
return this;
}
public dispose(): void {
this.refCount--;
if (this.refCount === 0) {
this.textModel.deltaDecorations(this.inlineCompletions.map(i => i.decorationId), []);
this.inlineCompletionProviderResult.dispose();
for (const i of this.prependedInlineCompletionItems) {
i.source.removeRef();
}
}
}
/**
* The ranges of the inline completions are extended as the user typed.
*/
public getInlineCompletions(versionId: number): readonly InlineCompletionWithUpdatedRange[] {
if (versionId !== this.textModel.getVersionId()) {
throw new BugIndicatingError();
}
if (this.textModel.getVersionId() !== this.lastVersionId) {
this.inlineCompletions.forEach(i => i.updateRange(this.textModel));
this.lastVersionId = this.textModel.getVersionId();
}
return this.inlineCompletions;
}
}
export class InlineCompletionWithUpdatedRange {
public readonly semanticId = JSON.stringify([this.inlineCompletion.filterText, this.inlineCompletion.insertText, this.inlineCompletion.range.getStartPosition().toString()]);
private _updatedRange: Range;
public get updatedRange(): Range { return this._updatedRange; }
constructor(
public readonly inlineCompletion: InlineCompletionItem,
public readonly decorationId: string,
initialRange?: Range,
) {
this._updatedRange = initialRange ?? inlineCompletion.range;
}
public updateRange(textModel: ITextModel): void {
const range = textModel.getDecorationRange(this.decorationId);
if (!range) {
throw new BugIndicatingError();
}
this._updatedRange = range;
}
public toInlineCompletion(): InlineCompletionItem {
return this.inlineCompletion.withRange(this.updatedRange);
}
public toSingleTextEdit(): SingleTextEdit {
return new SingleTextEdit(this.updatedRange, this.inlineCompletion.insertText);
}
public toFilterTextReplacement(): SingleTextEdit {
return new SingleTextEdit(this.updatedRange, this.inlineCompletion.filterText);
}
public isVisible(model: ITextModel, cursorPosition: Position): boolean {
const minimizedReplacement = this.toFilterTextReplacement().removeCommonPrefix(model);
if (!this.inlineCompletion.range.getStartPosition().equals(this.updatedRange.getStartPosition())) {
return false;
}
if (cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber) {
return false;
}
const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF).toLowerCase();
const filterText = this.inlineCompletion.filterText.toLowerCase();
const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn);
let filterTextBefore = filterText.substring(0, cursorPosIndex);
let filterTextAfter = filterText.substring(cursorPosIndex);
let originalValueBefore = originalValue.substring(0, cursorPosIndex);
let originalValueAfter = originalValue.substring(cursorPosIndex);
const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber);
if (this.updatedRange.startColumn <= originalValueIndent) {
// Remove indentation
originalValueBefore = originalValueBefore.trimStart();
if (originalValueBefore.length === 0) {
originalValueAfter = originalValueAfter.trimStart();
}
filterTextBefore = filterTextBefore.trimStart();
if (filterTextBefore.length === 0) {
filterTextAfter = filterTextAfter.trimStart();
}
}
return filterTextBefore.startsWith(originalValueBefore)
&& !!matchesSubString(originalValueAfter, filterTextAfter);
}
public isSmallerThanOriginal(): boolean {
return length(this.updatedRange).isBefore(length(this.inlineCompletion.range));
}
}
function length(range: Range): Position {
if (range.startLineNumber === range.endLineNumber) {
return new Position(1, 1 + range.endColumn - range.startColumn);
} else {
return new Position(1 + range.endLineNumber - range.startLineNumber, range.endColumn);
}
}
@@ -0,0 +1,267 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertNever } from 'vs/base/common/assert';
import { CancellationToken } from 'vs/base/common/cancellation';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
import { Command, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from 'vs/editor/common/languages';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ITextModel } from 'vs/editor/common/model';
import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser';
export async function provideInlineCompletions(
registry: LanguageFeatureRegistry<InlineCompletionsProvider>,
position: Position,
model: ITextModel,
context: InlineCompletionContext,
token: CancellationToken = CancellationToken.None,
languageConfigurationService?: ILanguageConfigurationService,
): Promise<InlineCompletionProviderResult> {
const providers = registry.all(model);
const providerResults = await Promise.all(providers.map(async provider => {
try {
const completions = await provider.provideInlineCompletions(model, position, context, token);
return ({ provider, completions });
} catch (e) {
onUnexpectedExternalError(e);
}
return ({ provider, completions: undefined });
}));
const defaultReplaceRange = getDefaultRange(position, model);
const itemsByHash = new Map<string, InlineCompletionItem>();
const lists: InlineCompletionList[] = [];
for (const result of providerResults) {
const completions = result.completions;
if (!completions) {
continue;
}
const list = new InlineCompletionList(completions, result.provider);
lists.push(list);
for (const item of completions.items) {
const inlineCompletionItem = InlineCompletionItem.from(
item,
list,
defaultReplaceRange,
model,
languageConfigurationService
);
itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem);
}
}
return new InlineCompletionProviderResult(Array.from(itemsByHash.values()), new Set(itemsByHash.keys()), lists);
}
export class InlineCompletionProviderResult implements IDisposable {
constructor(
/**
* Free of duplicates.
*/
public readonly completions: readonly InlineCompletionItem[],
private readonly hashs: Set<string>,
private readonly providerResults: readonly InlineCompletionList[],
) { }
public has(item: InlineCompletionItem): boolean {
return this.hashs.has(item.hash());
}
dispose(): void {
for (const result of this.providerResults) {
result.removeRef();
}
}
}
/**
* A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that
* computed them.
*/
export class InlineCompletionList {
private refCount = 0;
constructor(
public readonly inlineCompletions: InlineCompletions,
public readonly provider: InlineCompletionsProvider,
) { }
addRef(): void {
this.refCount++;
}
removeRef(): void {
this.refCount--;
if (this.refCount === 0) {
this.provider.freeInlineCompletions(this.inlineCompletions);
}
}
}
export class InlineCompletionItem {
public static from(
inlineCompletion: InlineCompletion,
source: InlineCompletionList,
defaultReplaceRange: Range,
textModel: ITextModel,
languageConfigurationService: ILanguageConfigurationService | undefined,
) {
let insertText: string;
let snippetInfo: SnippetInfo | undefined;
let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange;
if (typeof inlineCompletion.insertText === 'string') {
insertText = inlineCompletion.insertText;
if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
insertText = closeBrackets(
insertText,
range.getStartPosition(),
textModel,
languageConfigurationService
);
// Modify range depending on if brackets are added or removed
const diff = insertText.length - inlineCompletion.insertText.length;
if (diff !== 0) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
}
}
snippetInfo = undefined;
} else if ('snippet' in inlineCompletion.insertText) {
const preBracketCompletionLength = inlineCompletion.insertText.snippet.length;
if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
inlineCompletion.insertText.snippet = closeBrackets(
inlineCompletion.insertText.snippet,
range.getStartPosition(),
textModel,
languageConfigurationService
);
// Modify range depending on if brackets are added or removed
const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength;
if (diff !== 0) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
}
}
const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet);
if (snippet.children.length === 1 && snippet.children[0] instanceof Text) {
insertText = snippet.children[0].value;
snippetInfo = undefined;
} else {
insertText = snippet.toString();
snippetInfo = {
snippet: inlineCompletion.insertText.snippet,
range: range
};
}
} else {
assertNever(inlineCompletion.insertText);
}
return new InlineCompletionItem(
insertText,
inlineCompletion.command,
range,
insertText,
snippetInfo,
inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(),
inlineCompletion,
source,
);
}
constructor(
readonly filterText: string,
readonly command: Command | undefined,
readonly range: Range,
readonly insertText: string,
readonly snippetInfo: SnippetInfo | undefined,
readonly additionalTextEdits: readonly ISingleEditOperation[],
/**
* A reference to the original inline completion this inline completion has been constructed from.
* Used for event data to ensure referential equality.
*/
readonly sourceInlineCompletion: InlineCompletion,
/**
* A reference to the original inline completion list this inline completion has been constructed from.
* Used for event data to ensure referential equality.
*/
readonly source: InlineCompletionList,
) {
filterText = filterText.replace(/\r\n|\r/g, '\n');
insertText = filterText.replace(/\r\n|\r/g, '\n');
}
public withRange(updatedRange: Range): InlineCompletionItem {
return new InlineCompletionItem(
this.filterText,
this.command,
updatedRange,
this.insertText,
this.snippetInfo,
this.additionalTextEdits,
this.sourceInlineCompletion,
this.source,
);
}
public hash(): string {
return JSON.stringify({ insertText: this.insertText, range: this.range.toString() });
}
public toSingleTextEdit(): SingleTextEdit {
return new SingleTextEdit(this.range, this.insertText);
}
}
export interface SnippetInfo {
snippet: string;
/* Could be different than the main range */
range: Range;
}
function getDefaultRange(position: Position, model: ITextModel): Range {
const word = model.getWordAtPosition(position);
const maxColumn = model.getLineMaxColumn(position.lineNumber);
// By default, always replace up until the end of the current line.
// This default might be subject to change!
return word
? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn)
: Range.fromPositions(position, position.with(undefined, maxColumn));
}
function closeBrackets(text: string, position: Position, model: ITextModel, languageConfigurationService: ILanguageConfigurationService): string {
const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1);
const newLine = lineStart + text;
const newTokens = model.tokenization.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text);
const slicedTokens = newTokens?.sliceAndInflate(position.column - 1, newLine.length, 0);
if (!slicedTokens) {
return text;
}
const newText = fixBracketsInLine(slicedTokens, languageConfigurationService);
return newText;
}
@@ -0,0 +1,247 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff';
import { commonPrefixLength, getLeadingWhitespace, splitLines } from 'vs/base/common/strings';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { addPositions, lengthOfText } from 'vs/editor/contrib/inlineCompletions/browser/utils';
export class SingleTextEdit {
constructor(
public readonly range: Range,
public readonly text: string
) {
}
removeCommonPrefix(model: ITextModel, validModelRange?: Range): SingleTextEdit {
const modelRange = validModelRange ? this.range.intersectRanges(validModelRange) : this.range;
if (!modelRange) {
return this;
}
const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF);
const commonPrefixLen = commonPrefixLength(valueToReplace, this.text);
const start = addPositions(this.range.getStartPosition(), lengthOfText(valueToReplace.substring(0, commonPrefixLen)));
const text = this.text.substring(commonPrefixLen);
const range = Range.fromPositions(start, this.range.getEndPosition());
return new SingleTextEdit(range, text);
}
augments(base: SingleTextEdit): boolean {
// The augmented completion must replace the base range, but can replace even more
return this.text.startsWith(base.text) && rangeExtends(this.range, base.range);
}
/**
* @param previewSuffixLength Sets where to split `inlineCompletion.text`.
* If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
*/
computeGhostText(
model: ITextModel,
mode: 'prefix' | 'subword' | 'subwordSmart',
cursorPosition?: Position,
previewSuffixLength = 0
): GhostText | undefined {
let edit = this.removeCommonPrefix(model);
if (edit.range.endLineNumber !== edit.range.startLineNumber) {
// This edit might span multiple lines, but the first lines must be a common prefix.
return undefined;
}
const sourceLine = model.getLineContent(edit.range.startLineNumber);
const sourceIndentationLength = getLeadingWhitespace(sourceLine).length;
const suggestionTouchesIndentation = edit.range.startColumn - 1 <= sourceIndentationLength;
if (suggestionTouchesIndentation) {
// source: ··········[······abc]
// ^^^^^^^^^ inlineCompletion.range
// ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
// ^^^^^^ replacedIndentation.length
// ^^^ rangeThatDoesNotReplaceIndentation
// inlineCompletion.text: '··foo'
// ^^ suggestionAddedIndentationLength
const suggestionAddedIndentationLength = getLeadingWhitespace(edit.text).length;
const replacedIndentation = sourceLine.substring(edit.range.startColumn - 1, sourceIndentationLength);
const rangeThatDoesNotReplaceIndentation = Range.fromPositions(
edit.range.getStartPosition().delta(0, replacedIndentation.length),
edit.range.getEndPosition()
);
const suggestionWithoutIndentationChange =
edit.text.startsWith(replacedIndentation)
// Adds more indentation without changing existing indentation: We can add ghost text for this
? edit.text.substring(replacedIndentation.length)
// Changes or removes existing indentation. Only add ghost text for the non-indentation part.
: edit.text.substring(suggestionAddedIndentationLength);
edit = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange);
}
// This is a single line string
const valueToBeReplaced = model.getValueInRange(edit.range);
const changes = cachingDiff(valueToBeReplaced, edit.text);
if (!changes) {
// No ghost text in case the diff would be too slow to compute
return undefined;
}
const lineNumber = edit.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;
}
}
const previewStartInCompletionText = edit.text.length - previewSuffixLength;
for (const c of changes) {
const insertColumn = edit.range.startColumn + c.originalStart + c.originalLength;
if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === edit.range.startLineNumber && insertColumn < cursorPosition.column) {
// No ghost text before cursor
return undefined;
}
if (c.originalLength > 0) {
return undefined;
}
if (c.modifiedLength === 0) {
continue;
}
const modifiedEnd = c.modifiedStart + c.modifiedLength;
const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
const nonPreviewText = edit.text.substring(c.modifiedStart, nonPreviewTextEnd);
const italicText = edit.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
if (nonPreviewText.length > 0) {
const lines = splitLines(nonPreviewText);
parts.push(new GhostTextPart(insertColumn, lines, false));
}
if (italicText.length > 0) {
const lines = splitLines(italicText);
parts.push(new GhostTextPart(insertColumn, lines, true));
}
}
return new GhostText(lineNumber, parts, 0);
}
}
function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean {
return rangeToExtend.getStartPosition().equals(extendingRange.getStartPosition())
&& rangeToExtend.getEndPosition().isBeforeOrEqual(extendingRange.getEndPosition());
}
let lastRequest: { originalValue: string; newValue: string; changes: readonly IDiffChange[] | undefined } | undefined = undefined;
function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] | undefined {
if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) {
return lastRequest?.changes;
} else {
let changes = smartDiff(originalValue, newValue, true);
if (changes) {
const deletedChars = deletedCharacters(changes);
if (deletedChars > 0) {
// For performance reasons, don't compute diff if there is nothing to improve
const newChanges = smartDiff(originalValue, newValue, false);
if (newChanges && deletedCharacters(newChanges) < deletedChars) {
// Disabling smartness seems to be better here
changes = newChanges;
}
}
}
lastRequest = {
originalValue,
newValue,
changes
};
return changes;
}
}
function deletedCharacters(changes: readonly IDiffChange[]): number {
let sum = 0;
for (const c of changes) {
sum += c.originalLength;
}
return sum;
}
/**
* 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, smartBracketMatching: boolean): (readonly IDiffChange[]) | undefined {
if (originalValue.length > 5000 || newValue.length > 5000) {
// We don't want to work on strings that are too big
return undefined;
}
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++) {
// TODO support more brackets
if (smartBracketMatching && source[i] === '(') {
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id);
level++;
} else if (smartBracketMatching && source[i] === ')') {
level = Math.max(level - 1, 0);
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id + 1);
if (level === 0) {
group++;
}
} 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;
}
@@ -3,54 +3,37 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, findMaxBy, numberComparator } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CompletionItemInsertTextRule, CompletionItemKind } from 'vs/editor/common/languages';
import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from 'vs/editor/common/languages';
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession';
import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { minimizeInlineCompletion, NormalizedInlineCompletion, normalizedInlineCompletionsEquals } from './inlineCompletionToGhostText';
import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { ITextModel } from 'vs/editor/common/model';
import { compareBy, findMaxBy, numberComparator } from 'vs/base/common/arrays';
export interface SuggestWidgetState {
/**
* Represents the currently selected item in the suggest widget as inline completion, if possible.
*/
selectedItem: SuggestItemInfo | undefined;
}
export interface SuggestItemInfo {
normalizedInlineCompletion: NormalizedInlineCompletion;
isSnippetText: boolean;
completionItemKind: CompletionItemKind;
}
export class SuggestWidgetInlineCompletionProvider extends Disposable {
export class SuggestWidgetAdaptor extends Disposable {
private isSuggestWidgetVisible: boolean = false;
private isShiftKeyPressed = false;
private _isActive = false;
private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined;
private readonly onDidChangeEmitter = new Emitter<void>();
public readonly onDidChange = this.onDidChangeEmitter.event;
private readonly _selectedItem = observableValue('suggestWidgetInlineCompletionProvider.selectedItem', undefined as SuggestItemInfo | undefined);
/**
* Returns undefined if the suggest widget is not active.
*/
get state(): SuggestWidgetState | undefined {
if (!this._isActive) {
return undefined;
}
return { selectedItem: this._currentSuggestItemInfo };
public get selectedItem(): IObservable<SuggestItemInfo | undefined> {
return this._selectedItem;
}
constructor(
private readonly editor: IActiveCodeEditor,
private readonly suggestControllerPreselector: () => NormalizedInlineCompletion | undefined
private readonly editor: ICodeEditor,
private readonly suggestControllerPreselector: () => SingleTextEdit | undefined,
private readonly checkModelVersion: (tx: ITransaction) => void,
) {
super();
@@ -73,26 +56,28 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
this._register(suggestController.registerSelector({
priority: 100,
select: (model, pos, suggestItems) => {
transaction(tx => this.checkModelVersion(tx));
const textModel = this.editor.getModel();
const normalizedItemToPreselect = minimizeInlineCompletion(textModel, this.suggestControllerPreselector());
if (!normalizedItemToPreselect) {
if (!textModel) {
// Should not happen
return -1;
}
const itemToPreselect = this.suggestControllerPreselector()?.removeCommonPrefix(textModel);
if (!itemToPreselect) {
return -1;
}
const position = Position.lift(pos);
const candidates = suggestItems
.map((suggestItem, index) => {
const inlineSuggestItem = suggestionToSuggestItemInfo(suggestController, position, suggestItem, this.isShiftKeyPressed);
const normalizedSuggestItem = minimizeInlineCompletion(textModel, inlineSuggestItem?.normalizedInlineCompletion);
if (!normalizedSuggestItem) {
return undefined;
}
const valid =
rangeStartsWith(normalizedItemToPreselect.range, normalizedSuggestItem.range) &&
normalizedItemToPreselect.insertText.startsWith(normalizedSuggestItem.insertText);
return { index, valid, prefixLength: normalizedSuggestItem.insertText.length, suggestItem };
const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed);
const suggestItemTextEdit = suggestItemInfo.toSingleTextEdit().removeCommonPrefix(textModel);
const valid = itemToPreselect.augments(suggestItemTextEdit);
return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem };
})
.filter(item => item && item.valid);
.filter(item => item && item.valid && item.prefixLength > 0);
const result = findMaxBy(
candidates,
@@ -132,37 +117,36 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
private update(newActive: boolean): void {
const newInlineCompletion = this.getSuggestItemInfo();
let shouldFire = false;
if (!suggestItemInfoEquals(this._currentSuggestItemInfo, newInlineCompletion)) {
this._currentSuggestItemInfo = newInlineCompletion;
shouldFire = true;
}
if (this._isActive !== newActive) {
if (this._isActive !== newActive || !suggestItemInfoEquals(this._currentSuggestItemInfo, newInlineCompletion)) {
this._isActive = newActive;
shouldFire = true;
}
if (shouldFire) {
this.onDidChangeEmitter.fire();
this._currentSuggestItemInfo = newInlineCompletion;
transaction(tx => {
this.checkModelVersion(tx);
this._selectedItem.set(this._isActive ? this._currentSuggestItemInfo : undefined, tx);
});
}
}
private getSuggestItemInfo(): SuggestItemInfo | undefined {
const suggestController = SuggestController.get(this.editor);
if (!suggestController) {
return undefined;
}
if (!this.isSuggestWidgetVisible) {
return undefined;
}
const focusedItem = suggestController.widget.value.getFocusedItem();
if (!focusedItem) {
if (!suggestController || !this.isSuggestWidgetVisible) {
return undefined;
}
// TODO: item.isResolved
return suggestionToSuggestItemInfo(
const focusedItem = suggestController.widget.value.getFocusedItem();
const position = this.editor.getPosition();
const model = this.editor.getModel();
if (!focusedItem || !position || !model) {
return undefined;
}
return SuggestItemInfo.fromSuggestion(
suggestController,
this.editor.getPosition(),
model,
position,
focusedItem.item,
this.isShiftKeyPressed
);
@@ -179,14 +163,56 @@ export class SuggestWidgetInlineCompletionProvider extends Disposable {
}
}
export function rangeStartsWith(rangeToTest: Range, prefix: Range): boolean {
return (
prefix.startLineNumber === rangeToTest.startLineNumber &&
prefix.startColumn === rangeToTest.startColumn &&
(prefix.endLineNumber < rangeToTest.endLineNumber ||
(prefix.endLineNumber === rangeToTest.endLineNumber &&
prefix.endColumn <= rangeToTest.endColumn))
);
export class SuggestItemInfo {
public static fromSuggestion(suggestController: SuggestController, model: ITextModel, position: Position, item: CompletionItem, toggleMode: boolean): SuggestItemInfo {
let { insertText } = item.completion;
let isSnippetText = false;
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
const snippet = new SnippetParser().parse(insertText);
if (snippet.children.length < 100) {
// Adjust whitespace is expensive.
SnippetSession.adjustWhitespace(model, position, true, snippet);
}
insertText = snippet.toString();
isSnippetText = true;
}
const info = suggestController.getOverwriteInfo(item, toggleMode);
return new SuggestItemInfo(
Range.fromPositions(
position.delta(0, -info.overwriteBefore),
position.delta(0, Math.max(info.overwriteAfter, 0))
),
insertText,
item.completion.kind,
isSnippetText,
);
}
private constructor(
public readonly range: Range,
public readonly insertText: string,
public readonly completionItemKind: CompletionItemKind,
public readonly isSnippetText: boolean,
) { }
public equals(other: SuggestItemInfo): boolean {
return this.range.equalsRange(other.range)
&& this.insertText === other.insertText
&& this.completionItemKind === other.completionItemKind
&& this.isSnippetText === other.isSnippetText;
}
public toSelectedSuggestionInfo(): SelectedSuggestionInfo {
return new SelectedSuggestionInfo(this.range, this.insertText, this.completionItemKind, this.isSnippetText);
}
public toSingleTextEdit(): SingleTextEdit {
return new SingleTextEdit(this.range, this.insertText);
}
}
function suggestItemInfoEquals(a: SuggestItemInfo | undefined, b: SuggestItemInfo | undefined): boolean {
@@ -196,59 +222,5 @@ function suggestItemInfoEquals(a: SuggestItemInfo | undefined, b: SuggestItemInf
if (!a || !b) {
return false;
}
return a.completionItemKind === b.completionItemKind &&
a.isSnippetText === b.isSnippetText &&
normalizedInlineCompletionsEquals(a.normalizedInlineCompletion, b.normalizedInlineCompletion);
}
function suggestionToSuggestItemInfo(suggestController: SuggestController, position: Position, item: CompletionItem, toggleMode: boolean): SuggestItemInfo | undefined {
// 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. TODO: Now we can.
return {
completionItemKind: item.completion.kind,
isSnippetText: false,
normalizedInlineCompletion: {
// Dummy element, so that space is reserved, but no text is shown
range: Range.fromPositions(position, position),
insertText: '',
filterText: '',
snippetInfo: undefined,
additionalTextEdits: [],
},
};
}
let { insertText } = item.completion;
let isSnippetText = false;
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
const snippet = new SnippetParser().parse(insertText);
const model = suggestController.editor.getModel()!;
// Ignore snippets that are too large.
// Adjust whitespace is expensive for them.
if (snippet.children.length > 100) {
return undefined;
}
SnippetSession.adjustWhitespace(model, position, true, snippet);
insertText = snippet.toString();
isSnippetText = true;
}
const info = suggestController.getOverwriteInfo(item, toggleMode);
return {
isSnippetText,
completionItemKind: item.completion.kind,
normalizedInlineCompletion: {
insertText: insertText,
filterText: insertText,
range: Range.fromPositions(
position.delta(0, -info.overwriteBefore),
position.delta(0, Math.max(info.overwriteAfter, 0))
),
snippetInfo: undefined,
additionalTextEdits: [],
}
};
return a.equals(b);
}
@@ -1,198 +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 { 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 { CompletionItemKind, InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/languages';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel';
import { BaseGhostTextWidgetModel, GhostText } from './ghostText';
import { provideInlineCompletions, TrackedInlineCompletions, UpdateOperation } from './inlineCompletionsModel';
import { inlineCompletionToGhostText, minimizeInlineCompletion, NormalizedInlineCompletion } from './inlineCompletionToGhostText';
import { SuggestWidgetInlineCompletionProvider } from './suggestWidgetInlineCompletionProvider';
export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel {
private readonly suggestionInlineCompletionSource = this._register(
new SuggestWidgetInlineCompletionProvider(
this.editor,
// Use the first cache item (if any) as preselection.
() => {
// We might get asked in a content change event before the cache has received that event.
this.cache.value?.updateRanges();
return this.cache.value?.completions[0]?.toLiveInlineCompletion();
}
)
);
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,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
) {
super(editor);
this._register(this.suggestionInlineCompletionSource.onDidChange(() => {
if (!this.editor.hasModel()) {
// onDidChange might be called when calling setModel on the editor, before we are disposed.
return;
}
this.updateCacheSoon.schedule();
const suggestWidgetState = this.suggestionInlineCompletionSource.state;
if (!suggestWidgetState) {
this.minReservedLineCount = 0;
}
const newGhostText = this.ghostText;
if (newGhostText) {
this.minReservedLineCount = Math.max(this.minReservedLineCount, sum(newGhostText.parts.map(p => p.lines.length - 1)));
}
if (this.minReservedLineCount >= 1) {
this.suggestionInlineCompletionSource.forceRenderingAbove();
} else {
this.suggestionInlineCompletionSource.stopForceRenderingAbove();
}
this.onDidChangeEmitter.fire();
}));
this._register(this.cache.onDidChange(() => {
this.onDidChangeEmitter.fire();
}));
this._register(this.editor.onDidChangeCursorPosition((e) => {
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.selectedItem) {
return;
}
const info: SelectedSuggestionInfo = {
text: state.selectedItem.normalizedInlineCompletion.insertText,
range: state.selectedItem.normalizedInlineCompletion.range,
isSnippetText: state.selectedItem.isSnippetText,
completionKind: state.selectedItem.completionItemKind,
};
const position = this.editor.getPosition();
if (
state.selectedItem.isSnippetText ||
state.selectedItem.completionItemKind === CompletionItemKind.Snippet ||
state.selectedItem.completionItemKind === CompletionItemKind.File ||
state.selectedItem.completionItemKind === CompletionItemKind.Folder
) {
// Don't ask providers for these types of suggestions.
this.cache.clear();
return;
}
const promise = createCancelablePromise(async token => {
let result: TrackedInlineCompletions;
try {
result = await provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, position,
this.editor.getModel(),
{ triggerKind: InlineCompletionTriggerKind.Automatic, selectedSuggestionInfo: info },
token
);
} catch (e) {
onUnexpectedError(e);
return;
}
if (token.isCancellationRequested) {
result.dispose();
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 {
const isSuggestionPreviewEnabled = this.isSuggestionPreviewEnabled();
const model = this.editor.getModel();
const augmentedCompletion = minimizeInlineCompletion(model, this.cache.value?.completions[0]?.toLiveInlineCompletion());
const suggestWidgetState = this.suggestionInlineCompletionSource.state;
const suggestInlineCompletion = minimizeInlineCompletion(model, suggestWidgetState?.selectedItem?.normalizedInlineCompletion);
const isAugmentedCompletionValid = augmentedCompletion
&& suggestInlineCompletion
// The intellisense completion must be a prefix of the augmented completion
&& augmentedCompletion.insertText.startsWith(suggestInlineCompletion.insertText)
// The augmented completion must replace the intellisense completion range, but can replace even more
&& rangeExtends(augmentedCompletion.range, suggestInlineCompletion.range);
if (!isSuggestionPreviewEnabled && !isAugmentedCompletionValid) {
return undefined;
}
// If the augmented completion is not valid and there is no suggest inline completion, we still show the augmented completion.
const finalCompletion = isAugmentedCompletionValid ? augmentedCompletion : (suggestInlineCompletion || augmentedCompletion);
const inlineCompletionPreviewLength = isAugmentedCompletionValid ? finalCompletion!.insertText.length - suggestInlineCompletion.insertText.length : 0;
const newGhostText = this.toGhostText(finalCompletion, inlineCompletionPreviewLength);
return newGhostText;
}
private toGhostText(completion: NormalizedInlineCompletion | undefined, inlineCompletionPreviewLength: number): 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;
}
}
function sum(arr: number[]): number {
return arr.reduce((a, b) => a + b, 0);
}
function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean {
return extendingRange.startLineNumber === rangeToExtend.startLineNumber &&
extendingRange.startColumn === rangeToExtend.startColumn &&
((extendingRange.endLineNumber === rangeToExtend.endLineNumber && extendingRange.endColumn >= rangeToExtend.endColumn)
|| extendingRange.endLineNumber > rangeToExtend.endLineNumber);
}
@@ -3,16 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
import { BugIndicatingError } from 'vs/base/common/errors';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun } from 'vs/base/common/observable';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
export function createDisposableRef<T>(object: T, disposable?: IDisposable): IReference<T> {
return {
object,
dispose: () => disposable?.dispose(),
};
}
import { IModelDeltaDecoration } from 'vs/editor/common/model';
export function applyEdits(text: string, edits: { range: IRange; text: string }[]): string {
const transformer = new PositionOffsetTransformer(text);
@@ -56,3 +53,55 @@ const array: ReadonlyArray<any> = [];
export function getReadonlyEmptyArray<T>(): readonly T[] {
return array;
}
export class ColumnRange {
constructor(
public readonly startColumn: number,
public readonly endColumnExclusive: number,
) {
if (startColumn > endColumnExclusive) {
throw new BugIndicatingError(`startColumn ${startColumn} cannot be after endColumnExclusive ${endColumnExclusive}`);
}
}
toRange(lineNumber: number): Range {
return new Range(lineNumber, this.startColumn, lineNumber, this.endColumnExclusive);
}
}
export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
const d = new DisposableStore();
let decorationIds: string[] = [];
d.add(autorun(`Apply decorations from ${decorations.debugName}`, reader => {
const d = decorations.read(reader);
editor.changeDecorations(a => {
decorationIds = a.deltaDecorations(decorationIds, d);
});
}));
d.add({
dispose: () => {
editor.changeDecorations(a => {
decorationIds = a.deltaDecorations(decorationIds, []);
});
}
});
return d;
}
export function addPositions(pos1: Position, pos2: Position): Position {
return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column);
}
export function lengthOfText(text: string): Position {
let line = 1;
let column = 1;
for (const c of text) {
if (c === '\n') {
line++;
column = 1;
} else {
column++;
}
}
return new Position(line, column);
}
@@ -9,17 +9,18 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { Range } from 'vs/editor/common/core/range';
import { InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages';
import { InlineCompletionsProvider } from 'vs/editor/common/languages';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit';
import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/browser/utils';
import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { inlineCompletionToGhostText } from '../../browser/inlineCompletionToGhostText';
suite('Inline Completions', () => {
ensureNoDisposablesAreLeakedInTestSuite();
@@ -35,11 +36,7 @@ suite('Inline Completions', () => {
const options = ['prefix', 'subword'] as const;
const result = {} as any;
for (const option of options) {
result[option] = inlineCompletionToGhostText(
{ insertText: suggestion, filterText: suggestion, snippetInfo: undefined, range, additionalTextEdits: [], },
tempModel,
option
)?.render(cleanedText, true);
result[option] = new SingleTextEdit(range, suggestion).computeGhostText(tempModel, option)?.render(cleanedText, true);
}
tempModel.dispose();
@@ -80,8 +77,12 @@ suite('Inline Completions', () => {
assert.deepStrictEqual(getOutput('bar[\tfoo]', 'foobar'), undefined);
});
test('Unsupported cases', () => {
assert.deepStrictEqual(getOutput('foo[\n]', '\n'), undefined);
test('Unsupported Case', () => {
assert.deepStrictEqual(getOutput('fo[o\n]', 'x\nbar'), undefined);
});
test('New Line', () => {
assert.deepStrictEqual(getOutput('fo[o\n]', 'o\nbar'), 'foo\n[bar]');
});
test('Multi Part Diffing', () => {
@@ -115,8 +116,6 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, inlineSuggest: { enabled: false } },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
await timeout(1000);
@@ -132,11 +131,9 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
@@ -152,7 +149,6 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, inlineSuggest: { enabled: true } },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
@@ -171,11 +167,9 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
context.keyboardType('foo');
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
provider.setReturnValue({ insertText: 'foobizz', range: new Range(1, 1, 1, 6) });
@@ -200,16 +194,14 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType(' ');
provider.setReturnValue({ insertText: 'foo', range: new Range(1, 2, 1, 3) });
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', ' [foo]']);
model.commitCurrentSuggestion();
model.accept(editor);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,3)', text: ' ', triggerKind: 1, },
@@ -225,16 +217,14 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('\t\t');
provider.setReturnValue({ insertText: 'foo', range: new Range(1, 2, 1, 3) });
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', '\t\t[foo]']);
model.commitCurrentSuggestion();
model.accept(editor);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,3)', text: '\t\t', triggerKind: 1, },
@@ -250,16 +240,14 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('buzz ');
provider.setReturnValue({ insertText: 'foo', range: new Range(1, 6, 1, 7) });
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'buzz ']);
assert.deepStrictEqual(context.getAndClearViewStates(), ['']);
model.commitCurrentSuggestion();
model.accept(editor);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,7)', text: 'buzz ', triggerKind: 1, },
@@ -275,11 +263,9 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
provider.setReturnValue({ insertText: 'foobar1', range: new Range(1, 1, 1, 4) });
model.trigger(InlineCompletionTriggerKind.Automatic);
model.trigger();
await timeout(1000);
assert.deepStrictEqual(
@@ -293,27 +279,27 @@ suite('Inline Completions', () => {
{ insertText: 'foobuzz3', range: new Range(1, 1, 1, 4) }
]);
model.showNext();
model.next();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[bizz2]']);
model.showNext();
model.next();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[buzz3]']);
model.showNext();
model.next();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[bar1]']);
model.showPrevious();
model.previous();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[buzz3]']);
model.showPrevious();
model.previous();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[bizz2]']);
model.showPrevious();
model.previous();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['foo[bar1]']);
@@ -330,8 +316,7 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
model.trigger(InlineCompletionTriggerKind.Automatic);
model.trigger();
context.keyboardType('f');
await timeout(40);
@@ -358,8 +343,6 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, inlineSuggest: { enabled: true } },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('foo');
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
@@ -387,11 +370,9 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
context.keyboardType('foo');
model.trigger(InlineCompletionTriggerKind.Automatic);
model.trigger();
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,4)', text: 'foo', triggerKind: 0, }
@@ -426,10 +407,9 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
context.keyboardType('foo');
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(100);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,4)', text: 'foo', triggerKind: 1, }
@@ -455,13 +435,11 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('fooba');
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 6) });
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
{ position: '(1,6)', text: 'fooba', triggerKind: 1, }
@@ -487,11 +465,10 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('h');
provider.setReturnValue({ insertText: 'helloworld', range: new Range(1, 1, 1, 2) }, 1000);
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1030);
context.keyboardType('ello');
@@ -513,9 +490,10 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider, inlineSuggest: { enabled: true } },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('hello\n');
context.cursorLeft();
context.keyboardType('x');
context.leftDelete();
provider.setReturnValue({ insertText: 'helloworld', range: new Range(1, 1, 1, 6) }, 1000);
await timeout(2000);
@@ -559,8 +537,6 @@ suite('Inline Completions', () => {
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
model.setActive(true);
context.keyboardType('buzz\nbaz');
provider.setReturnValue({
insertText: 'bazz',
@@ -570,10 +546,10 @@ suite('Inline Completions', () => {
text: 'bla'
}],
});
model.trigger(InlineCompletionTriggerKind.Explicit);
model.triggerExplicitly();
await timeout(1000);
model.commitCurrentSuggestion();
model.accept(editor);
assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(2,4)", triggerKind: 1, text: "buzz\nbaz" }]));
@@ -597,7 +573,6 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
}, async () => {
const disposableStore = new DisposableStore();
try {
if (options.provider) {
const languageFeaturesService = new LanguageFeaturesService();
@@ -605,20 +580,25 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
options.serviceCollection = new ServiceCollection();
}
options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);
options.serviceCollection.set(IAudioCueService, {
playAudioCue: async () => { },
isEnabled(cue: unknown) { return false; },
} as any);
const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider);
disposableStore.add(d);
}
let result: T;
await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {
const cache = disposableStore.add(new SharedInlineCompletionCache());
const model = instantiationService.createInstance(InlineCompletionsModel, editor, cache);
const controller = instantiationService.createInstance(InlineCompletionsController, editor);
const model = controller.model.get()!;
const context = new GhostTextContext(model, editor);
try {
result = await callback({ editor, editorViewModel, model, context });
} finally {
context.dispose();
model.dispose();
controller.dispose();
}
});
@@ -12,8 +12,6 @@ import { Range } from 'vs/editor/common/core/range';
import { CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/languages';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel';
import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel';
import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/browser/utils';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
@@ -28,24 +26,21 @@ import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/com
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import * as assert from 'assert';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { rangeStartsWith } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { minimizeInlineCompletion } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { autorun } from 'vs/base/common/observable';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
suite('Suggest Widget Model', () => {
test('rangeStartsWith', () => {
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 1, 1)), true);
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 10, 5)), true);
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 10, 4)), true);
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 9, 6)), true);
assert.strictEqual(rangeStartsWith(new Range(2, 1, 10, 5), new Range(1, 1, 10, 5)), false);
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 10, 6)), false);
assert.strictEqual(rangeStartsWith(new Range(1, 1, 10, 5), new Range(1, 1, 11, 4)), false);
setup(() => {
setUnexpectedErrorHandler(function (err) {
throw err;
});
});
// This test is skipped because the fix for this causes https://github.com/microsoft/vscode/issues/166023
@@ -55,9 +50,10 @@ suite('Suggest Widget Model', () => {
async ({ editor, editorViewModel, context, model }) => {
let last: boolean | undefined = undefined;
const history = new Array<boolean>();
model.onDidChange(() => {
if (last !== model.isActive) {
last = model.isActive;
const d = autorun('debug', reader => {
const selectedSuggestItem = !!model.selectedSuggestItem.read(reader);
if (last !== selectedSuggestItem) {
last = selectedSuggestItem;
history.push(last);
}
});
@@ -77,6 +73,8 @@ suite('Suggest Widget Model', () => {
await timeout(1000);
assert.deepStrictEqual(history.splice(0), [false]);
d.dispose();
}
);
});
@@ -89,11 +87,11 @@ suite('Suggest Widget Model', () => {
const suggestController = (editor.getContribution(SuggestController.ID) as SuggestController);
suggestController.triggerSuggest();
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h', 'h[ello]']);
assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h[ello]']);
context.keyboardType('.');
await timeout(1000);
assert.deepStrictEqual(context.getAndClearViewStates(), ['h', 'hello', 'hello.', 'hello.[hello]']);
assert.deepStrictEqual(context.getAndClearViewStates(), ['h', 'hello.[hello]']);
suggestController.cancelSuggestWidget();
@@ -102,27 +100,6 @@ suite('Suggest Widget Model', () => {
}
);
});
test('minimizeInlineCompletion', async () => {
const model = createTextModel('fun');
const result = minimizeInlineCompletion(model, {
range: new Range(1, 1, 1, 4),
filterText: 'function',
insertText: 'function',
snippetInfo: undefined,
additionalTextEdits: [],
})!;
assert.deepStrictEqual({
range: result.range.toString(),
text: result.insertText
}, {
range: '[1,4 -> 1,4]',
text: 'ction'
});
model.dispose();
});
});
const provider: CompletionItemProvider = {
@@ -148,7 +125,7 @@ const provider: CompletionItemProvider = {
async function withAsyncTestCodeEditorAndInlineCompletionsModel(
text: string,
options: TestCodeEditorInstantiationOptions & { provider?: CompletionItemProvider; fakeClock?: boolean; serviceCollection?: never },
callback: (args: { editor: ITestCodeEditor; editorViewModel: ViewModel; model: SuggestWidgetPreviewModel; context: GhostTextContext }) => Promise<void>
callback: (args: { editor: ITestCodeEditor; editorViewModel: ViewModel; model: InlineCompletionsModel; context: GhostTextContext }) => Promise<void>
): Promise<void> {
await runWithFakedTimers({ useFakeTimers: options.fakeClock }, async () => {
const disposableStore = new DisposableStore();
@@ -178,6 +155,10 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel(
}],
[ILabelService, new class extends mock<ILabelService>() { }],
[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }],
[IAudioCueService, {
playAudioCue: async () => { },
isEnabled(cue: unknown) { return false; },
} as any]
);
if (options.provider) {
@@ -190,11 +171,12 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel(
await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => {
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController);
const cache = disposableStore.add(new SharedInlineCompletionCache());
const model = instantiationService.createInstance(SuggestWidgetPreviewModel, editor, cache);
editor.registerAndInstantiateContribution(InlineCompletionsController.ID, InlineCompletionsController);
const model = InlineCompletionsController.get(editor)?.model.get()!;
const context = new GhostTextContext(model, editor);
await callback({ editor, editorViewModel, model, context });
model.dispose();
context.dispose();
});
} finally {
disposableStore.dispose();
@@ -10,8 +10,9 @@ import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/c
import { Position } from 'vs/editor/common/core/position';
import { ITextModel } from 'vs/editor/common/model';
import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } from 'vs/editor/common/languages';
import { GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText';
import { ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { autorun } from 'vs/base/common/observable';
export class MockInlineCompletionsProvider implements InlineCompletionsProvider {
private returnValue: InlineCompletion[] = [];
@@ -76,30 +77,23 @@ export class GhostTextContext extends Disposable {
return this._currentPrettyViewState;
}
constructor(private readonly model: GhostTextWidgetModel, private readonly editor: ITestCodeEditor) {
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
super();
this._register(
model.onDidChange(() => {
this.update();
})
);
this.update();
}
this._register(autorun('update', reader => {
const ghostText = model.ghostText.read(reader);
let view: string | undefined;
if (ghostText) {
view = ghostText.render(this.editor.getValue(), true);
} else {
view = this.editor.getValue();
}
private update(): void {
const ghostText = this.model?.ghostText;
let view: string | undefined;
if (ghostText) {
view = ghostText.render(this.editor.getValue(), true);
} else {
view = this.editor.getValue();
}
if (this._currentPrettyViewState !== view) {
this.prettyViewStates.push(view);
}
this._currentPrettyViewState = view;
if (this._currentPrettyViewState !== view) {
this.prettyViewStates.push(view);
}
this._currentPrettyViewState = view;
}));
}
public getAndClearViewStates(): (string | undefined)[] {
+1 -1
View File
@@ -26,7 +26,7 @@ import 'vs/editor/contrib/folding/browser/folding';
import 'vs/editor/contrib/fontZoom/browser/fontZoom';
import 'vs/editor/contrib/format/browser/formatActions';
import 'vs/editor/contrib/documentSymbols/browser/documentSymbols';
import 'vs/editor/contrib/inlineCompletions/browser/ghostText.contribution';
import 'vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution';
import 'vs/editor/contrib/inlineProgress/browser/inlineProgress';
import 'vs/editor/contrib/gotoSymbol/browser/goToCommands';
import 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition';
@@ -791,5 +791,6 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages {
// classes
FoldingRangeKind: languages.FoldingRangeKind,
SelectedSuggestionInfo: <any>languages.SelectedSuggestionInfo,
};
}
+7 -5
View File
@@ -6786,11 +6786,13 @@ declare namespace monaco.languages {
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: IRange;
text: string;
isSnippetText: boolean;
completionKind: CompletionItemKind;
export class SelectedSuggestionInfo {
readonly range: IRange;
readonly text: string;
readonly completionKind: CompletionItemKind;
readonly isSnippetText: boolean;
constructor(range: IRange, text: string, completionKind: CompletionItemKind, isSnippetText: boolean);
equals(other: SelectedSuggestionInfo): boolean;
}
export interface InlineCompletion {
@@ -25,7 +25,7 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetCon
import { IModelService } from 'vs/editor/common/services/model';
import { URI } from 'vs/base/common/uri';
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
@@ -159,7 +159,7 @@ class InteractiveEditorWidget {
isSimpleWidget: true,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
SnippetController2.ID,
GhostTextController.ID,
InlineCompletionsController.ID,
SuggestController.ID
])
};
@@ -88,7 +88,7 @@ import { DropIntoEditorController } from 'vs/editor/contrib/dropIntoEditor/brows
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import { contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry';
import { defaultButtonStyles, defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles';
import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController';
import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
import { Schemas } from 'vs/base/common/network';
@@ -1964,7 +1964,7 @@ class SCMInputWidget {
SelectionClipboardContributionID,
SnippetController2.ID,
SuggestController.ID,
GhostTextController.ID,
InlineCompletionsController.ID,
CodeActionController.ID,
])
};