mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
Implements experimental inline edits
Signed-off-by: Henning Dieterichs <hdieterichs@microsoft.com>
This commit is contained in:
parent
d6ffb9f68a
commit
309351259c
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
185
src/vs/editor/contrib/inlineEdits/browser/commands.ts
Normal file
185
src/vs/editor/contrib/inlineEdits/browser/commands.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
16
src/vs/editor/contrib/inlineEdits/browser/consts.ts
Normal file
16
src/vs/editor/contrib/inlineEdits/browser/consts.ts
Normal 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"));
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
}
|
289
src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts
Normal file
289
src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts
Normal 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;
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
400
src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts
Normal file
400
src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue