Implements experimental inline edits

Signed-off-by: Henning Dieterichs <hdieterichs@microsoft.com>
This commit is contained in:
Henning Dieterichs 2024-06-20 22:05:42 +02:00 committed by Henning Dieterichs
parent d6ffb9f68a
commit 309351259c
17 changed files with 1277 additions and 7 deletions

View file

@ -94,6 +94,7 @@
"--vscode-debugTokenExpression-name",
"--vscode-debugTokenExpression-number",
"--vscode-debugTokenExpression-string",
"--vscode-debugTokenExpression-type",
"--vscode-debugTokenExpression-value",
"--vscode-debugToolBar-background",
"--vscode-debugToolBar-border",
@ -852,6 +853,7 @@
"--z-index-notebook-scrollbar",
"--z-index-run-button-container",
"--zoom-factor",
"--test-bar-width"
"--test-bar-width",
"--widget-color"
]
}

View file

@ -2373,6 +2373,107 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
return result;
}
export function svgElem<TTag extends string>
(tag: TTag):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function svgElem<TTag extends string, T extends Child[]>
(tag: TTag, children: [...T]):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function svgElem<TTag extends string>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function svgElem<TTag extends string, T extends Child[]>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
if (Array.isArray(args[0])) {
attributes = {};
children = args[0];
} else {
attributes = args[0] as any || {};
children = args[1];
}
const match = H_REGEX.exec(tag);
if (!match || !match.groups) {
throw new Error('Bad use of h');
}
const tagName = match.groups['tag'] || 'div';
const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement;
if (match.groups['id']) {
el.id = match.groups['id'];
}
const classNames = [];
if (match.groups['class']) {
for (const className of match.groups['class'].split('.')) {
if (className !== '') {
classNames.push(className);
}
}
}
if (attributes.className !== undefined) {
for (const className of attributes.className.split('.')) {
if (className !== '') {
classNames.push(className);
}
}
}
if (classNames.length > 0) {
el.className = classNames.join(' ');
}
const result: Record<string, HTMLElement> = {};
if (match.groups['name']) {
result[match.groups['name']] = el;
}
if (children) {
for (const c of children) {
if (isHTMLElement(c)) {
el.appendChild(c);
} else if (typeof c === 'string') {
el.append(c);
} else if ('root' in c) {
Object.assign(result, c);
el.appendChild(c.root);
}
}
}
for (const [key, value] of Object.entries(attributes)) {
if (key === 'className') {
continue;
} else if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
el.style.setProperty(
camelCaseToHyphenCase(cssKey),
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
);
}
} else if (key === 'tabIndex') {
el.tabIndex = value;
} else {
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
}
}
result['root'] = el;
return result;
}
function camelCaseToHyphenCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}

View file

