Path: blob/main/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.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 { IAction } from '../../../../base/common/actions.js';6import { coalesce } from '../../../../base/common/arrays.js';7import { CancelablePromise, createCancelablePromise, raceCancellation } from '../../../../base/common/async.js';8import { CancellationToken } from '../../../../base/common/cancellation.js';9import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';10import { isCancellationError } from '../../../../base/common/errors.js';11import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';12import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';13import { localize } from '../../../../nls.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';16import { LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { toExternalVSDataTransfer } from '../../../browser/dataTransfer.js';19import { ICodeEditor } from '../../../browser/editorBrowser.js';20import { EditorOption } from '../../../common/config/editorOptions.js';21import { IPosition } from '../../../common/core/position.js';22import { Range } from '../../../common/core/range.js';23import { IEditorContribution } from '../../../common/editorCommon.js';24import { DocumentDropEdit, DocumentDropEditProvider } from '../../../common/languages.js';25import { ITextModel } from '../../../common/model.js';26import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';27import { DraggedTreeItemsIdentifier } from '../../../common/services/treeViewsDnd.js';28import { ITreeViewsDnDService } from '../../../common/services/treeViewsDndService.js';29import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';30import { InlineProgressManager } from '../../inlineProgress/browser/inlineProgress.js';31import { PreferredDropConfiguration } from './dropIntoEditorContribution.js';32import { sortEditsByYieldTo } from './edit.js';33import { PostEditWidgetManager } from './postEditWidget.js';3435export const dropAsPreferenceConfig = 'editor.dropIntoEditor.preferences';3637export const changeDropTypeCommandId = 'editor.changeDropType';3839export const dropWidgetVisibleCtx = new RawContextKey<boolean>('dropWidgetVisible', false, localize('dropWidgetVisible', "Whether the drop widget is showing"));4041export class DropIntoEditorController extends Disposable implements IEditorContribution {4243public static readonly ID = 'editor.contrib.dropIntoEditorController';4445public static get(editor: ICodeEditor): DropIntoEditorController | null {46return editor.getContribution<DropIntoEditorController>(DropIntoEditorController.ID);47}4849public static setConfigureDefaultAction(action: IAction) {50this._configureDefaultAction = action;51}5253private static _configureDefaultAction?: IAction;5455/**56* Global tracking the current drop operation.57*58* TODO: figure out how to make this work with multiple windows59*/60private static _currentDropOperation?: CancelablePromise<void>;6162private readonly _dropProgressManager: InlineProgressManager;63private readonly _postDropWidgetManager: PostEditWidgetManager<DocumentDropEdit>;6465private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();6667constructor(68editor: ICodeEditor,69@IInstantiationService instantiationService: IInstantiationService,70@IConfigurationService private readonly _configService: IConfigurationService,71@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,72@ITreeViewsDnDService private readonly _treeViewsDragAndDropService: ITreeViewsDnDService73) {74super();7576this._dropProgressManager = this._register(instantiationService.createInstance(InlineProgressManager, 'dropIntoEditor', editor));77this._postDropWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'dropIntoEditor', editor, dropWidgetVisibleCtx,78{ id: changeDropTypeCommandId, label: localize('postDropWidgetTitle', "Show drop options...") },79() => DropIntoEditorController._configureDefaultAction ? [DropIntoEditorController._configureDefaultAction] : []));8081this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)));82}8384public clearWidgets() {85this._postDropWidgetManager.clear();86}8788public changeDropType() {89this._postDropWidgetManager.tryShowSelector();90}9192private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) {93if (!dragEvent.dataTransfer || !editor.hasModel()) {94return;95}9697DropIntoEditorController._currentDropOperation?.cancel();9899editor.focus();100editor.setPosition(position);101102const p = createCancelablePromise(async (token) => {103const disposables = new DisposableStore();104105const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token));106try {107const ourDataTransfer = await this.extractDataTransferData(dragEvent);108if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) {109return;110}111112const model = editor.getModel();113if (!model) {114return;115}116117const providers = this._languageFeaturesService.documentDropEditProvider118.ordered(model)119.filter(provider => {120if (!provider.dropMimeTypes) {121// Keep all providers that don't specify mime types122return true;123}124return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime));125});126127const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource.token));128if (tokenSource.token.isCancellationRequested) {129return;130}131132if (editSession.edits.length) {133const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits);134const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop';135// Pass in the parent token here as it tracks cancelling the entire drop operation136await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token);137}138} finally {139disposables.dispose();140if (DropIntoEditorController._currentDropOperation === p) {141DropIntoEditorController._currentDropOperation = undefined;142}143}144});145146this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() });147DropIntoEditorController._currentDropOperation = p;148}149150private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken) {151const disposables = new DisposableStore();152153const results = await raceCancellation(Promise.all(providers.map(async provider => {154try {155const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, token);156if (edits) {157disposables.add(edits);158}159return edits?.edits.map(edit => ({ ...edit, providerId: provider.id }));160} catch (err) {161if (!isCancellationError(err)) {162console.error(err);163}164console.error(err);165}166return undefined;167})), token);168169const edits = coalesce(results ?? []).flat();170return {171edits: sortEditsByYieldTo(edits),172dispose: () => disposables.dispose()173};174}175176private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray<DocumentDropEdit>): number {177const preferredProviders = this._configService.getValue<PreferredDropConfiguration[]>(dropAsPreferenceConfig, { resource: model.uri });178for (const config of Array.isArray(preferredProviders) ? preferredProviders : []) {179const desiredKind = new HierarchicalKind(config);180const editIndex = edits.findIndex(edit => edit.kind && desiredKind.contains(edit.kind));181if (editIndex >= 0) {182return editIndex;183}184}185return 0;186}187188private async extractDataTransferData(dragEvent: DragEvent): Promise<VSDataTransfer> {189if (!dragEvent.dataTransfer) {190return new VSDataTransfer();191}192193const dataTransfer = toExternalVSDataTransfer(dragEvent.dataTransfer);194195if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {196const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype);197if (Array.isArray(data)) {198for (const id of data) {199const treeDataTransfer = await this._treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier);200if (treeDataTransfer) {201for (const [type, value] of treeDataTransfer) {202dataTransfer.replace(type, value);203}204}205}206}207}208209return dataTransfer;210}211}212213214