Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts
5272 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { Button } from '../../../../base/browser/ui/button/button.js';7import { IAction } from '../../../../base/common/actions.js';8import { raceCancellationError } from '../../../../base/common/async.js';9import { CancellationToken } from '../../../../base/common/cancellation.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { toErrorMessage } from '../../../../base/common/errorMessage.js';12import { isCancellationError } from '../../../../base/common/errors.js';13import { Event } from '../../../../base/common/event.js';14import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';15import { ThemeIcon } from '../../../../base/common/themables.js';16import { localize } from '../../../../nls.js';17import { ActionListItemKind, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';18import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';19import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';20import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';21import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';22import { INotificationService } from '../../../../platform/notification/common/notification.js';23import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';24import { IBulkEditResult, IBulkEditService } from '../../../browser/services/bulkEditService.js';25import { Range } from '../../../common/core/range.js';26import { DocumentDropEdit, DocumentPasteEdit } from '../../../common/languages.js';27import { TrackedRangeStickiness } from '../../../common/model.js';28import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';29import { createCombinedWorkspaceEdit } from './edit.js';30import './postEditWidget.css';313233interface EditSet<Edit extends DocumentPasteEdit | DocumentDropEdit> {34readonly activeEditIndex: number;35readonly allEdits: ReadonlyArray<Edit>;36}3738interface ShowCommand {39readonly id: string;40readonly label: string;41}4243class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable implements IContentWidget {44private static readonly baseId = 'editor.widget.postEditWidget';4546readonly allowEditorOverflow = true;47readonly suppressMouseDown = true;4849private domNode!: HTMLElement;50private button!: Button;5152private readonly visibleContext: IContextKey<boolean>;5354constructor(55private readonly typeId: string,56private readonly editor: ICodeEditor,57visibleContext: RawContextKey<boolean>,58private readonly showCommand: ShowCommand,59private readonly range: Range,60private readonly edits: EditSet<T>,61private readonly onSelectNewEdit: (editIndex: number) => void,62private readonly additionalActions: readonly IAction[],63@IContextKeyService contextKeyService: IContextKeyService,64@IKeybindingService private readonly _keybindingService: IKeybindingService,65@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,66) {67super();6869this.create();7071this.visibleContext = visibleContext.bindTo(contextKeyService);72this.visibleContext.set(true);73this._register(toDisposable(() => this.visibleContext.reset()));7475this.editor.addContentWidget(this);76this.editor.layoutContentWidget(this);7778this._register(toDisposable((() => this.editor.removeContentWidget(this))));7980this._register(this.editor.onDidChangeCursorPosition(e => {81this.dispose();82}));8384this._register(Event.runAndSubscribe(_keybindingService.onDidUpdateKeybindings, () => {85this._updateButtonTitle();86}));87}8889private _updateButtonTitle() {90this.button.element.title = this._keybindingService.appendKeybinding(this.showCommand.label, this.showCommand.id);91}9293private create(): void {94this.domNode = dom.$('.post-edit-widget');9596this.button = this._register(new Button(this.domNode, {97supportIcons: true,98}));99this.button.label = '$(insert)';100101this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => this.showSelector()));102}103104getId(): string {105return PostEditWidget.baseId + '.' + this.typeId;106}107108getDomNode(): HTMLElement {109return this.domNode;110}111112getPosition(): IContentWidgetPosition | null {113return {114position: this.range.getEndPosition(),115preference: [ContentWidgetPositionPreference.BELOW]116};117}118119showSelector() {120const pos = dom.getDomNodePagePosition(this.button.element);121const anchor = { x: pos.left + pos.width, y: pos.top + pos.height };122123this._actionWidgetService.show('postEditWidget', false,124this.edits.allEdits.map((edit, i): IActionListItem<T> => {125return {126kind: ActionListItemKind.Action,127item: edit,128label: edit.title,129disabled: false,130canPreview: false,131group: { title: '', icon: ThemeIcon.fromId(i === this.edits.activeEditIndex ? Codicon.check.id : Codicon.blank.id) },132};133}), {134onHide: () => {135this.editor.focus();136},137onSelect: (item) => {138this._actionWidgetService.hide(false);139140const i = this.edits.allEdits.findIndex(edit => edit === item);141if (i !== this.edits.activeEditIndex) {142return this.onSelectNewEdit(i);143}144},145}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);146}147}148149export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable {150151private readonly _currentWidget = this._register(new MutableDisposable<PostEditWidget<T>>());152153constructor(154private readonly _id: string,155private readonly _editor: ICodeEditor,156private readonly _visibleContext: RawContextKey<boolean>,157private readonly _showCommand: ShowCommand,158private readonly _getAdditionalActions: () => readonly IAction[],159@IInstantiationService private readonly _instantiationService: IInstantiationService,160@IBulkEditService private readonly _bulkEditService: IBulkEditService,161@INotificationService private readonly _notificationService: INotificationService,162) {163super();164165this._register(Event.any(166_editor.onDidChangeModel,167_editor.onDidChangeModelContent,168)(() => this.clear()));169}170171public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken) {172if (!ranges.length || !this._editor.hasModel()) {173return;174}175176const model = this._editor.getModel();177const edit = edits.allEdits.at(edits.activeEditIndex);178if (!edit) {179return;180}181182const onDidSelectEdit = async (newEditIndex: number) => {183const model = this._editor.getModel();184if (!model) {185return;186}187188await model.undo();189this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token);190};191192const handleError = (e: Error, message: string) => {193if (isCancellationError(e)) {194return;195}196197this._notificationService.error(message);198if (canShowWidget) {199this.show(ranges[0], edits, onDidSelectEdit);200}201};202203const editorStateCts = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);204let resolvedEdit: T;205try {206resolvedEdit = await raceCancellationError(resolve(edit, editorStateCts.token), editorStateCts.token);207} catch (e) {208return handleError(e, localize('resolveError', "Error resolving edit '{0}':\n{1}", edit.title, toErrorMessage(e)));209} finally {210editorStateCts.dispose();211}212213if (token.isCancellationRequested) {214return;215}216217const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit);218219// Use a decoration to track edits around the trigger range220const primaryRange = ranges[0];221const editTrackingDecoration = model.deltaDecorations([], [{222range: primaryRange,223options: { description: 'paste-line-suffix', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }224}]);225226this._editor.focus();227let editResult: IBulkEditResult;228let editRange: Range | null;229try {230editResult = await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor, token });231editRange = model.getDecorationRange(editTrackingDecoration[0]);232} catch (e) {233return handleError(e, localize('applyError', "Error applying edit '{0}':\n{1}", edit.title, toErrorMessage(e)));234} finally {235model.deltaDecorations(editTrackingDecoration, []);236}237238if (token.isCancellationRequested) {239return;240}241242if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) {243this.show(editRange ?? primaryRange, edits, onDidSelectEdit);244}245}246247public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number) => void) {248this.clear();249250if (this._editor.hasModel()) {251this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions());252}253}254255public clear() {256this._currentWidget.clear();257}258259public tryShowSelector() {260this._currentWidget.value?.showSelector();261}262}263264265