Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts
3296 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() {90const binding = this._keybindingService.lookupKeybinding(this.showCommand.id)?.getLabel();91this.button.element.title = this.showCommand.label + (binding ? ` (${binding})` : '');92}9394private create(): void {95this.domNode = dom.$('.post-edit-widget');9697this.button = this._register(new Button(this.domNode, {98supportIcons: true,99}));100this.button.label = '$(insert)';101102this._register(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, () => this.showSelector()));103}104105getId(): string {106return PostEditWidget.baseId + '.' + this.typeId;107}108109getDomNode(): HTMLElement {110return this.domNode;111}112113getPosition(): IContentWidgetPosition | null {114return {115position: this.range.getEndPosition(),116preference: [ContentWidgetPositionPreference.BELOW]117};118}119120showSelector() {121const pos = dom.getDomNodePagePosition(this.button.element);122const anchor = { x: pos.left + pos.width, y: pos.top + pos.height };123124this._actionWidgetService.show('postEditWidget', false,125this.edits.allEdits.map((edit, i): IActionListItem<T> => {126return {127kind: ActionListItemKind.Action,128item: edit,129label: edit.title,130disabled: false,131canPreview: false,132group: { title: '', icon: ThemeIcon.fromId(i === this.edits.activeEditIndex ? Codicon.check.id : Codicon.blank.id) },133};134}), {135onHide: () => {136this.editor.focus();137},138onSelect: (item) => {139this._actionWidgetService.hide(false);140141const i = this.edits.allEdits.findIndex(edit => edit === item);142if (i !== this.edits.activeEditIndex) {143return this.onSelectNewEdit(i);144}145},146}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);147}148}149150export class PostEditWidgetManager<T extends DocumentPasteEdit | DocumentDropEdit> extends Disposable {151152private readonly _currentWidget = this._register(new MutableDisposable<PostEditWidget<T>>());153154constructor(155private readonly _id: string,156private readonly _editor: ICodeEditor,157private readonly _visibleContext: RawContextKey<boolean>,158private readonly _showCommand: ShowCommand,159private readonly _getAdditionalActions: () => readonly IAction[],160@IInstantiationService private readonly _instantiationService: IInstantiationService,161@IBulkEditService private readonly _bulkEditService: IBulkEditService,162@INotificationService private readonly _notificationService: INotificationService,163) {164super();165166this._register(Event.any(167_editor.onDidChangeModel,168_editor.onDidChangeModelContent,169)(() => this.clear()));170}171172public async applyEditAndShowIfNeeded(ranges: readonly Range[], edits: EditSet<T>, canShowWidget: boolean, resolve: (edit: T, token: CancellationToken) => Promise<T>, token: CancellationToken) {173if (!ranges.length || !this._editor.hasModel()) {174return;175}176177const model = this._editor.getModel();178const edit = edits.allEdits.at(edits.activeEditIndex);179if (!edit) {180return;181}182183const onDidSelectEdit = async (newEditIndex: number) => {184const model = this._editor.getModel();185if (!model) {186return;187}188189await model.undo();190this.applyEditAndShowIfNeeded(ranges, { activeEditIndex: newEditIndex, allEdits: edits.allEdits }, canShowWidget, resolve, token);191};192193const handleError = (e: Error, message: string) => {194if (isCancellationError(e)) {195return;196}197198this._notificationService.error(message);199if (canShowWidget) {200this.show(ranges[0], edits, onDidSelectEdit);201}202};203204const editorStateCts = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token);205let resolvedEdit: T;206try {207resolvedEdit = await raceCancellationError(resolve(edit, editorStateCts.token), editorStateCts.token);208} catch (e) {209return handleError(e, localize('resolveError', "Error resolving edit '{0}':\n{1}", edit.title, toErrorMessage(e)));210} finally {211editorStateCts.dispose();212}213214if (token.isCancellationRequested) {215return;216}217218const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, ranges, resolvedEdit);219220// Use a decoration to track edits around the trigger range221const primaryRange = ranges[0];222const editTrackingDecoration = model.deltaDecorations([], [{223range: primaryRange,224options: { description: 'paste-line-suffix', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }225}]);226227this._editor.focus();228let editResult: IBulkEditResult;229let editRange: Range | null;230try {231editResult = await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor, token });232editRange = model.getDecorationRange(editTrackingDecoration[0]);233} catch (e) {234return handleError(e, localize('applyError', "Error applying edit '{0}':\n{1}", edit.title, toErrorMessage(e)));235} finally {236model.deltaDecorations(editTrackingDecoration, []);237}238239if (token.isCancellationRequested) {240return;241}242243if (canShowWidget && editResult.isApplied && edits.allEdits.length > 1) {244this.show(editRange ?? primaryRange, edits, onDidSelectEdit);245}246}247248public show(range: Range, edits: EditSet<T>, onDidSelectEdit: (newIndex: number) => void) {249this.clear();250251if (this._editor.hasModel()) {252this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget<T>, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions());253}254}255256public clear() {257this._currentWidget.clear();258}259260public tryShowSelector() {261this._currentWidget.value?.showSelector();262}263}264265266