@ -687,6 +687,11 @@ export interface InlineCompletionContext {
*/
readonly triggerKind: InlineCompletionTriggerKind;
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
/**
* @experimental
* @internal
*/
readonly userPrompt?: string | undefined;
}
export class SelectedSuggestionInfo {
@ -765,6 +770,12 @@ export type InlineCompletionProviderGroupId = string;
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
/**
* @experimental
* @internal
*/
provideInlineEdits?(model: model.ITextModel, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
/**
* Will be called when an item is shown.
* @param updatedInsertText Is useful to understand bracket completion.

View file

@ -18,19 +18,19 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua
import { ITextModel } from 'vs/editor/common/model';
import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets';
import { SingleTextEdit } from 'vs/editor/common/core/textEdit';
import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils';
import { getReadonlyEmptyArray } from './utils';
import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser';
export async function provideInlineCompletions(
registry: LanguageFeatureRegistry<InlineCompletionsProvider>,
position: Position,
positionOrRange: Position | Range,
model: ITextModel,
context: InlineCompletionContext,
token: CancellationToken = CancellationToken.None,
languageConfigurationService?: ILanguageConfigurationService,
): Promise<InlineCompletionProviderResult> {
// Important: Don't use position after the await calls, as the model could have been changed in the meantime!
const defaultReplaceRange = getDefaultRange(position, model);
const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange;
const providers = registry.all(model);
const multiMap = new SetMap<InlineCompletionProviderGroupId, InlineCompletionsProvider<any>>();
@ -100,8 +100,13 @@ export async function provideInlineCompletions(
}
try {
const completions = await provider.provideInlineCompletions(model, position, context, token);
return completions;
if (positionOrRange instanceof Position) {
const completions = await provider.provideInlineCompletions(model, positionOrRange, context, token);
return completions;
} else {
const completions = await provider.provideInlineEdits?.(model, positionOrRange, context, token);
return completions;
}
} catch (e) {
onUnexpectedExternalError(e);
return undefined;

View file

@ -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 { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { transaction } from 'vs/base/common/observable';
import { asyncTransaction } from 'vs/base/common/observableInternal/base';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from 'vs/editor/contrib/inlineEdits/browser/consts';
import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController';
import * as nls from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
function labelAndAlias(str: nls.ILocalizedString): { label: string, alias: string } {
return {
label: str.value,
alias: str.original,
};
}
export class ShowNextInlineEditAction extends EditorAction {
public static ID = showNextInlineEditActionId;
constructor() {
super({
id: ShowNextInlineEditAction.ID,
...labelAndAlias(nls.localize2('action.inlineEdits.showNext', "Show Next Inline Edit")),
precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketRight,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineEditsController.get(editor);
controller?.model.get()?.next();
}
}
export class ShowPreviousInlineEditAction extends EditorAction {
public static ID = showPreviousInlineEditActionId;
constructor() {
super({
id: ShowPreviousInlineEditAction.ID,
...labelAndAlias(nls.localize2('action.inlineEdits.showPrevious', "Show Previous Inline Edit")),
precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible),
kbOpts: {
weight: 100,
primary: KeyMod.Alt | KeyCode.BracketLeft,
},
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineEditsController.get(editor);
controller?.model.get()?.previous();
}
}
export class TriggerInlineEditAction extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineEdits.trigger',
...labelAndAlias(nls.localize2('action.inlineEdits.trigger', "Trigger Inline Edit")),
precondition: EditorContextKeys.writable
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineEditsController.get(editor);
await asyncTransaction(async tx => {
/** @description triggerExplicitly from command */
await controller?.model.get()?.triggerExplicitly(tx);
});
}
}
export class AcceptInlineEdit extends EditorAction {
constructor() {
super({
id: inlineEditAcceptId,
...labelAndAlias(nls.localize2('action.inlineEdits.accept', "Accept Inline Edit")),
precondition: inlineEditVisible,
menuOpts: {
menuId: MenuId.InlineEditsActions,
title: nls.localize('inlineEditsActions', "Accept Inline Edit"),
group: 'primary',
order: 1,
icon: Codicon.check,
},
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.Space,
weight: 20000,
kbExpr: inlineEditVisible,
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
if (editor instanceof EmbeddedCodeEditorWidget) {
editor = editor.getParentEditor();
}
const controller = InlineEditsController.get(editor);
if (controller) {
controller.model.get()?.accept(controller.editor);
controller.editor.focus();
}
}
}
/*
TODO@hediet
export class PinInlineEdit extends EditorAction {
constructor() {
super({
id: 'editor.action.inlineEdits.pin',
...labelAndAlias(nls.localize2('action.inlineEdits.pin', "Pin Inline Edit")),
precondition: undefined,
kbOpts: {
primary: KeyMod.Shift | KeyCode.Space,
weight: 20000,
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineEditsController.get(editor);
if (controller) {
controller.model.get()?.togglePin();
}
}
}
MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, {
command: {
id: 'editor.action.inlineEdits.pin',
title: nls.localize('Pin', "Pin"),
icon: Codicon.pin,
},
group: 'primary',
order: 1,
when: isPinnedContextKey.negate(),
});
MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, {
command: {
id: 'editor.action.inlineEdits.unpin',
title: nls.localize('Unpin', "Unpin"),
icon: Codicon.pinned,
},
group: 'primary',
order: 1,
when: isPinnedContextKey,
});*/
export class HideInlineEdit extends EditorAction {
public static ID = 'editor.action.inlineEdits.hide';
constructor() {
super({
id: HideInlineEdit.ID,
...labelAndAlias(nls.localize2('action.inlineEdits.hide', "Hide Inline Edit")),
precondition: inlineEditVisible,
kbOpts: {
weight: 100,
primary: KeyCode.Escape,
}
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = InlineEditsController.get(editor);
transaction(tx => {
controller?.model.get()?.stop(tx);
});
}
}

View file

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const inlineEditAcceptId = 'editor.action.inlineEdits.accept';
export const showPreviousInlineEditActionId = 'editor.action.inlineEdits.showPrevious';
export const showNextInlineEditActionId = 'editor.action.inlineEdits.showNext';
export const inlineEditVisible = new RawContextKey<boolean>('inlineEditsVisible', false, localize('inlineEditsVisible', "Whether an inline edit is visible"));
export const isPinnedContextKey = new RawContextKey<boolean>('inlineEditsIsPinned', false, localize('isPinned', "Whether an inline edit is visible"));

View file

@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import {
TriggerInlineEditAction, ShowNextInlineEditAction, ShowPreviousInlineEditAction,
AcceptInlineEdit, HideInlineEdit,
} from 'vs/editor/contrib/inlineEdits/browser/commands';
import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController';
registerEditorContribution(InlineEditsController.ID, InlineEditsController, EditorContributionInstantiation.Eventually);
registerEditorAction(TriggerInlineEditAction);
registerEditorAction(ShowNextInlineEditAction);
registerEditorAction(ShowPreviousInlineEditAction);
registerEditorAction(AcceptInlineEdit);
registerEditorAction(HideInlineEdit);

View file

@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from 'vs/base/common/observable';
import { derivedDisposable, derivedWithSetter } from 'vs/base/common/observableInternal/derived';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor';
import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils';
import { Selection } from 'vs/editor/common/core/selection';
import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { inlineEditVisible, isPinnedContextKey } from 'vs/editor/contrib/inlineEdits/browser/consts';
import { InlineEditsModel } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsModel';
import { InlineEditsWidget } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils';
export class InlineEditsController extends Disposable {
static ID = 'editor.contrib.inlineEditsController';
public static get(editor: ICodeEditor): InlineEditsController | null {
return editor.getContribution<InlineEditsController>(InlineEditsController.ID);
}
private readonly _enabled = observableConfigValue('editor.inlineEdits.enabled', false, this._configurationService);
private readonly _editorObs = observableCodeEditor(this.editor);
private readonly _selection = derived(this, reader => this._editorObs.cursorSelection.read(reader) ?? new Selection(1, 1, 1, 1));
private readonly _debounceValue = this._debounceService.for(
this._languageFeaturesService.inlineCompletionsProvider,
'InlineEditsDebounce',
{ min: 50, max: 50 }
);
public readonly model = derivedDisposable<InlineEditsModel | undefined>(this, reader => {
if (!this._enabled.read(reader)) {
return undefined;
}
if (this._editorObs.isReadonly.read(reader)) { return undefined; }
const textModel = this._editorObs.model.read(reader);
if (!textModel) { return undefined; }
const model: InlineEditsModel = this._instantiationService.createInstance(
readHotReloadableExport(InlineEditsModel, reader),
textModel,
this._editorObs.versionId,
this._selection,
this._debounceValue,
);
return model;
});
private readonly _hadInlineEdit = derivedObservableWithCache<boolean>(this, (reader, lastValue) => lastValue || this.model.read(reader)?.inlineEdit.read(reader) !== undefined);
protected readonly _widget = derivedDisposable(this, reader => {
if (!this._hadInlineEdit.read(reader)) { return undefined; }
return this._instantiationService.createInstance(
readHotReloadableExport(InlineEditsWidget, reader),
this.editor,
this.model.map((m, reader) => m?.inlineEdit.read(reader)),
flattenSettableObservable((reader) => this.model.read(reader)?.userPrompt ?? observableValue('empty', '')),
);
});
constructor(
public readonly editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ILanguageFeatureDebounceService private readonly _debounceService: ILanguageFeatureDebounceService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
this._register(bindContextKey(inlineEditVisible, this._contextKeyService, r => !!this.model.read(r)?.inlineEdit.read(r)));
this._register(bindContextKey(isPinnedContextKey, this._contextKeyService, r => !!this.model.read(r)?.isPinned.read(r)));
this.model.recomputeInitiallyAndOnChange(this._store);
this._widget.recomputeInitiallyAndOnChange(this._store);
}
}
function flattenSettableObservable<TResult>(fn: (reader: IReader | undefined) => ISettableObservable<TResult>): ISettableObservable<TResult> {
return derivedWithSetter(undefined, reader => {
const obs = fn(reader);
return obs.read(reader);
}, (value, tx) => {
fn(undefined).set(value, tx);
});
}

View file

@ -0,0 +1,289 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { timeout } from 'vs/base/common/async';
import { CancellationToken, cancelOnDispose } from 'vs/base/common/cancellation';
import { itemsEquals, structuralEquals } from 'vs/base/common/equals';
import { BugIndicatingError } from 'vs/base/common/errors';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from 'vs/base/common/observable';
import { derivedDisposable } from 'vs/base/common/observableInternal/derived';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { Command, InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IModelService } from 'vs/editor/common/services/model';
import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions';
import { InlineEdit } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget';
export class InlineEditsModel extends Disposable {
private static _modelId = 0;
private static _createUniqueUri(): URI {
return URI.from({ scheme: 'inline-edits', path: new Date().toString() + String(InlineEditsModel._modelId++) });
}
private readonly _forceUpdateExplicitlySignal = observableSignal(this);
// We use a semantic id to keep the same inline completion selected even if the provider reorders the completions.
private readonly _selectedInlineCompletionId = observableValue<string | undefined>(this, undefined);
private readonly _isActive = observableValue<boolean>(this, false);
private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store);
private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store);
private readonly _pinnedRange = new TrackedRange(this.textModel, this._textModelVersionId);
public readonly isPinned = this._pinnedRange.range.map(range => !!range);
public readonly userPrompt: ISettableObservable<string | undefined> = observableValue<string | undefined>(this, undefined);
constructor(
public readonly textModel: ITextModel,
public readonly _textModelVersionId: IObservable<number | null, IModelContentChangedEvent | undefined>,
private readonly _selection: IObservable<Selection>,
protected readonly _debounceValue: IFeatureDebounceInformation,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService,
@IModelService private readonly _modelService: IModelService,
) {
super();
this._register(recomputeInitiallyAndOnChange(this._fetchInlineEditsPromise));
}
public readonly inlineEdit = derived<InlineEdit | undefined>(this, reader => {
return this._inlineEdit.read(reader)?.promiseResult.read(reader)?.data;
});
public readonly _inlineEdit = derived<ObservablePromise<InlineEdit | undefined> | undefined>(this, reader => {
const edit = this.selectedInlineEdit.read(reader);
if (!edit) { return undefined; }
const range = edit.inlineCompletion.range;
if (edit.inlineCompletion.insertText.trim() === '') {
return undefined;
}
let newLines = edit.inlineCompletion.insertText.split(/\r\n|\r|\n/);
function removeIndentation(lines: string[]): string[] {
const indentation = lines[0].match(/^\s*/)?.[0] ?? '';
return lines.map(l => l.replace(new RegExp('^' + indentation), ''));
}
newLines = removeIndentation(newLines);
const existing = this.textModel.getValueInRange(range);
let existingLines = existing.split(/\r\n|\r|\n/);
existingLines = removeIndentation(existingLines);
this._originalModel.get().setValue(existingLines.join('\n'));
this._modifiedModel.get().setValue(newLines.join('\n'));
const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' });
return ObservablePromise.fromFn(async () => {
const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), {
computeMoves: false,
ignoreTrimWhitespace: false,
maxComputationTimeMs: 1000,
}, CancellationToken.None);
if (result.identical) {
return undefined;
}
return new InlineEdit(LineRange.fromRangeInclusive(range), removeIndentation(newLines), result.changes);
});
});
private readonly _fetchStore = this._register(new DisposableStore());
private readonly _inlineEditsFetchResult = disposableObservableValue<InlineCompletionProviderResult | undefined>(this, undefined);
private readonly _inlineEdits = derivedOpts<InlineEditData[]>({ owner: this, equalsFn: structuralEquals }, reader => {
return this._inlineEditsFetchResult.read(reader)?.completions.map(c => new InlineEditData(c)) ?? [];
});
private readonly _fetchInlineEditsPromise = derivedHandleChanges({
owner: this,
createEmptyChangeSummary: () => ({
inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic
}),
handleChange: (ctx, changeSummary) => {
/** @description fetch inline completions */
if (ctx.didChange(this._forceUpdateExplicitlySignal)) {
changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;
}
return true;
},
}, async (reader, changeSummary) => {
this._fetchStore.clear();
this._forceUpdateExplicitlySignal.read(reader);
/*if (!this._isActive.read(reader)) {
return undefined;
}*/
this._textModelVersionId.read(reader);
function mapValue<T, TOut>(value: T, fn: (value: T) => TOut): TOut {
return fn(value);
}
const selection = this._pinnedRange.range.read(reader) ?? mapValue(this._selection.read(reader), v => v.isEmpty() ? undefined : v);
if (!selection) {
this._inlineEditsFetchResult.set(undefined, undefined);
this.userPrompt.set(undefined, undefined);
return undefined;
}
const context: InlineCompletionContext = {
triggerKind: changeSummary.inlineCompletionTriggerKind,
selectedSuggestionInfo: undefined,
userPrompt: this.userPrompt.read(reader),
};
const token = cancelOnDispose(this._fetchStore);
await timeout(200, token);
const result = await provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, selection, this.textModel, context, token);
if (token.isCancellationRequested) {
return;
}
this._inlineEditsFetchResult.set(result, undefined);
});
public async trigger(tx?: ITransaction): Promise<void> {
this._isActive.set(true, tx);
await this._fetchInlineEditsPromise.get();
}
public async triggerExplicitly(tx?: ITransaction): Promise<void> {
subtransaction(tx, tx => {
this._isActive.set(true, tx);
this._forceUpdateExplicitlySignal.trigger(tx);
});
await this._fetchInlineEditsPromise.get();
}
public stop(tx?: ITransaction): void {
subtransaction(tx, tx => {
this.userPrompt.set(undefined, tx);
this._isActive.set(false, tx);
this._inlineEditsFetchResult.set(undefined, tx);
this._pinnedRange.setRange(undefined, tx);
//this._source.clear(tx);
});
}
private readonly _filteredInlineEditItems = derivedOpts<InlineEditData[]>({ owner: this, equalsFn: itemsEquals() }, reader => {
return this._inlineEdits.read(reader);
});
public readonly selectedInlineCompletionIndex = derived<number>(this, (reader) => {
const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader);
const filteredCompletions = this._filteredInlineEditItems.read(reader);
const idx = this._selectedInlineCompletionId === undefined ? -1
: filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId);
if (idx === -1) {
// Reset the selection so that the selection does not jump back when it appears again
this._selectedInlineCompletionId.set(undefined, undefined);
return 0;
}
return idx;
});
public readonly selectedInlineEdit = derived<InlineEditData | undefined>(this, (reader) => {
const filteredCompletions = this._filteredInlineEditItems.read(reader);
const idx = this.selectedInlineCompletionIndex.read(reader);
return filteredCompletions[idx];
});
public readonly activeCommands = derivedOpts<Command[]>({ owner: this, equalsFn: itemsEquals() },
r => this.selectedInlineEdit.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? []
);
private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise<void> {
await this.triggerExplicitly();
const completions = this._filteredInlineEditItems.get() || [];
if (completions.length > 0) {
const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length;
this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined);
} else {
this._selectedInlineCompletionId.set(undefined, undefined);
}
}
public async next(): Promise<void> {
await this._deltaSelectedInlineCompletionIndex(1);
}
public async previous(): Promise<void> {
await this._deltaSelectedInlineCompletionIndex(-1);
}
public togglePin(): void {
if (this.isPinned.get()) {
this._pinnedRange.setRange(undefined, undefined);
} else {
this._pinnedRange.setRange(this._selection.get(), undefined);
}
}
public async accept(editor: ICodeEditor): Promise<void> {
if (editor.getModel() !== this.textModel) {
throw new BugIndicatingError();
}
const edit = this.selectedInlineEdit.get();
if (!edit) {
return;
}
editor.pushUndoStop();
editor.executeEdits(
'inlineSuggestion.accept',
[
edit.inlineCompletion.toSingleTextEdit().toSingleEditOperation()
]
);
this.stop();
}
}
class InlineEditData {
public readonly semanticId = this.inlineCompletion.hash();
constructor(public readonly inlineCompletion: InlineCompletionItem) {
}
}
class TrackedRange extends Disposable {
private readonly _decorations = observableValue<string[]>(this, []);
constructor(
private readonly _textModel: ITextModel,
private readonly _versionId: IObservable<number | null>,
) {
super();
this._register(toDisposable(() => {
this._textModel.deltaDecorations(this._decorations.get(), []);
}));
}
setRange(range: Range | undefined, tx: ITransaction | undefined): void {
this._decorations.set(this._textModel.deltaDecorations(this._decorations.get(), range ? [{ range, options: { description: 'trackedRange' } }] : []), tx);
}
public readonly range = derived(this, reader => {
this._versionId.read(reader);
const deco = this._decorations.read(reader)[0];
if (!deco) { return null; }
return this._textModel.getDecorationRange(deco) ?? null;
});
}

