mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Cleanup inline edits view (#242557)
This commit is contained in:
committed by
GitHub
parent
d8e9c15fa3
commit
3bf4028bbd
@@ -4278,10 +4278,6 @@ export interface IInlineSuggestOptions {
|
||||
* @internal
|
||||
*/
|
||||
useMultiLineGhostText?: boolean;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
useGutterIndicator?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4313,7 +4309,6 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
|
||||
useMixedLinesDiff: 'forStableInsertions',
|
||||
useInterleavedLinesDiff: 'never',
|
||||
renderSideBySide: 'auto',
|
||||
useGutterIndicator: true,
|
||||
codeShifting: true,
|
||||
useMultiLineGhostText: true
|
||||
},
|
||||
@@ -4422,7 +4417,6 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
|
||||
codeShifting: boolean(input.edits?.codeShifting, this.defaultValue.edits.codeShifting),
|
||||
renderSideBySide: stringSet(input.edits?.renderSideBySide, this.defaultValue.edits.renderSideBySide, ['never', 'auto']),
|
||||
useInterleavedLinesDiff: stringSet(input.edits?.useInterleavedLinesDiff, this.defaultValue.edits.useInterleavedLinesDiff, ['never', 'always', 'afterJump']),
|
||||
useGutterIndicator: boolean(input.edits?.useGutterIndicator, this.defaultValue.edits.useGutterIndicator),
|
||||
useMultiLineGhostText: boolean(input.edits?.useMultiLineGhostText, this.defaultValue.edits.useMultiLineGhostText),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -771,7 +771,7 @@ export class InlineCompletionsModel extends Disposable {
|
||||
|
||||
// TODO: clean this up if we keep it
|
||||
private readonly _inAcceptPartialFlow = observableValue(this, false);
|
||||
public readonly inAcceptPartialFlow: IObservable<boolean> = this._inAcceptPartialFlow;
|
||||
public readonly inPartialAcceptFlow: IObservable<boolean> = this._inAcceptPartialFlow;
|
||||
public async acceptNextInlineEditPart(editor: ICodeEditor): Promise<void> {
|
||||
if (editor.getModel() !== this.textModel) {
|
||||
throw new BugIndicatingError();
|
||||
|
||||
@@ -14,7 +14,7 @@ import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHi
|
||||
import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js';
|
||||
import { convertItemsToStableObservables } from '../utils.js';
|
||||
import { GhostTextView } from './ghostText/ghostTextView.js';
|
||||
import { InlineEditsViewAndDiffProducer } from './inlineEdits/viewAndDiffProducer.js';
|
||||
import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js';
|
||||
|
||||
export class InlineCompletionsView extends Disposable {
|
||||
private readonly _ghostTexts = derived(this, (reader) => {
|
||||
|
||||
@@ -22,15 +22,15 @@ import { asCssVariable, descriptionForeground, editorActionListForeground, edito
|
||||
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
|
||||
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
|
||||
import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, toggleShowCollapsedId } from '../../../controller/commandIds.js';
|
||||
import { IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { FirstFnArg, InlineEditTabAction } from '../utils/utils.js';
|
||||
import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { FirstFnArg, } from '../utils/utils.js';
|
||||
|
||||
export class GutterIndicatorMenuContent {
|
||||
|
||||
private readonly _inlineEditsShowCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed);
|
||||
|
||||
constructor(
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _model: IInlineEditModel,
|
||||
private readonly _close: (focusEditor: boolean) => void,
|
||||
private readonly _editorObs: ObservableCodeEditor,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@@ -60,39 +60,74 @@ export class GutterIndicatorMenuContent {
|
||||
};
|
||||
};
|
||||
|
||||
// TODO make this menu contributable!
|
||||
return hoverContent([
|
||||
header(this._host.displayName),
|
||||
const title = header(this._model.displayName);
|
||||
|
||||
const gotoAndAccept = option(createOptionArgs({
|
||||
id: 'gotoAndAccept',
|
||||
title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`,
|
||||
icon: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.check : Codicon.arrowRight),
|
||||
commandId: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? inlineSuggestCommitId : jumpToNextInlineEditId)
|
||||
}));
|
||||
|
||||
const reject = option(createOptionArgs({
|
||||
id: 'reject',
|
||||
title: localize('reject', "Reject"),
|
||||
icon: Codicon.close,
|
||||
commandId: hideInlineCompletionId
|
||||
}));
|
||||
|
||||
const extensionCommands = this._model.extensionCommands.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments })));
|
||||
|
||||
const toggleCollapsedMode = this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ?
|
||||
option(createOptionArgs({
|
||||
id: 'gotoAndAccept', title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`,
|
||||
icon: this._host.tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.check : Codicon.arrowRight),
|
||||
commandId: this._host.tabAction.map(action => action === InlineEditTabAction.Accept ? inlineSuggestCommitId : jumpToNextInlineEditId)
|
||||
id: 'showExpanded',
|
||||
title: localize('showExpanded', "Show Expanded"),
|
||||
icon: Codicon.expandAll,
|
||||
commandId: toggleShowCollapsedId
|
||||
}))
|
||||
: option(createOptionArgs({
|
||||
id: 'showCollapsed',
|
||||
title: localize('showCollapsed', "Show Collapsed"),
|
||||
icon: Codicon.collapseAll,
|
||||
commandId: toggleShowCollapsedId
|
||||
}))
|
||||
);
|
||||
|
||||
const settings = option(createOptionArgs({
|
||||
id: 'settings',
|
||||
title: localize('settings', "Settings"),
|
||||
icon: Codicon.gear,
|
||||
commandId: 'workbench.action.openSettings',
|
||||
commandArgs: ['@tag:nextEditSuggestions']
|
||||
}));
|
||||
|
||||
const actions = this._model.action ? [this._model.action] : [];
|
||||
const actionBarFooter = actions.length > 0 ? actionBar(
|
||||
actions.map(action => ({
|
||||
id: action.id,
|
||||
label: action.title,
|
||||
enabled: true,
|
||||
run: () => this._commandService.executeCommand(action.id, ...(action.arguments ?? [])),
|
||||
class: undefined,
|
||||
tooltip: action.tooltip ?? action.title
|
||||
})),
|
||||
option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: hideInlineCompletionId })),
|
||||
{ hoverDelegate: nativeHoverDelegate /* unable to show hover inside another hover */ }
|
||||
) : undefined;
|
||||
|
||||
return hoverContent([
|
||||
title,
|
||||
gotoAndAccept,
|
||||
reject,
|
||||
separator(),
|
||||
this._host.extensionCommands?.map(c => c && c.length > 0 ? [
|
||||
...c.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))),
|
||||
separator()
|
||||
] : []),
|
||||
this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ?
|
||||
option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) :
|
||||
option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId }))
|
||||
),
|
||||
option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['@tag:nextEditSuggestions'] })),
|
||||
this._host.action.map(action => action ? [
|
||||
separator(),
|
||||
actionBar(
|
||||
[{
|
||||
id: action.id,
|
||||
label: action.title,
|
||||
enabled: true,
|
||||
run: () => this._commandService.executeCommand(action.id, ...(action.arguments ?? [])),
|
||||
class: undefined,
|
||||
tooltip: action.tooltip ?? action.title
|
||||
}],
|
||||
{ hoverDelegate: nativeHoverDelegate /* unable to show hover inside another hover */ }
|
||||
)
|
||||
] : [])
|
||||
|
||||
...extensionCommands,
|
||||
extensionCommands.length ? separator() : undefined,
|
||||
|
||||
toggleCollapsedMode,
|
||||
settings,
|
||||
|
||||
actionBarFooter ? separator() : undefined,
|
||||
actionBarFooter
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -188,6 +223,7 @@ function actionBar(actions: IAction[], options: IActionBarOptions) {
|
||||
|
||||
function separator() {
|
||||
return n.div({
|
||||
id: 'inline-edit-gutter-indicator-menu-separator',
|
||||
class: 'menu-separator',
|
||||
style: {
|
||||
color: asCssVariable(editorActionListForeground),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { n, trackFocus } from '../../../../../../../base/browser/dom.js';
|
||||
import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js';
|
||||
import { Codicon } from '../../../../../../../base/common/codicons.js';
|
||||
import { BugIndicatingError } from '../../../../../../../base/common/errors.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { IObservable, ISettableObservable, constObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js';
|
||||
import { debouncedObservable } from '../../../../../../../base/common/observableInternal/utils.js';
|
||||
@@ -21,17 +22,24 @@ import { EditorOption } from '../../../../../../common/config/editorOptions.js';
|
||||
import { LineRange } from '../../../../../../common/core/lineRange.js';
|
||||
import { OffsetRange } from '../../../../../../common/core/offsetRange.js';
|
||||
import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js';
|
||||
import { IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulForeground } from '../theme.js';
|
||||
import { InlineEditTabAction, mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
import { mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js';
|
||||
|
||||
export class InlineEditsGutterIndicator extends Disposable {
|
||||
|
||||
private get model() {
|
||||
const model = this._model.get();
|
||||
if (!model) { throw new BugIndicatingError('Inline Edit Model not available'); }
|
||||
return model;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _editorObs: ObservableCodeEditor,
|
||||
private readonly _originalRange: IObservable<LineRange | undefined>,
|
||||
private readonly _verticalOffset: IObservable<number>,
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _model: IObservable<IInlineEditModel | undefined>,
|
||||
private readonly _isHoveringOverInlineEdit: IObservable<boolean>,
|
||||
private readonly _focusIsInMenu: ISettableObservable<boolean>,
|
||||
@IHoverService private readonly _hoverService: HoverService,
|
||||
@@ -131,7 +139,7 @@ export class InlineEditsGutterIndicator extends Disposable {
|
||||
const disposableStore = new DisposableStore();
|
||||
const content = disposableStore.add(this._instantiationService.createInstance(
|
||||
GutterIndicatorMenuContent,
|
||||
this._host,
|
||||
this.model,
|
||||
(focusEditor) => {
|
||||
if (focusEditor) {
|
||||
this._editorObs.editor.focus();
|
||||
@@ -161,15 +169,21 @@ export class InlineEditsGutterIndicator extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _tabAction = derived(this, reader => {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) { return InlineEditTabAction.Inactive; }
|
||||
return model.tabAction.read(reader);
|
||||
});
|
||||
|
||||
private readonly _indicator = n.div({
|
||||
class: 'inline-edits-view-gutter-indicator',
|
||||
onclick: () => {
|
||||
const docked = this._layout.map(l => l && l.docked).get();
|
||||
this._editorObs.editor.focus();
|
||||
if (docked) {
|
||||
this._host.accept();
|
||||
this.model.accept();
|
||||
} else {
|
||||
this._host.jump();
|
||||
this.model.jump();
|
||||
}
|
||||
},
|
||||
tabIndex: 0,
|
||||
@@ -199,14 +213,14 @@ export class InlineEditsGutterIndicator extends Disposable {
|
||||
cursor: 'pointer',
|
||||
zIndex: '1000',
|
||||
position: 'absolute',
|
||||
backgroundColor: this._host.tabAction.map(v => {
|
||||
backgroundColor: this._tabAction.map(v => {
|
||||
switch (v) {
|
||||
case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryBackground);
|
||||
case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryBackground);
|
||||
case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulBackground);
|
||||
}
|
||||
}),
|
||||
['--vscodeIconForeground' as any]: this._host.tabAction.map(v => {
|
||||
['--vscodeIconForeground' as any]: this._tabAction.map(v => {
|
||||
switch (v) {
|
||||
case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryForeground);
|
||||
case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryForeground);
|
||||
@@ -235,7 +249,7 @@ export class InlineEditsGutterIndicator extends Disposable {
|
||||
justifyContent: 'center',
|
||||
}
|
||||
}, [
|
||||
this._host.tabAction.map(v => v === InlineEditTabAction.Accept ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight))
|
||||
this._tabAction.map(v => v === InlineEditTabAction.Accept ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight))
|
||||
])
|
||||
]),
|
||||
])).keepUpdated(this._store);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { derived, IObservable } from '../../../../../../base/common/observable.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { ICodeEditor } from '../../../../../browser/editorBrowser.js';
|
||||
import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js';
|
||||
import { LineRange } from '../../../../../common/core/lineRange.js';
|
||||
import { StringText, TextEdit } from '../../../../../common/core/textEdit.js';
|
||||
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
|
||||
import { InlineCompletionWithUpdatedRange } from '../../model/inlineCompletionsSource.js';
|
||||
import { IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js';
|
||||
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
|
||||
|
||||
export class InlineEditModel implements IInlineEditModel {
|
||||
|
||||
readonly action = this.inlineEdit.inlineCompletion.action;
|
||||
readonly displayName = this.inlineEdit.inlineCompletion.source.provider.displayName ?? localize('inlineEdit', "Inline Edit");
|
||||
readonly extensionCommands = this.inlineEdit.inlineCompletion.source.inlineCompletions.commands ?? [];
|
||||
|
||||
readonly inAcceptFlow = this._model.inAcceptFlow;
|
||||
readonly inPartialAcceptFlow = this._model.inPartialAcceptFlow;
|
||||
|
||||
constructor(
|
||||
private readonly _model: InlineCompletionsModel,
|
||||
readonly inlineEdit: InlineEditWithChanges,
|
||||
readonly tabAction: IObservable<InlineEditTabAction>,
|
||||
) { }
|
||||
|
||||
accept() {
|
||||
this._model.accept();
|
||||
}
|
||||
|
||||
jump() {
|
||||
this._model.jump();
|
||||
}
|
||||
|
||||
abort(reason: string) {
|
||||
console.error(reason); // TODO: add logs/telemetry
|
||||
this._model.stop();
|
||||
}
|
||||
|
||||
handleInlineEditShown() {
|
||||
this._model.handleInlineEditShown(this.inlineEdit.inlineCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class GhostTextIndicator {
|
||||
|
||||
readonly model: InlineEditModel;
|
||||
|
||||
private readonly _editorObs = observableCodeEditor(this._editor);
|
||||
|
||||
constructor(
|
||||
private _editor: ICodeEditor,
|
||||
model: InlineCompletionsModel,
|
||||
readonly lineRange: LineRange,
|
||||
inlineCompletion: InlineCompletionWithUpdatedRange,
|
||||
renderExplicitly: boolean,
|
||||
) {
|
||||
|
||||
const tabAction = derived<InlineEditTabAction>(this, reader => {
|
||||
if (this._editorObs.isFocused.read(reader)) {
|
||||
if (model.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) {
|
||||
return InlineEditTabAction.Accept;
|
||||
}
|
||||
}
|
||||
return InlineEditTabAction.Inactive;
|
||||
});
|
||||
|
||||
this.model = new InlineEditModel(
|
||||
model,
|
||||
new InlineEditWithChanges(
|
||||
new StringText(''),
|
||||
new TextEdit([]),
|
||||
model.primaryPosition.get(),
|
||||
renderExplicitly,
|
||||
inlineCompletion.source.inlineCompletions.commands ?? [],
|
||||
inlineCompletion.inlineCompletion
|
||||
),
|
||||
tabAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equal
|
||||
import { BugIndicatingError } from '../../../../../../base/common/errors.js';
|
||||
import { Event } from '../../../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { autorunWithStore, derived, derivedObservableWithCache, derivedOpts, derivedWithStore, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { autorunWithStore, derived, derivedOpts, derivedWithStore, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js';
|
||||
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ICodeEditor } from '../../../../../browser/editorBrowser.js';
|
||||
import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js';
|
||||
@@ -20,18 +19,18 @@ import { AbstractText, SingleTextEdit, StringText } from '../../../../../common/
|
||||
import { TextLength } from '../../../../../common/core/textLength.js';
|
||||
import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js';
|
||||
import { TextModel } from '../../../../../common/model/textModel.js';
|
||||
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
|
||||
import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js';
|
||||
import { IInlineEditsIndicatorState, InlineEditsIndicator } from './components/indicatorView.js';
|
||||
import { InlineEditsIndicator } from './components/indicatorView.js';
|
||||
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
|
||||
import { IInlineEditsViewHost } from './inlineEditsViewInterface.js';
|
||||
import { GhostTextIndicator, InlineEditModel } from './inlineEditsModel.js';
|
||||
import { IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js';
|
||||
import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js';
|
||||
import { InlineEditsInsertionView } from './inlineEditsViews/inlineEditsInsertionView.js';
|
||||
import { InlineEditsLineReplacementView } from './inlineEditsViews/inlineEditsLineReplacementView.js';
|
||||
import { InlineEditsSideBySideView } from './inlineEditsViews/inlineEditsSideBySideView.js';
|
||||
import { InlineEditsWordReplacementView } from './inlineEditsViews/inlineEditsWordReplacementView.js';
|
||||
import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineEditsViews/originalEditorInlineDiffView.js';
|
||||
import { applyEditToModifiedRangeMappings, createReindentEdit, InlineEditTabAction } from './utils/utils.js';
|
||||
import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils/utils.js';
|
||||
import './view.css';
|
||||
|
||||
export class InlineEditsView extends Disposable {
|
||||
@@ -44,6 +43,8 @@ export class InlineEditsView extends Disposable {
|
||||
private readonly _showCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed);
|
||||
private readonly _useMultiLineGhostText = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText);
|
||||
|
||||
private readonly _tabAction = derived<InlineEditTabAction>(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive);
|
||||
|
||||
private _previousView: {
|
||||
id: string;
|
||||
view: ReturnType<typeof InlineEditsView.prototype.determineView>;
|
||||
@@ -54,14 +55,19 @@ export class InlineEditsView extends Disposable {
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _edit: IObservable<InlineEditWithChanges | undefined>,
|
||||
private readonly _model: IObservable<InlineCompletionsModel | undefined>,
|
||||
private readonly _model: IObservable<InlineEditModel | undefined>,
|
||||
private readonly _ghostTextIndicator: IObservable<GhostTextIndicator | undefined>,
|
||||
private readonly _focusIsInMenu: ISettableObservable<boolean>,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(autorunWithStore((reader, store) => {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.add(
|
||||
Event.any(
|
||||
this._sideBySide.onDidClick,
|
||||
@@ -71,9 +77,9 @@ export class InlineEditsView extends Disposable {
|
||||
...this._wordReplacementViews.read(reader).map(w => w.onDidClick),
|
||||
this._inlineDiffView.onDidClick,
|
||||
)(e => {
|
||||
if (this._viewHasBeenShownLongThan(350)) {
|
||||
if (this._viewHasBeenShownLongerThan(350)) {
|
||||
e.preventDefault();
|
||||
this._host.accept();
|
||||
model.accept();
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -93,35 +99,36 @@ export class InlineEditsView extends Disposable {
|
||||
newTextLineCount: number;
|
||||
originalDisplayRange: LineRange;
|
||||
} | undefined>(this, reader => {
|
||||
const edit = this._edit.read(reader);
|
||||
if (!edit) {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._model.get()?.handleInlineEditShown(edit.inlineCompletion);
|
||||
model.handleInlineEditShown();
|
||||
|
||||
let mappings = RangeMapping.fromEdit(edit.edit);
|
||||
let newText = edit.edit.apply(edit.originalText);
|
||||
let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText));
|
||||
const inlineEdit = model.inlineEdit;
|
||||
let mappings = RangeMapping.fromEdit(inlineEdit.edit);
|
||||
let newText = inlineEdit.edit.apply(inlineEdit.originalText);
|
||||
let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText));
|
||||
|
||||
const originalDisplayRange = edit.originalText.lineRange.intersect(
|
||||
edit.originalLineRange.join(
|
||||
LineRange.ofLength(edit.originalLineRange.startLineNumber, edit.lineEdit.newLines.length)
|
||||
const originalDisplayRange = inlineEdit.originalText.lineRange.intersect(
|
||||
inlineEdit.originalLineRange.join(
|
||||
LineRange.ofLength(inlineEdit.originalLineRange.startLineNumber, inlineEdit.lineEdit.newLines.length)
|
||||
)
|
||||
)!;
|
||||
|
||||
let state = this.determineRenderState(edit, reader, diff, new StringText(newText), originalDisplayRange);
|
||||
let state = this.determineRenderState(model, reader, diff, new StringText(newText), originalDisplayRange);
|
||||
if (!state) {
|
||||
this._model.get()?.stop();
|
||||
model.abort(`unable to determine view: tried to render ${this._previousView?.view}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state.kind === 'sideBySide') {
|
||||
const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange);
|
||||
const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange);
|
||||
newText = indentationAdjustmentEdit.applyToString(newText);
|
||||
|
||||
mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit);
|
||||
diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText));
|
||||
diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText));
|
||||
}
|
||||
|
||||
this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId());
|
||||
@@ -132,16 +139,16 @@ export class InlineEditsView extends Disposable {
|
||||
this._previewTextModel.setValue(newText);
|
||||
}
|
||||
|
||||
if (this._showCollapsed.read(reader) && this._host.tabAction.read(reader) !== InlineEditTabAction.Accept && !this._indicator.read(reader)?.isHoverVisible.read(reader) && !this._model.get()!.inAcceptFlow.read(reader)) {
|
||||
if (this._showCollapsed.read(reader) && model.tabAction.read(reader) !== InlineEditTabAction.Accept && !this._indicator.read(reader)?.isHoverVisible.read(reader) && !model.inAcceptFlow.read(reader)) {
|
||||
state = { kind: 'hidden' };
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
diff,
|
||||
edit,
|
||||
edit: inlineEdit,
|
||||
newText,
|
||||
newTextLineCount: edit.modifiedLineRange.length,
|
||||
newTextLineCount: inlineEdit.modifiedLineRange.length,
|
||||
originalDisplayRange: originalDisplayRange,
|
||||
};
|
||||
});
|
||||
@@ -154,37 +161,6 @@ export class InlineEditsView extends Disposable {
|
||||
null
|
||||
));
|
||||
|
||||
// TODO: This has become messy, should it be passed in to the InlineEditsView? Maybe include in accept flow?
|
||||
private readonly _host: IInlineEditsViewHost = {
|
||||
displayName: derivedObservableWithCache<string>(this, (reader, previousDisplayName) => {
|
||||
const state = this._model.read(reader)?.inlineEditState;
|
||||
const item = state?.read(reader);
|
||||
const completionSource = item?.inlineCompletion?.source;
|
||||
// TODO: expose the provider (typed) and expose the provider the edit belongs to typing and get correct edit
|
||||
return (completionSource?.inlineCompletions as any)?.edits?.[0]?.provider?.displayName ?? previousDisplayName
|
||||
?? completionSource?.provider.displayName ?? localize('inlineEdit', "Inline Edit");
|
||||
}),
|
||||
tabAction: derived<InlineEditTabAction>(this, reader => {
|
||||
const m = this._model.read(reader);
|
||||
if (this._editorObs.isFocused.read(reader)) {
|
||||
if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; }
|
||||
if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; }
|
||||
if (m && m.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) { return InlineEditTabAction.Accept; }
|
||||
}
|
||||
return InlineEditTabAction.Inactive;
|
||||
}),
|
||||
action: this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.action),
|
||||
extensionCommands: this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.source.inlineCompletions.commands ?? []),
|
||||
accept: () => {
|
||||
this._model.get()?.accept();
|
||||
},
|
||||
jump: () => {
|
||||
this._model.get()?.jump();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useGutterIndicator);
|
||||
|
||||
private readonly _indicatorCyclicDependencyCircuitBreaker = observableValue(this, false);
|
||||
|
||||
protected readonly _indicator = derivedWithStore<InlineEditsGutterIndicator | InlineEditsIndicator | undefined>(this, (reader, store) => {
|
||||
@@ -193,9 +169,9 @@ export class InlineEditsView extends Disposable {
|
||||
}
|
||||
|
||||
const indicatorDisplayRange = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => {
|
||||
const s = this._model.read(reader)?.inlineCompletionState.read(reader);
|
||||
if (s && s.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) {
|
||||
return LineRange.ofLength(s.primaryGhostText.lineNumber, 1);
|
||||
const ghostTextIndicator = this._ghostTextIndicator.read(reader);
|
||||
if (ghostTextIndicator) {
|
||||
return ghostTextIndicator.lineRange;
|
||||
}
|
||||
|
||||
const state = this._uiState.read(reader);
|
||||
@@ -205,29 +181,29 @@ export class InlineEditsView extends Disposable {
|
||||
return state?.originalDisplayRange;
|
||||
});
|
||||
|
||||
if (this._useGutterIndicator.read(reader)) {
|
||||
return store.add(this._instantiationService.createInstance(
|
||||
InlineEditsGutterIndicator,
|
||||
this._editorObs,
|
||||
indicatorDisplayRange,
|
||||
this._gutterIndicatorOffset,
|
||||
this._host,
|
||||
this._inlineEditsIsHovered,
|
||||
this._focusIsInMenu,
|
||||
));
|
||||
} else {
|
||||
return store.add(new InlineEditsIndicator(
|
||||
this._editorObs,
|
||||
derived<IInlineEditsIndicatorState | undefined>(reader => {
|
||||
const state = this._uiState.read(reader);
|
||||
const range = indicatorDisplayRange.read(reader);
|
||||
if (!state || !state.state || !range) { return undefined; }
|
||||
const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader) + this._gutterIndicatorOffset.read(reader);
|
||||
return { editTop: top, showAlways: state.state.kind !== 'sideBySide' };
|
||||
}),
|
||||
this._model,
|
||||
));
|
||||
}
|
||||
const modelWithGhostTextSupport = derived<InlineEditModel | undefined>(this, reader => {
|
||||
const model = this._model.read(reader);
|
||||
if (model) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const ghostTextIndicator = this._ghostTextIndicator.read(reader);
|
||||
if (ghostTextIndicator) {
|
||||
return ghostTextIndicator.model;
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
|
||||
return store.add(this._instantiationService.createInstance(
|
||||
InlineEditsGutterIndicator,
|
||||
this._editorObs,
|
||||
indicatorDisplayRange,
|
||||
this._gutterIndicatorOffset,
|
||||
modelWithGhostTextSupport,
|
||||
this._inlineEditsIsHovered,
|
||||
this._focusIsInMenu,
|
||||
));
|
||||
});
|
||||
|
||||
private readonly _inlineEditsIsHovered = derived(this, reader => {
|
||||
@@ -249,24 +225,24 @@ export class InlineEditsView extends Disposable {
|
||||
|
||||
private readonly _sideBySide = this._register(this._instantiationService.createInstance(InlineEditsSideBySideView,
|
||||
this._editor,
|
||||
this._edit,
|
||||
this._model.map(m => m?.inlineEdit),
|
||||
this._previewTextModel,
|
||||
this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({
|
||||
edit: s.edit,
|
||||
newTextLineCount: s.newTextLineCount,
|
||||
originalDisplayRange: s.originalDisplayRange,
|
||||
}) : undefined),
|
||||
this._host,
|
||||
this._tabAction,
|
||||
));
|
||||
|
||||
protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView,
|
||||
this._editor,
|
||||
this._edit,
|
||||
this._model.map(m => m?.inlineEdit),
|
||||
this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({
|
||||
originalRange: s.state.originalRange,
|
||||
deletions: s.state.deletions,
|
||||
}) : undefined),
|
||||
this._host,
|
||||
this._tabAction,
|
||||
));
|
||||
|
||||
protected readonly _insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView,
|
||||
@@ -276,7 +252,7 @@ export class InlineEditsView extends Disposable {
|
||||
startColumn: s.state.column,
|
||||
text: s.state.text,
|
||||
}) : undefined),
|
||||
this._host,
|
||||
this._tabAction,
|
||||
));
|
||||
|
||||
private readonly _inlineDiffViewState = derived<IOriginalEditorInlineDiffViewState | undefined>(this, reader => {
|
||||
@@ -296,7 +272,7 @@ export class InlineEditsView extends Disposable {
|
||||
protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel));
|
||||
|
||||
protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => {
|
||||
return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, [e], this._host));
|
||||
return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, [e], this._tabAction));
|
||||
});
|
||||
|
||||
protected readonly _lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView,
|
||||
@@ -307,21 +283,23 @@ export class InlineEditsView extends Disposable {
|
||||
modifiedLines: s.state.modifiedLines,
|
||||
replacements: s.state.replacements,
|
||||
}) : undefined),
|
||||
this._host
|
||||
this._tabAction,
|
||||
));
|
||||
|
||||
private getCacheId(edit: InlineEditWithChanges) {
|
||||
if (this._model.get()?.inAcceptPartialFlow.get()) {
|
||||
return `${edit.inlineCompletion.id}_${edit.edit.edits.map(edit => edit.range.toString() + edit.text).join(',')}`;
|
||||
private getCacheId(model: IInlineEditModel) {
|
||||
const inlineEdit = model.inlineEdit;
|
||||
if (model.inPartialAcceptFlow.get()) {
|
||||
return `${inlineEdit.inlineCompletion.id}_${inlineEdit.edit.edits.map(innerEdit => innerEdit.range.toString() + innerEdit.text).join(',')}`;
|
||||
}
|
||||
|
||||
return edit.inlineCompletion.id;
|
||||
return inlineEdit.inlineCompletion.id;
|
||||
}
|
||||
|
||||
private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string {
|
||||
private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string {
|
||||
// Check if we can use the previous view if it is the same InlineCompletion as previously shown
|
||||
const canUseCache = this._previousView?.id === this.getCacheId(edit);
|
||||
const reconsiderViewAfterJump = edit.userJumpedToIt !== this._previousView?.userJumpedToIt &&
|
||||
const inlineEdit = model.inlineEdit;
|
||||
const canUseCache = this._previousView?.id === this.getCacheId(model);
|
||||
const reconsiderViewAfterJump = inlineEdit.userJumpedToIt !== this._previousView?.userJumpedToIt &&
|
||||
(
|
||||
(this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') ||
|
||||
(this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines')
|
||||
@@ -344,13 +322,13 @@ export class InlineEditsView extends Disposable {
|
||||
isSingleInnerEdit && (
|
||||
this._useMixedLinesDiff.read(reader) === 'forStableInsertions'
|
||||
&& this._useCodeShifting.read(reader)
|
||||
&& isSingleLineInsertionAfterPosition(diff, edit.cursorPosition)
|
||||
&& isSingleLineInsertionAfterPosition(diff, inlineEdit.cursorPosition)
|
||||
)
|
||||
) {
|
||||
return 'insertionInline';
|
||||
}
|
||||
|
||||
const innerValues = inner.map(m => ({ original: edit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) }));
|
||||
const innerValues = inner.map(m => ({ original: inlineEdit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) }));
|
||||
if (innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== ''))) {
|
||||
return 'deletion';
|
||||
}
|
||||
@@ -359,20 +337,20 @@ export class InlineEditsView extends Disposable {
|
||||
return 'insertionMultiLine';
|
||||
}
|
||||
|
||||
const numOriginalLines = edit.originalLineRange.length;
|
||||
const numModifiedLines = edit.modifiedLineRange.length;
|
||||
const numOriginalLines = inlineEdit.originalLineRange.length;
|
||||
const numModifiedLines = inlineEdit.modifiedLineRange.length;
|
||||
const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < InlineEditsWordReplacementView.MAX_LENGTH && TextLength.ofRange(m.modifiedRange).columnCount < InlineEditsWordReplacementView.MAX_LENGTH);
|
||||
if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) {
|
||||
// Make sure there is no insertion, even if we grow them
|
||||
if (
|
||||
!inner.some(m => m.originalRange.isEmpty()) ||
|
||||
!growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), edit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH)
|
||||
!growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), inlineEdit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH)
|
||||
) {
|
||||
return 'wordReplacements';
|
||||
}
|
||||
}
|
||||
if (numOriginalLines > 0 && numModifiedLines > 0) {
|
||||
if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, edit, originalDisplayRange, reader)) {
|
||||
if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, inlineEdit, originalDisplayRange, reader)) {
|
||||
return 'sideBySide';
|
||||
}
|
||||
|
||||
@@ -380,24 +358,25 @@ export class InlineEditsView extends Disposable {
|
||||
}
|
||||
|
||||
if (
|
||||
(this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible'))
|
||||
(this._useMixedLinesDiff.read(reader) === 'whenPossible' || (inlineEdit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible'))
|
||||
&& diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m))
|
||||
) {
|
||||
return 'mixedLines';
|
||||
}
|
||||
|
||||
if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) {
|
||||
if (this._useInterleavedLinesDiff.read(reader) === 'always' || (inlineEdit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) {
|
||||
return 'interleavedLines';
|
||||
}
|
||||
|
||||
return 'sideBySide';
|
||||
}
|
||||
|
||||
private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) {
|
||||
private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) {
|
||||
const inlineEdit = model.inlineEdit;
|
||||
|
||||
const view = this.determineView(edit, reader, diff, newText, originalDisplayRange);
|
||||
const view = this.determineView(model, reader, diff, newText, originalDisplayRange);
|
||||
|
||||
this._previousView = { id: this.getCacheId(edit), view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() };
|
||||
this._previousView = { id: this.getCacheId(model), view, userJumpedToIt: inlineEdit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() };
|
||||
|
||||
switch (view) {
|
||||
case 'insertionInline': return { kind: 'insertionInline' as const };
|
||||
@@ -412,7 +391,7 @@ export class InlineEditsView extends Disposable {
|
||||
if (view === 'deletion') {
|
||||
return {
|
||||
kind: 'deletion' as const,
|
||||
originalRange: edit.originalLineRange,
|
||||
originalRange: inlineEdit.originalLineRange,
|
||||
deletions: inner.map(m => m.originalRange),
|
||||
};
|
||||
}
|
||||
@@ -433,10 +412,10 @@ export class InlineEditsView extends Disposable {
|
||||
}
|
||||
|
||||
if (view === 'wordReplacements') {
|
||||
let grownEdits = growEditsToEntireWord(replacements, edit.originalText);
|
||||
let grownEdits = growEditsToEntireWord(replacements, inlineEdit.originalText);
|
||||
|
||||
if (grownEdits.some(e => e.range.isEmpty())) {
|
||||
grownEdits = growEditsUntilWhitespace(replacements, edit.originalText);
|
||||
grownEdits = growEditsUntilWhitespace(replacements, inlineEdit.originalText);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -448,9 +427,9 @@ export class InlineEditsView extends Disposable {
|
||||
if (view === 'lineReplacement') {
|
||||
return {
|
||||
kind: 'lineReplacement' as const,
|
||||
originalRange: edit.originalLineRange,
|
||||
modifiedRange: edit.modifiedLineRange,
|
||||
modifiedLines: edit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)),
|
||||
originalRange: inlineEdit.originalLineRange,
|
||||
modifiedRange: inlineEdit.modifiedLineRange,
|
||||
modifiedLines: inlineEdit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)),
|
||||
replacements: inner.map(m => ({ originalRange: m.originalRange, modifiedRange: m.modifiedRange })),
|
||||
};
|
||||
}
|
||||
@@ -458,7 +437,7 @@ export class InlineEditsView extends Disposable {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _viewHasBeenShownLongThan(durationMs: number): boolean {
|
||||
private _viewHasBeenShownLongerThan(durationMs: number): boolean {
|
||||
const viewCreationTime = this._previousView?.timestamp;
|
||||
if (!viewCreationTime) {
|
||||
throw new BugIndicatingError('viewHasBeenShownLongThan called before a view has been shown');
|
||||
|
||||
@@ -7,18 +7,31 @@ import { IMouseEvent } from '../../../../../../base/browser/mouseEvent.js';
|
||||
import { Event } from '../../../../../../base/common/event.js';
|
||||
import { IObservable } from '../../../../../../base/common/observable.js';
|
||||
import { Command } from '../../../../../common/languages.js';
|
||||
import { InlineEditTabAction } from './utils/utils.js';
|
||||
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
|
||||
|
||||
export enum InlineEditTabAction {
|
||||
Jump = 'jump',
|
||||
Accept = 'accept',
|
||||
Inactive = 'inactive'
|
||||
}
|
||||
|
||||
export interface IInlineEditsView {
|
||||
isHovered: IObservable<boolean>;
|
||||
onDidClick: Event<IMouseEvent>;
|
||||
}
|
||||
|
||||
export interface IInlineEditsViewHost {
|
||||
displayName: IObservable<string>;
|
||||
action: IObservable<Command | undefined>;
|
||||
export interface IInlineEditModel {
|
||||
displayName: string;
|
||||
action: Command | undefined;
|
||||
extensionCommands: Command[];
|
||||
inlineEdit: InlineEditWithChanges;
|
||||
|
||||
tabAction: IObservable<InlineEditTabAction>;
|
||||
extensionCommands: IObservable<readonly Command[] | undefined>;
|
||||
inAcceptFlow: IObservable<boolean>;
|
||||
inPartialAcceptFlow: IObservable<boolean>;
|
||||
|
||||
handleInlineEditShown(): void;
|
||||
accept(): void;
|
||||
jump(): void;
|
||||
abort(reason: string): void;
|
||||
}
|
||||
|
||||
@@ -8,17 +8,23 @@ import { Disposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { derived, IObservable, ISettableObservable } from '../../../../../../base/common/observable.js';
|
||||
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ICodeEditor } from '../../../../../browser/editorBrowser.js';
|
||||
import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js';
|
||||
import { LineRange } from '../../../../../common/core/lineRange.js';
|
||||
import { Range } from '../../../../../common/core/range.js';
|
||||
import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js';
|
||||
import { TextModelText } from '../../../../../common/model/textModelText.js';
|
||||
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
|
||||
import { InlineEdit } from '../../model/inlineEdit.js';
|
||||
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
|
||||
import { GhostTextIndicator, InlineEditModel } from './inlineEditsModel.js';
|
||||
import { InlineEditsView } from './inlineEditsView.js';
|
||||
import { InlineEditTabAction } from './inlineEditsViewInterface.js';
|
||||
|
||||
export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This class is no longer a diff producer. Rename it or get rid of it
|
||||
public static readonly hot = createHotClass(InlineEditsViewAndDiffProducer);
|
||||
|
||||
private readonly _editorObs = observableCodeEditor(this._editor);
|
||||
|
||||
private readonly _inlineEdit = derived<InlineEditWithChanges | undefined>(this, (reader) => {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) { return undefined; }
|
||||
@@ -30,7 +36,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
|
||||
const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit.read(reader);
|
||||
if (!editOffset) { return undefined; }
|
||||
|
||||
const offsetEdits = model.inAcceptPartialFlow.read(reader) ? [editOffset.edits[0]] : editOffset.edits;
|
||||
const offsetEdits = model.inPartialAcceptFlow.read(reader) ? [editOffset.edits[0]] : editOffset.edits;
|
||||
const edits = offsetEdits.map(e => {
|
||||
const innerEditRange = Range.fromPositions(
|
||||
textModel.getPositionAt(e.replaceRange.start),
|
||||
@@ -45,6 +51,41 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
|
||||
return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion);
|
||||
});
|
||||
|
||||
private readonly _inlineEditModel = derived<InlineEditModel | undefined>(this, reader => {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) { return undefined; }
|
||||
const edit = this._inlineEdit.read(reader);
|
||||
if (!edit) { return undefined; }
|
||||
|
||||
const tabAction = derived<InlineEditTabAction>(this, reader => {
|
||||
if (this._editorObs.isFocused.read(reader)) {
|
||||
if (model.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; }
|
||||
if (model.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; }
|
||||
}
|
||||
return InlineEditTabAction.Inactive;
|
||||
});
|
||||
|
||||
return new InlineEditModel(model, edit, tabAction);
|
||||
});
|
||||
|
||||
private readonly _ghostTextIndicator = derived<GhostTextIndicator | undefined>(this, reader => {
|
||||
const model = this._model.read(reader);
|
||||
if (!model) { return undefined; }
|
||||
const state = model.inlineCompletionState.read(reader);
|
||||
if (!state) { return undefined; }
|
||||
const inlineCompletion = state.inlineCompletion;
|
||||
if (!inlineCompletion) { return undefined; }
|
||||
|
||||
if (!inlineCompletion.sourceInlineCompletion.showInlineEditMenu) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lineRange = LineRange.ofLength(state.primaryGhostText.lineNumber, 1);
|
||||
const renderExplicitly = false;
|
||||
|
||||
return new GhostTextIndicator(this._editor, model, lineRange, inlineCompletion, renderExplicitly);
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _edit: IObservable<InlineEdit | undefined>,
|
||||
@@ -54,6 +95,6 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model, this._focusIsInMenu));
|
||||
this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEditModel, this._ghostTextIndicator, this._focusIsInMenu));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { Point } from '../../../../../../browser/point.js';
|
||||
import { LineRange } from '../../../../../../common/core/lineRange.js';
|
||||
import { Position } from '../../../../../../common/core/position.js';
|
||||
import { Range } from '../../../../../../common/core/range.js';
|
||||
import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { InlineEditWithChanges } from '../inlineEditWithChanges.js';
|
||||
import { getOriginalBorderColor, originalBackgroundColor } from '../theme.js';
|
||||
import { createRectangle, getPrefixTrim, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js';
|
||||
@@ -32,7 +32,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV
|
||||
originalRange: LineRange;
|
||||
deletions: Range[];
|
||||
} | undefined>,
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _tabAction: IObservable<InlineEditTabAction>,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -151,7 +151,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV
|
||||
{ hideLeft: layoutInfo.horizontalScrollOffset !== 0 }
|
||||
);
|
||||
|
||||
const originalBorderColor = getOriginalBorderColor(this._host.tabAction).read(reader);
|
||||
const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader);
|
||||
|
||||
return [
|
||||
n.svgElem('path', {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
|
||||
import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js';
|
||||
import { GhostText, GhostTextPart } from '../../../model/ghostText.js';
|
||||
import { GhostTextView } from '../../ghostText/ghostTextView.js';
|
||||
import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { getModifiedBorderColor, modifiedChangedLineBackgroundColor } from '../theme.js';
|
||||
import { createRectangle, getPrefixTrim, mapOutFalsy } from '../utils/utils.js';
|
||||
|
||||
@@ -106,7 +106,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits
|
||||
startColumn: number;
|
||||
text: string;
|
||||
} | undefined>,
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _tabAction: IObservable<InlineEditTabAction>,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ILanguageService private readonly _languageService: ILanguageService,
|
||||
) {
|
||||
@@ -255,7 +255,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits
|
||||
{ hideLeft: croppedOverlay.left !== overlay.left }
|
||||
);
|
||||
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader);
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader);
|
||||
|
||||
return [
|
||||
n.svgElem('path', {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../
|
||||
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
|
||||
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
|
||||
import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js';
|
||||
import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { getModifiedBorderColor, modifiedChangedLineBackgroundColor } from '../theme.js';
|
||||
import { getPrefixTrim, mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
import { rangesToBubbleRanges, Replacement } from './inlineEditsWordReplacementView.js';
|
||||
@@ -197,7 +197,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin
|
||||
l.style.position = 'relative';
|
||||
});
|
||||
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader);
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader);
|
||||
|
||||
return [
|
||||
n.div({
|
||||
@@ -287,7 +287,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin
|
||||
modifiedLines: string[];
|
||||
replacements: Replacement[];
|
||||
} | undefined>,
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _tabAction: IObservable<InlineEditTabAction>,
|
||||
@ILanguageService private readonly _languageService: ILanguageService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Range } from '../../../../../../common/core/range.js';
|
||||
import { ITextModel } from '../../../../../../common/model.js';
|
||||
import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js';
|
||||
import { InlineCompletionContextKeys } from '../../../controller/inlineCompletionContextKeys.js';
|
||||
import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { InlineEditWithChanges } from '../inlineEditWithChanges.js';
|
||||
import { getModifiedBorderColor, getOriginalBorderColor, modifiedBackgroundColor, originalBackgroundColor } from '../theme.js';
|
||||
import { PathBuilder, createRectangle, getOffsetForPos, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js';
|
||||
@@ -64,7 +64,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit
|
||||
newTextLineCount: number;
|
||||
originalDisplayRange: LineRange;
|
||||
} | undefined>,
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _tabAction: IObservable<InlineEditTabAction>,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
) {
|
||||
@@ -123,7 +123,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit
|
||||
private readonly previewRef = n.ref<HTMLDivElement>();
|
||||
|
||||
private readonly _editorContainer = n.div({
|
||||
class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.useGutterIndicator && 'showHover')],
|
||||
class: ['editorContainer'],
|
||||
style: { position: 'absolute', overflow: 'hidden', cursor: 'pointer' },
|
||||
onmousedown: e => {
|
||||
e.preventDefault(); // This prevents that the editor loses focus
|
||||
@@ -508,8 +508,8 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit
|
||||
const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader);
|
||||
if (!layoutInfoObs) { return undefined; }
|
||||
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader);
|
||||
const originalBorderColor = getOriginalBorderColor(this._host.tabAction).read(reader);
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader);
|
||||
const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader);
|
||||
|
||||
return [
|
||||
n.svgElem('path', {
|
||||
|
||||
@@ -14,9 +14,9 @@ import { Rect } from '../../../../../../browser/rect.js';
|
||||
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
|
||||
import { OffsetRange } from '../../../../../../common/core/offsetRange.js';
|
||||
import { SingleTextEdit } from '../../../../../../common/core/textEdit.js';
|
||||
import { IInlineEditsView } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { getModifiedBorderColor } from '../theme.js';
|
||||
import { InlineEditTabAction, mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
import { mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
|
||||
export class InlineEditsWordInsertView extends Disposable implements IInlineEditsView {
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getWindow, n, ObserverNodeWithElement } from '../../../../../../../base
|
||||
import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js';
|
||||
import { Emitter } from '../../../../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { constObservable, derived, observableValue } from '../../../../../../../base/common/observable.js';
|
||||
import { constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js';
|
||||
import { editorBackground, editorHoverForeground, scrollbarShadow } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
|
||||
@@ -22,7 +22,7 @@ import { SingleTextEdit } from '../../../../../../common/core/textEdit.js';
|
||||
import { ILanguageService } from '../../../../../../common/languages/language.js';
|
||||
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
|
||||
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
|
||||
import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js';
|
||||
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
|
||||
import { getModifiedBorderColor, modifiedChangedTextOverlayColor, originalChangedTextOverlayColor, replacementViewBackground } from '../theme.js';
|
||||
import { mapOutFalsy, rectToProps } from '../utils/utils.js';
|
||||
|
||||
@@ -47,7 +47,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin
|
||||
/** Must be single-line in both sides */
|
||||
private readonly _edit: SingleTextEdit,
|
||||
private readonly _innerEdits: SingleTextEdit[],
|
||||
private readonly _host: IInlineEditsViewHost,
|
||||
private readonly _tabAction: IObservable<InlineEditTabAction>,
|
||||
@ILanguageService private readonly _languageService: ILanguageService,
|
||||
) {
|
||||
super();
|
||||
@@ -162,7 +162,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin
|
||||
|
||||
const edits = layoutProps.innerEdits.map(edit => ({ modified: edit.modified.translateX(-contentLeft), original: edit.original.translateX(-contentLeft) }));
|
||||
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader);
|
||||
const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader);
|
||||
|
||||
return [
|
||||
n.div({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IObservable } from '../../../../../../base/common/observable.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { diffRemoved, diffInsertedLine, diffInserted, editorHoverBorder, editorHoverStatusBarBackground, buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { registerColor, transparent, asCssVariable, lighten, darken } from '../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { InlineEditTabAction } from './utils/utils.js';
|
||||
import { InlineEditTabAction } from './inlineEditsViewInterface.js';
|
||||
|
||||
export const originalBackgroundColor = registerColor(
|
||||
'inlineEdit.originalBackground',
|
||||
|
||||
@@ -26,12 +26,6 @@ import { SingleTextEdit, TextEdit } from '../../../../../../common/core/textEdit
|
||||
import { RangeMapping } from '../../../../../../common/diff/rangeMapping.js';
|
||||
import { indentOfLine } from '../../../../../../common/model/textModel.js';
|
||||
|
||||
export enum InlineEditTabAction {
|
||||
Jump = 'jump',
|
||||
Accept = 'accept',
|
||||
Inactive = 'inactive'
|
||||
}
|
||||
|
||||
export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number {
|
||||
editor.layoutInfo.read(reader);
|
||||
editor.value.read(reader);
|
||||
|
||||
Reference in New Issue
Block a user