mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-01 05:02:35 +01:00
Rewrites the inline completion feature.
Fixes #172162 Fixes #174329 Fixes #172153 Fixes #164716 Fixes #170527
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
+18
-42
@@ -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
-4
@@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+102
-106
@@ -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;
|
||||
}
|
||||
+99
-127
@@ -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);
|
||||
}
|
||||
|
||||
+43
-63
@@ -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)[] {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+7
-5
@@ -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,
|
||||
])
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user