View file

@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor div.inline-edits-widget {
--widget-color: var(--vscode-notifications-background);
.promptEditor .monaco-editor {
--vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground);
}
.toolbar, .promptEditor {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
&:hover, &.focused {
.toolbar, .promptEditor {
opacity: 1;
}
}
.preview .monaco-editor {
.mtk1 {
/*color: rgba(215, 215, 215, 0.452);*/
color: var(--vscode-editorGhostText-foreground);
}
.view-overlays .current-line-exact {
border: none;
}
.current-line-margin {
border: none;
}
--vscode-editor-background: var(--widget-color);
}
svg {
.gradient-start {
stop-color: var(--vscode-editor-background);
}
.gradient-stop {
stop-color: var(--widget-color);
}
}
}

View file

@ -0,0 +1,400 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { h, svgElem } from 'vs/base/browser/dom';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { autorun, constObservable, derived, IObservable, ISettableObservable } from 'vs/base/common/observable';
import { derivedWithSetter } from 'vs/base/common/observableInternal/derived';
import 'vs/css!./inlineEditsWidget';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution';
import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu';
import { PlaceholderTextContribution } from 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class InlineEdit {
constructor(
public readonly range: LineRange,
public readonly newLines: string[],
public readonly changes: readonly DetailedLineRangeMapping[],
) {
}
}
export class InlineEditsWidget extends Disposable {
private readonly _editorObs = observableCodeEditor(this._editor);
private readonly _elements = h('div.inline-edits-widget', {
style: {
position: 'absolute',
overflow: 'visible',
top: '0px',
left: '0px',
},
}, [
h('div@editorContainer', { style: { position: 'absolute', top: '0px', left: '0px', width: '500px', height: '500px', } }, [
h('div.toolbar@toolbar', { style: { position: 'absolute', top: '-25px', left: '0px' } }),
h('div.promptEditor@promptEditor', { style: { position: 'absolute', top: '-25px', left: '80px', width: '300px', height: '22px' } }),
h('div.preview@editor', { style: { position: 'absolute', top: '0px', left: '0px' } }),
]),
svgElem('svg', { style: { overflow: 'visible', pointerEvents: 'none' }, }, [
svgElem('defs', [
svgElem('linearGradient', {
id: 'Gradient2',
x1: '0',
y1: '0',
x2: '1',
y2: '0',
}, [
/*svgElem('stop', { offset: '0%', class: 'gradient-start', }),
svgElem('stop', { offset: '0%', class: 'gradient-start', }),
svgElem('stop', { offset: '20%', class: 'gradient-stop', }),*/
svgElem('stop', { offset: '0%', class: 'gradient-stop', }),
svgElem('stop', { offset: '100%', class: 'gradient-stop', }),
]),
]),
svgElem('path@path', {
d: '',
fill: 'url(#Gradient2)',
}),
]),
]);
protected readonly _toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar, MenuId.InlineEditsActions, {
toolbarOptions: {
primaryGroup: g => g.startsWith('primary'),
},
}));
private readonly _previewTextModel = this._register(this._instantiationService.createInstance(
TextModel,
'',
PLAINTEXT_LANGUAGE_ID,
TextModel.DEFAULT_CREATION_OPTIONS,
null
));
private readonly _setText = derived(reader => {
const edit = this._edit.read(reader);
if (!edit) { return; }
this._previewTextModel.setValue(edit.newLines.join('\n'));
}).recomputeInitiallyAndOnChange(this._store);
private readonly _promptTextModel = this._register(this._instantiationService.createInstance(
TextModel,
'',
PLAINTEXT_LANGUAGE_ID,
TextModel.DEFAULT_CREATION_OPTIONS,
null
));
private readonly _promptEditor = this._register(this._instantiationService.createInstance(
EmbeddedCodeEditorWidget,
this._elements.promptEditor,
{
glyphMargin: false,
lineNumbers: 'off',
minimap: { enabled: false },
guides: {
indentation: false,
bracketPairs: false,
bracketPairsHorizontal: false,
highlightActiveIndentation: false,
},
folding: false,
selectOnLineNumbers: false,
selectionHighlight: false,
columnSelection: false,
overviewRulerBorder: false,
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
placeholder: 'Describe the change you want...',
fontFamily: DEFAULT_FONT_FAMILY,
},
{
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
SuggestController.ID,
PlaceholderTextContribution.ID,
ContextMenuController.ID,
]),
isSimpleWidget: true
},
this._editor
));
private readonly _previewEditor = this._register(this._instantiationService.createInstance(
EmbeddedCodeEditorWidget,
this._elements.editor,
{
glyphMargin: false,
lineNumbers: 'off',
minimap: { enabled: false },
guides: {
indentation: false,
bracketPairs: false,
bracketPairsHorizontal: false,
highlightActiveIndentation: false,
},
folding: false,
selectOnLineNumbers: false,
selectionHighlight: false,
columnSelection: false,
overviewRulerBorder: false,
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
},
{ contributions: [], },
this._editor
));
private readonly _previewEditorObs = observableCodeEditor(this._previewEditor);
private readonly _decorations = derived(this, (reader) => {
this._setText.read(reader);
const diff = this._edit.read(reader)?.changes;
if (!diff) { return []; }
const originalDecorations: IModelDeltaDecoration[] = [];
const modifiedDecorations: IModelDeltaDecoration[] = [];
if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) {
return [];
}
for (const m of diff) {
if (!m.original.isEmpty) {
originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffLineDeleteDecorationBackgroundWithIndicator });
}
if (!m.modified.isEmpty) {
modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator });
}
if (m.modified.isEmpty || m.original.isEmpty) {
if (!m.original.isEmpty) {
originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration });
}
if (!m.modified.isEmpty) {
modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration });
}
} else {
for (const i of m.innerChanges || []) {
// Don't show empty markers outside the line range
if (m.original.contains(i.originalRange.startLineNumber)) {
originalDecorations.push({ range: i.originalRange, options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration });
}
if (m.modified.contains(i.modifiedRange.startLineNumber)) {
modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration });
}
}
}
}
return modifiedDecorations;
});
private readonly _layout1 = derived(this, reader => {
const model = this._editor.getModel()!;
const inlineEdit = this._edit.read(reader);
if (!inlineEdit) { return null; }
const range = inlineEdit.range;
let maxLeft = 0;
for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) {
const column = model.getLineMaxColumn(i);
const left = this._editor.getOffsetForColumn(i, column);
maxLeft = Math.max(maxLeft, left);
}
const layoutInfo = this._editor.getLayoutInfo();
const contentLeft = layoutInfo.contentLeft;
return { left: contentLeft + maxLeft };
});
private readonly _layout = derived(this, (reader) => {
const inlineEdit = this._edit.read(reader);
if (!inlineEdit) { return null; }
const range = inlineEdit.range;
const scrollLeft = this._editorObs.scrollLeft.read(reader);
const left = this._layout1.read(reader)!.left + 20 - scrollLeft;
const selectionTop = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader);
const selectionBottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader);
const topCode = new Point(left, selectionTop);
const bottomCode = new Point(left, selectionBottom);
const codeHeight = selectionBottom - selectionTop;
const codeEditDist = 50;
const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.newLines.length;
const difference = codeHeight - editHeight;
const topEdit = new Point(left + codeEditDist, selectionTop + (difference / 2));
const bottomEdit = new Point(left + codeEditDist, selectionBottom - (difference / 2));
return {
topCode,
bottomCode,
codeHeight,
topEdit,
bottomEdit,
editHeight,
};
});
constructor(
private readonly _editor: ICodeEditor,
private readonly _edit: IObservable<InlineEdit | undefined>,
private readonly _userPrompt: ISettableObservable<string | undefined>,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
const visible = derived(this, reader => this._edit.read(reader) !== undefined || this._userPrompt.read(reader) !== undefined);
this._register(applyStyle(this._elements.root, {
display: derived(this, reader => visible.read(reader) ? 'block' : 'none')
}));
this._register(appendRemoveOnDispose(this._editor.getDomNode()!, this._elements.root));
this._register(observableCodeEditor(_editor).createOverlayWidget({
domNode: this._elements.root,
position: constObservable(null),
allowEditorOverflow: false,
minContentWidthInPx: derived(reader => {
const x = this._layout1.read(reader)?.left;
if (x === undefined) { return 0; }
const width = this._previewEditorObs.contentWidth.read(reader);
return x + width;
}),
}));
this._previewEditor.setModel(this._previewTextModel);
this._register(this._previewEditorObs.setDecorations(this._decorations));
this._register(autorun(reader => {
const layoutInfo = this._layout.read(reader);
if (!layoutInfo) { return; }
const { topCode, bottomCode, topEdit, bottomEdit, editHeight } = layoutInfo;
const straightWidthCode = 10;
const straightWidthEdit = 0;
const bezierDist = 40;
const path = new PathBuilder()
.moveTo(topCode)
.lineTo(topCode.deltaX(straightWidthCode))
.curveTo(
topCode.deltaX(straightWidthCode + bezierDist),
topEdit.deltaX(-bezierDist - straightWidthEdit),
topEdit.deltaX(-straightWidthEdit),
)
.lineTo(topEdit)
.lineTo(bottomEdit)
.lineTo(bottomEdit.deltaX(-straightWidthEdit))
.curveTo(
bottomEdit.deltaX(-bezierDist - straightWidthEdit),
bottomCode.deltaX(straightWidthCode + bezierDist),
bottomCode.deltaX(straightWidthCode),
)
.lineTo(bottomCode)
.build();
this._elements.path.setAttribute('d', path);
this._elements.editorContainer.style.top = `${topEdit.y}px`;
this._elements.editorContainer.style.left = `${topEdit.x}px`;
this._elements.editorContainer.style.height = `${editHeight}px`;
const width = this._previewEditorObs.contentWidth.read(reader);
this._previewEditor.layout({ height: editHeight, width });
}));
this._promptEditor.setModel(this._promptTextModel);
this._promptEditor.layout();
this._register(createTwoWaySync(mapSettableObservable(this._userPrompt, v => v ?? '', v => v), observableCodeEditor(this._promptEditor).value));
this._register(autorun(reader => {
const isFocused = observableCodeEditor(this._promptEditor).isFocused.read(reader);
this._elements.root.classList.toggle('focused', isFocused);
}));
}
}
function mapSettableObservable<T, T1>(obs: ISettableObservable<T>, fn1: (value: T) => T1, fn2: (value: T1) => T): ISettableObservable<T1> {
return derivedWithSetter(undefined, reader => fn1(obs.read(reader)), (value, tx) => obs.set(fn2(value), tx));
}
class Point {
constructor(
public readonly x: number,
public readonly y: number,
) { }
public add(other: Point): Point {
return new Point(this.x + other.x, this.y + other.y);
}
public deltaX(delta: number): Point {
return new Point(this.x + delta, this.y);
}
}
class PathBuilder {
private _data: string = '';
public moveTo(point: Point): this {
this._data += `M ${point.x} ${point.y} `;
return this;
}
public lineTo(point: Point): this {
this._data += `L ${point.x} ${point.y} `;
return this;
}
public curveTo(cp1: Point, cp2: Point, to: Point): this {
this._data += `C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y} ${to.x} ${to.y} `;
return this;
}
public build(): string {
return this._data;
}
}
function createTwoWaySync<T>(main: ISettableObservable<T>, target: ISettableObservable<T>): IDisposable {
const store = new DisposableStore();
store.add(autorun(reader => {
const value = main.read(reader);
target.set(value, undefined);
}));
store.add(autorun(reader => {
const value = target.read(reader);
main.set(value, undefined);
}));
return store;
}

