diff --git a/.eslintrc.json b/.eslintrc.json index 0a6dc4864c3..4f6bbe81b13 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -164,6 +164,7 @@ "delete", "discover", "dispose", + "drop", "edit", "end", "expand", diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f1583c3e78c..99f788be65c 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -15,6 +15,9 @@ "categories": [ "Programming Languages" ], + "enabledApiProposals": [ + "textEditorDrop" + ], "activationEvents": [ "onLanguage:markdown", "onCommand:markdown.preview.toggleLock", diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 7602cee8ed8..022ab92f11e 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import * as vscode from 'vscode'; +import * as URI from 'vscode-uri'; import { CommandManager } from './commandManager'; import * as commands from './commands/index'; import LinkProvider from './features/documentLinkProvider'; @@ -45,6 +47,43 @@ export function activate(context: vscode.ExtensionContext) { logger.updateConfiguration(); previewManager.updateConfiguration(); })); + + context.subscriptions.push(vscode.workspace.onWillDropOnTextEditor(e => { + e.waitUntil((async () => { + const resourceUrls = await e.dataTransfer.get('resourceurls')?.asString(); + if (!resourceUrls) { + return; + } + + const uris: vscode.Uri[] = []; + for (const resource of JSON.parse(resourceUrls)) { + try { + uris.push(vscode.Uri.parse(resource)); + } catch { + // noop + } + } + + if (!uris.length) { + return; + } + + const snippet = new vscode.SnippetString(); + uris.forEach((uri, i) => { + const rel = path.relative(URI.Utils.dirname(e.editor.document.uri).fsPath, uri.fsPath); + + snippet.appendText('['); + snippet.appendTabstop(); + snippet.appendText(`](${rel})`); + + if (i <= uris.length - 1 && uris.length > 1) { + snippet.appendText(' '); + } + }); + + return e.editor.insertSnippet(snippet, e.position); + })()); + })); } function registerMarkdownLanguageFeatures( diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index fcd79775de5..2a31282bd89 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -5,6 +5,8 @@ }, "include": [ "src/**/*", - "../../src/vscode-dts/vscode.d.ts" + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.textEditorDrop.d.ts", + ] } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 73071670728..f465dc9d495 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1677,3 +1677,62 @@ export const enum ZIndex { ModalDialog = 2600, PaneDropOverlay = 10000 } + + +export interface IDragAndDropObserverCallbacks { + readonly onDragEnter: (e: DragEvent) => void; + readonly onDragLeave: (e: DragEvent) => void; + readonly onDrop: (e: DragEvent) => void; + readonly onDragEnd: (e: DragEvent) => void; + + readonly onDragOver?: (e: DragEvent) => void; +} + +export class DragAndDropObserver extends Disposable { + + // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE + // calls see https://github.com/microsoft/vscode/issues/14470 + // when the element has child elements where the events are fired + // repeadedly. + private counter: number = 0; + + constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => { + this.counter++; + + this.callbacks.onDragEnter(e); + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + + if (this.callbacks.onDragOver) { + this.callbacks.onDragOver(e); + } + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => { + this.counter--; + + if (this.counter === 0) { + this.callbacks.onDragLeave(e); + } + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => { + this.counter = 0; + this.callbacks.onDragEnd(e); + })); + + this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => { + this.counter = 0; + this.callbacks.onDrop(e); + })); + } +} diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index e613d8170d2..8ed06d0f3ac 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -609,6 +609,12 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ readonly onMouseDropCanceled: Event; + /** + * An event emitted when content is dropped into the editor. + * @internal + * @event + */ + readonly onDropIntoEditor: Event<{ readonly position: IPosition; readonly dataTransfer: DataTransfer }>; /** * An event emitted on a "contextmenu". * @event diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index ea17ac01b11..003981141c6 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -110,6 +110,11 @@ class ModelData { export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor { + private static readonly dropIntoEditorDecorationOptions = ModelDecorationOptions.register({ + description: 'workbench-dnd-target', + className: 'dnd-target' + }); + //#region Eventing private readonly _onDidDispose: Emitter = this._register(new Emitter()); public readonly onDidDispose: Event = this._onDidDispose.event; @@ -185,6 +190,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onMouseDropCanceled: Emitter = this._register(new Emitter()); public readonly onMouseDropCanceled: Event = this._onMouseDropCanceled.event; + private readonly _onDropIntoEditor = this._register(new Emitter<{ position: IPosition; dataTransfer: DataTransfer }>()); + public readonly onDropIntoEditor = this._onDropIntoEditor.event; + private readonly _onContextMenu: Emitter = this._register(new Emitter()); public readonly onContextMenu: Event = this._onContextMenu.event; @@ -253,6 +261,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private _bannerDomNode: HTMLElement | null = null; + private _dropIntoEditorDecorationIds: string[] = []; + constructor( domElement: HTMLElement, _options: Readonly, @@ -352,6 +362,34 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._actions[internalAction.id] = internalAction; }); + this._register(new dom.DragAndDropObserver(this._domElement, { + onDragEnter: () => undefined, + onDragOver: e => { + const target = this.getTargetAtClientPoint(e.clientX, e.clientY); + if (target?.position) { + this.showDropIndicatorAt(target.position); + } + }, + onDrop: async e => { + this.removeDropIndicator(); + + if (!e.dataTransfer) { + return; + } + + const target = this.getTargetAtClientPoint(e.clientX, e.clientY); + if (target?.position) { + this._onDropIntoEditor.fire({ position: target.position, dataTransfer: e.dataTransfer }); + } + }, + onDragLeave: () => { + this.removeDropIndicator(); + }, + onDragEnd: () => { + this.removeDropIndicator(); + }, + })); + this._codeEditorService.addCodeEditor(this); } @@ -1763,6 +1801,20 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public hasModel(): this is editorBrowser.IActiveCodeEditor { return (this._modelData !== null); } + + private showDropIndicatorAt(position: Position): void { + let newDecorations: IModelDeltaDecoration[] = [{ + range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), + options: CodeEditorWidget.dropIntoEditorDecorationOptions + }]; + + this._dropIntoEditorDecorationIds = this.deltaDecorations(this._dropIntoEditorDecorationIds, newDecorations); + this.revealPosition(position, editorCommon.ScrollType.Immediate); + } + + private removeDropIndicator(): void { + this._dropIntoEditorDecorationIds = this.deltaDecorations(this._dropIntoEditorDecorationIds, []); + } } const enum BooleanEventValue { diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 0b56ed07df4..f24b5ecb04d 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -420,6 +420,15 @@ export class MainThreadDocumentsAndEditors { return undefined; } + getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined { + for (const [id, editor] of this._textEditors) { + if (editor.getCodeEditor() === codeEditor) { + return id; + } + } + return undefined; + } + getEditor(id: string): MainThreadTextEditor | undefined { return this._textEditors.get(id); } diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 5e10873b485..7606bc7001e 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -29,7 +29,11 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IEditorPane } from 'vs/workbench/common/editor'; +import { IEditorControl } from 'vs/workbench/common/editor'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { @@ -51,7 +55,8 @@ export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): Re export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; - findTextEditorIdFor(editorPane: IEditorPane): string | undefined; + findTextEditorIdFor(editorControl: IEditorControl): string | undefined; + getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined; } export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -64,6 +69,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { private _textEditorsListenersMap: { [editorId: string]: IDisposable[] }; private _editorPositionData: ITextEditorPositionData | null; private _registeredDecorationTypes: { [decorationType: string]: boolean }; + private readonly _dropIntoEditorListeners = new Map(); constructor( private readonly _editorLocator: IMainThreadEditorLocator, @@ -71,7 +77,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IEditorService private readonly _editorService: IEditorService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, ) { this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); @@ -83,6 +89,21 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors())); + const registerDropListenerOnEditor = (editor: ICodeEditor) => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.dataTransfer))); + }; + + this._toDispose.add(_codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); + + this._toDispose.add(_codeEditorService.onCodeEditorRemove(editor => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + })); + + for (const editor of this._codeEditorService.listCodeEditors()) { + registerDropListenerOnEditor(editor); + } + this._registeredDecorationTypes = Object.create(null); } @@ -95,6 +116,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { for (let decorationType in this._registeredDecorationTypes) { this._codeEditorService.removeDecorationType(decorationType); } + dispose(this._dropIntoEditorListeners.values()); + this._dropIntoEditorListeners.clear(); this._registeredDecorationTypes = Object.create(null); } @@ -134,6 +157,31 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } + private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dataTransfer: DataTransfer) { + const id = this._editorLocator.getIdOfCodeEditor(editor); + if (typeof id !== 'string') { + return; + } + + const textEditorDataTransfer: IDataTransfer = new Map(); + + for (const item of dataTransfer.items) { + if (item.kind === 'string') { + const type = item.type; + const asStringValue = new Promise(resolve => item.getAsString(resolve)); + textEditorDataTransfer.set(type, { + asString: () => asStringValue, + value: undefined + }); + } + } + + if (textEditorDataTransfer.size > 0) { + const dataTransferDto = await DataTransferConverter.toDataTransferDTO(textEditorDataTransfer); + return this._proxy.$textEditorHandleDrop(id, position, dataTransferDto); + } + } + // --- from extension host process async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 29c01dfe5d5..ab0a883d089 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -13,8 +13,9 @@ import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { TreeDataTransferConverter } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -174,12 +175,12 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { readonly hasWillDrop: boolean, private readonly _proxy: ExtHostTreeViewsShape) { } - async handleDrop(dataTransfer: ITreeDataTransfer, targetTreeItem: ITreeItem, token: CancellationToken, + async handleDrop(dataTransfer: IDataTransfer, targetTreeItem: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise { - return this._proxy.$handleDrop(this.treeViewId, await TreeDataTransferConverter.toTreeDataTransferDTO(dataTransfer), targetTreeItem.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); + return this._proxy.$handleDrop(this.treeViewId, await DataTransferConverter.toDataTransferDTO(dataTransfer), targetTreeItem.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); } - async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { + async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { if (!this.hasWillDrop) { return; } @@ -187,7 +188,7 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { if (!additionalTransferItems) { return; } - return TreeDataTransferConverter.toITreeDataTransfer(additionalTransferItems); + return DataTransferConverter.toDataTransfer(additionalTransferItems); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0d6587f70a8..da05c098834 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -756,7 +756,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getInlineCompletionItemController(provider: vscode.InlineCompletionItemProvider): vscode.InlineCompletionController { checkProposedApiEnabled(extension, 'inlineCompletions'); return InlineCompletionController.get(provider); - } + }, }; // namespace: workspace @@ -868,6 +868,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, + onWillDropOnTextEditor: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'textEditorDrop'); + return extHostEditors.onWillDropOnTextEditor(listener, thisArgs, disposables); + }, get notebookDocuments(): vscode.NotebookDocument[] { return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 07de952e587..ea2deffb974 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -44,7 +44,7 @@ import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; -import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @@ -1303,6 +1303,7 @@ export interface ISelectionChangeEvent { export interface ExtHostEditorsShape { $acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void; $acceptEditorPositionData(data: ITextEditorPositionData): void; + $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise; } export interface IDocumentsAndEditorsDelta { @@ -1319,8 +1320,8 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise; - $handleDrop(destinationViewId: string, treeDataTransfer: TreeDataTransferDTO, newParentTreeItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; - $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; + $handleDrop(destinationViewId: string, treeDataTransfer: DataTransferDTO, newParentTreeItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; + $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index fac8326994b..db69a6a67bc 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; -import type * as vscode from 'vscode'; +import * as vscode from 'vscode'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; +import { IPosition } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -21,6 +24,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorViewColumn = new Emitter(); private readonly _onDidChangeActiveTextEditor = new Emitter(); private readonly _onDidChangeVisibleTextEditors = new Emitter(); + private readonly _onWillDropOnTextEditor = new AsyncEmitter(); readonly onDidChangeTextEditorSelection: Event = this._onDidChangeTextEditorSelection.event; readonly onDidChangeTextEditorOptions: Event = this._onDidChangeTextEditorOptions.event; @@ -28,6 +32,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeTextEditorViewColumn: Event = this._onDidChangeTextEditorViewColumn.event; readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; + readonly onWillDropOnTextEditor: Event = this._onWillDropOnTextEditor.event; private readonly _proxy: MainThreadTextEditorsShape; @@ -159,4 +164,24 @@ export class ExtHostEditors implements ExtHostEditorsShape { getDiffInformation(id: string): Promise { return Promise.resolve(this._proxy.$getDiffInformation(id)); } + + // --- Text editor drag and drop + + async $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise { + const textEditor = this._extHostDocumentsAndEditors.getEditor(id); + if (!textEditor) { + throw new Error('Unknown text editor'); + } + + const pos = TypeConverters.Position.to(position); + const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto); + + const event = Object.freeze({ + editor: textEditor.value, + position: pos, + dataTransfer: dataTransfer + }); + + await this._onWillDropOnTextEditor.fireAsync(event, CancellationToken.None); + } } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index bd55707aa00..29524c764e9 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; -import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; import { TreeItemCollapsibleState, ThemeIcon, MarkdownString as MarkdownStringType } from 'vs/workbench/api/common/extHostTypes'; @@ -22,8 +22,9 @@ import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/languages'; -import { TreeDataTransferConverter, TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; type TreeItemHandle = string; @@ -134,22 +135,22 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } - async $handleDrop(destinationViewId: string, treeDataTransferDTO: TreeDataTransferDTO, newParentItemHandle: string, token: CancellationToken, + async $handleDrop(destinationViewId: string, treeDataTransferDTO: DataTransferDTO, newParentItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise { const treeView = this.treeViews.get(destinationViewId); if (!treeView) { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', destinationViewId))); } - const treeDataTransfer = TreeDataTransferConverter.toITreeDataTransfer(treeDataTransferDTO); + const treeDataTransfer = DataTransferConverter.toDataTransfer(treeDataTransferDTO); if ((sourceViewId === destinationViewId) && sourceTreeItemHandles) { await this.addAdditionalTransferItems(treeDataTransfer, treeView, sourceTreeItemHandles, token, operationUuid); } return treeView.onDrop(treeDataTransfer, newParentItemHandle, token); } - private async addAdditionalTransferItems(treeDataTransfer: ITreeDataTransfer, treeView: ExtHostTreeView, - sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise { + private async addAdditionalTransferItems(treeDataTransfer: IDataTransfer, treeView: ExtHostTreeView, + sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise { const existingTransferOperation = this.treeDragAndDropService.removeDragOperationTransfer(operationUuid); if (existingTransferOperation) { (await existingTransferOperation)?.forEach((value, key) => { @@ -165,7 +166,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeDataTransfer; } - async $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { + async $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { const treeView = this.treeViews.get(sourceViewId); if (!treeView) { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', sourceViewId))); @@ -176,7 +177,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return; } - return TreeDataTransferConverter.toTreeDataTransferDTO(treeDataTransfer); + return DataTransferConverter.toDataTransferDTO(treeDataTransfer); } async $hasResolve(treeViewId: string): Promise { @@ -431,7 +432,7 @@ class ExtHostTreeView extends Disposable { } } - async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: ITreeDataTransfer, token: CancellationToken): Promise { + async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: IDataTransfer, token: CancellationToken): Promise { const extensionTreeItems: T[] = []; for (const sourceHandle of sourceTreeItemHandles) { const extensionItem = this.getExtensionElement(sourceHandle); diff --git a/src/vs/workbench/api/common/shared/treeDataTransfer.ts b/src/vs/workbench/api/common/shared/dataTransfer.ts similarity index 59% rename from src/vs/workbench/api/common/shared/treeDataTransfer.ts rename to src/vs/workbench/api/common/shared/dataTransfer.ts index 18936dca2f3..d062e9d2d91 100644 --- a/src/vs/workbench/api/common/shared/treeDataTransfer.ts +++ b/src/vs/workbench/api/common/shared/dataTransfer.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITreeDataTransfer, ITreeDataTransferItem } from 'vs/workbench/common/views'; +import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; -interface TreeDataTransferItemDTO { +interface DataTransferItemDTO { asString: string; } -export interface TreeDataTransferDTO { +export interface DataTransferDTO { types: string[]; - items: TreeDataTransferItemDTO[]; + items: DataTransferItemDTO[]; } -export namespace TreeDataTransferConverter { - export function toITreeDataTransfer(value: TreeDataTransferDTO): ITreeDataTransfer { - const newDataTransfer: ITreeDataTransfer = new Map(); +export namespace DataTransferConverter { + export function toDataTransfer(value: DataTransferDTO): IDataTransfer { + const newDataTransfer: IDataTransfer = new Map(); value.types.forEach((type, index) => { newDataTransfer.set(type, { asString: async () => value.items[index].asString, @@ -26,8 +26,8 @@ export namespace TreeDataTransferConverter { return newDataTransfer; } - export async function toTreeDataTransferDTO(value: ITreeDataTransfer): Promise { - const newDTO: TreeDataTransferDTO = { + export async function toDataTransferDTO(value: IDataTransfer): Promise { + const newDTO: DataTransferDTO = { types: [], items: [] }; diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 8af60fcc6fa..a43d0472e25 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -23,7 +23,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IEditorIdentifier, GroupIdentifier, isEditorIdentifier, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { addDisposableListener, DragAndDropObserver, EventType } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -33,7 +33,7 @@ import { parse, stringify } from 'vs/base/common/marshalling'; import { ILabelService } from 'vs/platform/label/common/label'; import { hasWorkspaceFileExtension, isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { ITreeDataTransfer } from 'vs/workbench/common/views'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; import { extractSelection } from 'vs/platform/opener/common/opener'; import { IListDragAndDrop } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; @@ -177,7 +177,7 @@ function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | return editors; } -export async function extractTreeDropData(dataTransfer: ITreeDataTransfer): Promise> { +export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise> { const editors: IDraggedResourceEditorInput[] = []; const resourcesKey = Mimes.uriList.toLowerCase(); @@ -632,64 +632,6 @@ export class LocalSelectionTransfer { } } -export interface IDragAndDropObserverCallbacks { - readonly onDragEnter: (e: DragEvent) => void; - readonly onDragLeave: (e: DragEvent) => void; - readonly onDrop: (e: DragEvent) => void; - readonly onDragEnd: (e: DragEvent) => void; - - readonly onDragOver?: (e: DragEvent) => void; -} - -export class DragAndDropObserver extends Disposable { - - // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE - // calls see https://github.com/microsoft/vscode/issues/14470 - // when the element has child elements where the events are fired - // repeadedly. - private counter: number = 0; - - constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => { - this.counter++; - - this.callbacks.onDragEnter(e); - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => { - e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) - - if (this.callbacks.onDragOver) { - this.callbacks.onDragOver(e); - } - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => { - this.counter--; - - if (this.counter === 0) { - this.callbacks.onDragLeave(e); - } - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => { - this.counter = 0; - this.callbacks.onDragEnd(e); - })); - - this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => { - this.counter = 0; - this.callbacks.onDrop(e); - })); - } -} - export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean { if (!event.dataTransfer) { return false; diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 853d0bcdf56..d05ff6a8ab5 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; -import { addDisposableListener, EventType, EventHelper, isAncestor } from 'vs/base/browser/dom'; +import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; +import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; @@ -21,11 +21,19 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; interface IDropOperation { splitDirection?: GroupDirection; } +function isDragIntoEditorEvent(configurationService: IConfigurationService, e: DragEvent): boolean { + if (!configurationService.getValue('workbench.experimental.editor.dragAndDropIntoEditor.enabled')) { + return false; + } + return e.shiftKey; +} + class DropOverlay extends Themable { private static readonly OVERLAY_ID = 'monaco-workbench-editor-drop-overlay'; @@ -46,7 +54,8 @@ class DropOverlay extends Themable { private accessor: IEditorGroupsAccessor, private groupView: IEditorGroupView, @IThemeService themeService: IThemeService, - @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @ITreeViewsService private readonly treeViewsDragAndDropService: ITreeViewsService, @@ -109,6 +118,11 @@ class DropOverlay extends Themable { this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { + if (isDragIntoEditorEvent(this.configurationService, e)) { + this.dispose(); + return; + } + const isDraggingGroup = this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype); const isDraggingEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype); @@ -528,6 +542,7 @@ export class EditorDropTarget extends Themable { private container: HTMLElement, private readonly delegate: IEditorDropTargetDelegate, @IThemeService themeService: IThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(themeService); @@ -550,6 +565,10 @@ export class EditorDropTarget extends Themable { } private onDragEnter(event: DragEvent): void { + if (isDragIntoEditorEvent(this.configurationService, event)) { + return; + } + this.counter++; // Validate transfer diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index bdae544eff5..5da06c5f378 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -29,11 +29,11 @@ import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; -import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9b412e5c503..1d556b829bd 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, registerAction2, Action2, IMenu } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -63,6 +63,7 @@ import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViews import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; import { Mimes } from 'vs/base/common/mime'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; export class TreeViewPane extends ViewPane { @@ -1378,7 +1379,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { if (!originalEvent.dataTransfer || !dndController || !targetNode) { return; } - const treeDataTransfer: ITreeDataTransfer = new Map(); + const treeDataTransfer: IDataTransfer = new Map(); let stringCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { if (current.kind === 'string') { return previous + 1; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 9d3d4ebd2bf..be75bf31cd1 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener, Dimension, EventType, isAncestor } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension, DragAndDropObserver, EventType, isAncestor } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -31,7 +31,7 @@ import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CompositeMenuActions } from 'vs/workbench/browser/actions'; -import { CompositeDragAndDropObserver, DragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; +import { CompositeDragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { Component } from 'vs/workbench/common/component'; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index cb5672193e4..58af19cd891 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -422,6 +422,12 @@ const registry = Registry.as(ConfigurationExtensions.Con 'tags': ['experimental'], 'default': 'both', 'description': localize('layoutControlType', "Controls whether the layout control in the custom title bar is displayed as a single menu button or with multiple UI toggles."), + }, + 'workbench.experimental.editor.dragAndDropIntoEditor.enabled': { + 'type': 'boolean', + 'tags': ['experimental'], + 'default': false, + 'description': localize('dragAndDropIntoEditor', "Controls whether you can drag and drop a file into an editor by holding down shift (instead of opening the file in an editor)."), } } }); diff --git a/src/vs/workbench/common/dnd.ts b/src/vs/workbench/common/dnd.ts new file mode 100644 index 00000000000..4d92fb48f9c --- /dev/null +++ b/src/vs/workbench/common/dnd.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IDataTransferItem { + asString(): Thenable; + value: any; +} + +export type IDataTransfer = Map; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 1ed3b5ed4bb..20cd1212787 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,6 +27,7 @@ import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -635,13 +636,6 @@ export interface IViewDescriptorService { // Custom views -export interface ITreeDataTransferItem { - asString(): Thenable; - value: any; -} - -export type ITreeDataTransfer = Map; - export interface ITreeView extends IDisposable { dataProvider: ITreeViewDataProvider | undefined; @@ -835,8 +829,8 @@ export interface ITreeViewDataProvider { export interface ITreeViewDragAndDropController { readonly dropMimeTypes: string[]; readonly dragMimeTypes: string[]; - handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; - handleDrop(elements: ITreeDataTransfer, target: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise; + handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; + handleDrop(elements: IDataTransfer, target: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise; } export interface IEditableData { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 88443ea1928..f51545869aa 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -12,7 +12,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { Action } from 'vs/base/common/actions'; -import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; +import { append, $, Dimension, hide, show, DragAndDropObserver } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -49,7 +49,6 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 7055b8d5066..91288a65d40 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -12,7 +12,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -20,6 +20,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isWeb } from 'vs/base/common/platform'; +import { DragAndDropObserver } from 'vs/base/browser/dom'; export class EmptyView extends ViewPane { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 28498c91d72..77b751f78a9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -48,7 +48,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { CodeDataTransfers, containsDragType, DragAndDropObserver, IDragAndDropObserverCallbacks } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; @@ -1120,7 +1120,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.focus(); await this.sendPath(path, false); }); - this._dndObserver = new DragAndDropObserver(container, dndController); + this._dndObserver = new dom.DragAndDropObserver(container, dndController); } hasSelection(): boolean { @@ -2248,7 +2248,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } -class TerminalInstanceDragAndDropController extends Disposable implements IDragAndDropObserverCallbacks { +class TerminalInstanceDragAndDropController extends Disposable implements dom.IDragAndDropObserverCallbacks { private _dropOverlay?: HTMLElement; private readonly _onDropFile = new Emitter(); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 45bfa8fd464..a5c03b15f6e 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -56,6 +56,7 @@ export const allApiProposals = Object.freeze({ testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textDocumentNotebook: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textDocumentNotebook.d.ts', + textEditorDrop: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', diff --git a/src/vs/workbench/services/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts index 7ca5d8fe698..81b80f081ad 100644 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ b/src/vs/workbench/services/views/browser/treeViewsService.ts @@ -5,9 +5,10 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITreeDataTransfer, ITreeItem } from 'vs/workbench/common/views'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; +import { ITreeItem } from 'vs/workbench/common/views'; import { ITreeViewsService as ITreeViewsServiceCommon, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; -export interface ITreeViewsService extends ITreeViewsServiceCommon { } +export interface ITreeViewsService extends ITreeViewsServiceCommon { } export const ITreeViewsService = createDecorator('treeViewsService'); registerSingleton(ITreeViewsService, TreeviewsService); diff --git a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts new file mode 100644 index 00000000000..0af1e3157ba --- /dev/null +++ b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/142990 + + export interface TextEditorDropEvent { + /** + * The {@link TextEditor} the resource was dropped onto. + */ + readonly editor: TextEditor; + + /** + * The position in the file where the drop occurred + */ + readonly position: Position; + + /** + * The {@link DataTransfer data transfer} associated with this drop. + */ + readonly dataTransfer: DataTransfer; + + /** + * Allows to pause the event to delay apply the drop. + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillDropOnTextEditor(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + token: CancellationToken; + } + + export namespace workspace { + /** + * Event fired when the user drops a resource into a text editor. + */ + export const onWillDropOnTextEditor: Event; + } +}