View file

@ -42,6 +42,7 @@ import 'vs/editor/contrib/links/browser/links';
import 'vs/editor/contrib/longLinesHelper/browser/longLinesHelper';
import 'vs/editor/contrib/multicursor/browser/multicursor';
import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution';
import 'vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution';
import 'vs/editor/contrib/parameterHints/browser/parameterHints';
import 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution';
import 'vs/editor/contrib/rename/browser/rename';

View file

@ -211,6 +211,7 @@ export class MenuId {
static readonly TerminalStickyScrollContext = new MenuId('TerminalStickyScrollContext');
static readonly WebviewContext = new MenuId('WebviewContext');
static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions');
static readonly InlineEditsActions = new MenuId('InlineEditsActions');
static readonly InlineEditActions = new MenuId('InlineEditActions');
static readonly NewFile = new MenuId('NewFile');
static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar');

View file

@ -612,6 +612,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined> => {
return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token);
},
provideInlineEdits: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined> => {
return this._proxy.$provideInlineEdits(handle, model.uri, range, context, token);
},
handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise<void> => {
if (supportsHandleEvents) {
await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText);

View file

@ -2190,6 +2190,7 @@ export interface ExtHostLanguageFeaturesShape {
$resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<ISuggestDataDto | undefined>;
$releaseCompletionItems(handle: number, id: number): void;
$provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined>;
$provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined>;
$handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void;
$handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void;
$freeInlineCompletionsList(handle: number, pid: number): void;

View file

@ -33,7 +33,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';
import { Cache } from './cache';
import * as extHostProtocol from './extHost.protocol';
@ -1287,6 +1287,10 @@ class InlineCompletionAdapterBase {
return undefined;
}
async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> {
return undefined;
}
disposeCompletions(pid: number): void { }
handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { }
@ -1392,6 +1396,82 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase {
};
}
override async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> {
if (!this._provider.provideInlineEdits) {
return undefined;
}
checkProposedApiEnabled(this._extension, 'inlineCompletionsAdditions');
const doc = this._documents.getDocument(resource);
const r = typeConvert.Range.to(range);
const result = await this._provider.provideInlineEdits(doc, r, {
selectedCompletionInfo:
context.selectedSuggestionInfo
? {
range: typeConvert.Range.to(context.selectedSuggestionInfo.range),
text: context.selectedSuggestionInfo.text
}
: undefined,
triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind],
userPrompt: context.userPrompt,
}, token);
if (!result) {
// undefined and null are valid results
return undefined;
}
if (token.isCancellationRequested) {
// cancelled -> return without further ado, esp no caching
// of results as they will leak
return undefined;
}
const normalizedResult = Array.isArray(result) ? result : result.items;
const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : [];
const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined;
let disposableStore: DisposableStore | undefined = undefined;
const pid = this._references.createReferenceId({
dispose() {
disposableStore?.dispose();
},
items: normalizedResult
});
return {
pid,
items: normalizedResult.map<extHostProtocol.IdentifiableInlineCompletion>((item, idx) => {
let command: languages.Command | undefined = undefined;
if (item.command) {
if (!disposableStore) {
disposableStore = new DisposableStore();
}
command = this._commands.toInternal(item.command, disposableStore);
}
const insertText = item.insertText;
return ({
insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value },
filterText: item.filterText,
range: item.range ? typeConvert.Range.from(item.range) : undefined,
command,
idx: idx,
completeBracketPairs: this._isAdditionsProposedApiEnabled ? item.completeBracketPairs : false,
});
}),
commands: commands.map(c => {
if (!disposableStore) {
disposableStore = new DisposableStore();
}
return this._commands.toInternal(c, disposableStore);
}),
suppressSuggestions: false,
enableForwardStability,
};
}
override disposeCompletions(pid: number) {
const data = this._references.disposeReferenceId(pid);
data?.dispose();
@ -2581,6 +2661,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token);
}
$provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> {
return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEdits(URI.revive(resource), range, context, token), undefined, token);
}
$handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void {
this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => {
adapter.handleDidShowCompletionItem(pid, idx, updatedInsertText);

View file

@ -60,6 +60,12 @@ declare module 'vscode' {
*/
// eslint-disable-next-line local/vscode-dts-provider-naming
handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void;
provideInlineEdits?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult<InlineCompletionItem[] | InlineCompletionList>;
}
export interface InlineCompletionContext {
readonly userPrompt?: string;
}
export interface PartialAcceptInfo {