mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-29 13:03:42 +01:00
1011 lines
32 KiB
TypeScript
1011 lines
32 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { localize } from 'vs/nls';
|
|
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
|
import { VSBuffer } from 'vs/base/common/buffer';
|
|
import Severity from 'vs/base/common/severity';
|
|
import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
|
import { basename, isEqual } from 'vs/base/common/resources';
|
|
import { ByteSize, IFileService } from 'vs/platform/files/common/files';
|
|
import { IWindowOpenable } from 'vs/platform/window/common/window';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
|
import { FileAccess, Schemas } from 'vs/base/common/network';
|
|
import { IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor';
|
|
import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd';
|
|
import { DragMouseEvent } from 'vs/base/browser/mouseEvent';
|
|
import { Mimes } from 'vs/base/common/mime';
|
|
import { isWeb, isWindows } from 'vs/base/common/platform';
|
|
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
|
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, 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';
|
|
import { Emitter } from 'vs/base/common/event';
|
|
import { coalesce } from 'vs/base/common/arrays';
|
|
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 { 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';
|
|
import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree';
|
|
import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess';
|
|
import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider';
|
|
import { DeferredPromise } from 'vs/base/common/async';
|
|
import { Registry } from 'vs/platform/registry/common/platform';
|
|
|
|
//#region Editor / Resources DND
|
|
|
|
export class DraggedEditorIdentifier {
|
|
|
|
constructor(readonly identifier: IEditorIdentifier) { }
|
|
}
|
|
|
|
export class DraggedEditorGroupIdentifier {
|
|
|
|
constructor(readonly identifier: GroupIdentifier) { }
|
|
}
|
|
|
|
export class DraggedTreeItemsIdentifier {
|
|
|
|
constructor(readonly identifier: string) { }
|
|
}
|
|
|
|
export const CodeDataTransfers = {
|
|
EDITORS: 'CodeEditors',
|
|
FILES: 'CodeFiles'
|
|
};
|
|
|
|
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
|
|
resource: URI | undefined;
|
|
|
|
/**
|
|
* A hint that the source of the dragged editor input
|
|
* might not be the application but some external tool.
|
|
*/
|
|
isExternal?: boolean;
|
|
|
|
/**
|
|
* Whether we probe for the dropped editor to be a workspace
|
|
* (i.e. code-workspace file or even a folder), allowing to
|
|
* open it as workspace instead of opening as editor.
|
|
*/
|
|
allowWorkspaceOpen?: boolean;
|
|
}
|
|
|
|
export async function extractEditorsDropData(accessor: ServicesAccessor, e: DragEvent): Promise<Array<IDraggedResourceEditorInput>> {
|
|
const editors: IDraggedResourceEditorInput[] = [];
|
|
if (e.dataTransfer && e.dataTransfer.types.length > 0) {
|
|
|
|
// Data Transfer: Code Editors
|
|
const rawEditorsData = e.dataTransfer.getData(CodeDataTransfers.EDITORS);
|
|
if (rawEditorsData) {
|
|
try {
|
|
editors.push(...parse(rawEditorsData));
|
|
} catch (error) {
|
|
// Invalid transfer
|
|
}
|
|
}
|
|
|
|
// Data Transfer: Resources
|
|
else {
|
|
try {
|
|
const rawResourcesData = e.dataTransfer.getData(DataTransfers.RESOURCES);
|
|
editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData));
|
|
} catch (error) {
|
|
// Invalid transfer
|
|
}
|
|
}
|
|
|
|
// Check for native file transfer
|
|
if (e.dataTransfer?.files) {
|
|
for (let i = 0; i < e.dataTransfer.files.length; i++) {
|
|
const file = e.dataTransfer.files[i];
|
|
if (file?.path /* Electron only */) {
|
|
try {
|
|
editors.push({ resource: URI.file(file.path), isExternal: true, allowWorkspaceOpen: true });
|
|
} catch (error) {
|
|
// Invalid URI
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for CodeFiles transfer
|
|
const rawCodeFiles = e.dataTransfer.getData(CodeDataTransfers.FILES);
|
|
if (rawCodeFiles) {
|
|
try {
|
|
const codeFiles: string[] = JSON.parse(rawCodeFiles);
|
|
for (const codeFile of codeFiles) {
|
|
editors.push({ resource: URI.file(codeFile), isExternal: true, allowWorkspaceOpen: true });
|
|
}
|
|
} catch (error) {
|
|
// Invalid transfer
|
|
}
|
|
}
|
|
|
|
// Web: Check for file transfer
|
|
if (isWeb && containsDragType(e, DataTransfers.FILES)) {
|
|
const files = e.dataTransfer.items;
|
|
if (files) {
|
|
const instantiationService = accessor.get(IInstantiationService);
|
|
const filesData = await instantiationService.invokeFunction(accessor => extractFilesDropData(accessor, e));
|
|
for (const fileData of filesData) {
|
|
editors.push({ resource: fileData.resource, contents: fileData.contents?.toString(), isExternal: true, allowWorkspaceOpen: fileData.isDirectory });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Workbench contributions
|
|
const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll();
|
|
for (const contribution of contributions) {
|
|
const data = e.dataTransfer.getData(contribution.dataFormatKey);
|
|
if (data) {
|
|
try {
|
|
editors.push(...contribution.getEditorInputs(data));
|
|
} catch (error) {
|
|
// Invalid transfer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return editors;
|
|
}
|
|
|
|
function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | undefined): IDraggedResourceEditorInput[] {
|
|
const editors: IDraggedResourceEditorInput[] = [];
|
|
|
|
if (rawResourcesData) {
|
|
const resourcesRaw: string[] = JSON.parse(rawResourcesData);
|
|
for (const resourceRaw of resourcesRaw) {
|
|
if (resourceRaw.indexOf(':') > 0) { // mitigate https://github.com/microsoft/vscode/issues/124946
|
|
const { selection, uri } = extractSelection(URI.parse(resourceRaw));
|
|
editors.push({ resource: uri, options: { selection } });
|
|
}
|
|
}
|
|
}
|
|
|
|
return editors;
|
|
}
|
|
|
|
export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise<Array<IDraggedResourceEditorInput>> {
|
|
const editors: IDraggedResourceEditorInput[] = [];
|
|
const resourcesKey = Mimes.uriList.toLowerCase();
|
|
|
|
// Data Transfer: Resources
|
|
if (dataTransfer.has(resourcesKey)) {
|
|
try {
|
|
const asString = await dataTransfer.get(resourcesKey)?.asString();
|
|
const rawResourcesData = JSON.stringify(asString?.split('\\n').filter(value => !value.startsWith('#')));
|
|
editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData));
|
|
} catch (error) {
|
|
// Invalid transfer
|
|
}
|
|
}
|
|
|
|
return editors;
|
|
}
|
|
|
|
export function convertResourceUrlsToUriList(resourceUrls: string): string {
|
|
const asJson: URI[] = JSON.parse(resourceUrls);
|
|
return asJson.map(uri => uri.toString()).join('\n');
|
|
}
|
|
|
|
interface IFileTransferData {
|
|
resource: URI;
|
|
isDirectory?: boolean;
|
|
contents?: VSBuffer;
|
|
}
|
|
|
|
async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent): Promise<IFileTransferData[]> {
|
|
|
|
// Try to extract via `FileSystemHandle`
|
|
if (WebFileSystemAccess.supported(window)) {
|
|
const items = event.dataTransfer?.items;
|
|
if (items) {
|
|
return extractFileTransferData(accessor, items);
|
|
}
|
|
}
|
|
|
|
// Try to extract via `FileList`
|
|
const files = event.dataTransfer?.files;
|
|
if (!files) {
|
|
return [];
|
|
}
|
|
|
|
return extractFileListData(accessor, files);
|
|
}
|
|
|
|
async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise<IFileTransferData[]> {
|
|
const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file);
|
|
if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) {
|
|
return []; // only supported when running in web
|
|
}
|
|
|
|
const results: DeferredPromise<IFileTransferData | undefined>[] = [];
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const file = items[i];
|
|
if (file) {
|
|
const result = new DeferredPromise<IFileTransferData | undefined>();
|
|
results.push(result);
|
|
|
|
(async () => {
|
|
try {
|
|
const handle = await file.getAsFileSystemHandle();
|
|
if (!handle) {
|
|
result.complete(undefined);
|
|
return;
|
|
}
|
|
|
|
if (WebFileSystemAccess.isFileSystemFileHandle(handle)) {
|
|
result.complete({
|
|
resource: await fileSystemProvider.registerFileHandle(handle),
|
|
isDirectory: false
|
|
});
|
|
} else if (WebFileSystemAccess.isFileSystemDirectoryHandle(handle)) {
|
|
result.complete({
|
|
resource: await fileSystemProvider.registerDirectoryHandle(handle),
|
|
isDirectory: true
|
|
});
|
|
} else {
|
|
result.complete(undefined);
|
|
}
|
|
} catch (error) {
|
|
result.complete(undefined);
|
|
}
|
|
})();
|
|
}
|
|
}
|
|
|
|
return coalesce(await Promise.all(results.map(result => result.p)));
|
|
}
|
|
|
|
export async function extractFileListData(accessor: ServicesAccessor, files: FileList): Promise<IFileTransferData[]> {
|
|
const dialogService = accessor.get(IDialogService);
|
|
|
|
const results: DeferredPromise<IFileTransferData | undefined>[] = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files.item(i);
|
|
if (file) {
|
|
|
|
// Skip for very large files because this operation is unbuffered
|
|
if (file.size > 100 * ByteSize.MB) {
|
|
dialogService.show(Severity.Warning, localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again."));
|
|
continue;
|
|
}
|
|
|
|
const result = new DeferredPromise<IFileTransferData | undefined>();
|
|
results.push(result);
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onerror = () => result.complete(undefined);
|
|
reader.onabort = () => result.complete(undefined);
|
|
|
|
reader.onload = async event => {
|
|
const name = file.name;
|
|
const loadResult = withNullAsUndefined(event.target?.result);
|
|
if (typeof name !== 'string' || typeof loadResult === 'undefined') {
|
|
result.complete(undefined);
|
|
return;
|
|
}
|
|
|
|
result.complete({
|
|
resource: URI.from({ scheme: Schemas.untitled, path: name }),
|
|
contents: typeof loadResult === 'string' ? VSBuffer.fromString(loadResult) : VSBuffer.wrap(new Uint8Array(loadResult))
|
|
});
|
|
};
|
|
|
|
// Start reading
|
|
reader.readAsArrayBuffer(file);
|
|
}
|
|
}
|
|
|
|
return coalesce(await Promise.all(results.map(result => result.p)));
|
|
}
|
|
|
|
export interface IResourcesDropHandlerOptions {
|
|
|
|
/**
|
|
* Whether we probe for the dropped resource to be a workspace
|
|
* (i.e. code-workspace file or even a folder), allowing to
|
|
* open it as workspace instead of opening as editor.
|
|
*/
|
|
readonly allowWorkspaceOpen: boolean;
|
|
}
|
|
|
|
/**
|
|
* Shared function across some components to handle drag & drop of resources.
|
|
* E.g. of folders and workspace files to open them in the window instead of
|
|
* the editor or to handle dirty editors being dropped between instances of Code.
|
|
*/
|
|
export class ResourcesDropHandler {
|
|
|
|
constructor(
|
|
private readonly options: IResourcesDropHandlerOptions,
|
|
@IFileService private readonly fileService: IFileService,
|
|
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
|
|
@IEditorService private readonly editorService: IEditorService,
|
|
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
|
|
@IHostService private readonly hostService: IHostService,
|
|
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService
|
|
) {
|
|
}
|
|
|
|
async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise<void> {
|
|
const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsDropData(accessor, event));
|
|
if (!editors.length) {
|
|
return;
|
|
}
|
|
|
|
// Make the window active to handle the drop properly within
|
|
await this.hostService.focus();
|
|
|
|
// Check for workspace file / folder being dropped if we are allowed to do so
|
|
if (this.options.allowWorkspaceOpen) {
|
|
const localFilesAllowedToOpenAsWorkspace = coalesce(editors.filter(editor => editor.allowWorkspaceOpen && editor.resource?.scheme === Schemas.file).map(editor => editor.resource));
|
|
if (localFilesAllowedToOpenAsWorkspace.length > 0) {
|
|
const isWorkspaceOpening = await this.handleWorkspaceDrop(localFilesAllowedToOpenAsWorkspace);
|
|
if (isWorkspaceOpening) {
|
|
return; // return early if the drop operation resulted in this window changing to a workspace
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add external ones to recently open list unless dropped resource is a workspace
|
|
// and only for resources that are outside of the currently opened workspace
|
|
const externalLocalFiles = coalesce(editors.filter(editor => editor.isExternal && editor.resource?.scheme === Schemas.file).map(editor => editor.resource));
|
|
if (externalLocalFiles.length) {
|
|
this.workspacesService.addRecentlyOpened(externalLocalFiles
|
|
.filter(resource => !this.contextService.isInsideWorkspace(resource))
|
|
.map(resource => ({ fileUri: resource }))
|
|
);
|
|
}
|
|
|
|
// Open in Editor
|
|
const targetGroup = resolveTargetGroup();
|
|
await this.editorService.openEditors(editors.map(editor => ({
|
|
...editor,
|
|
resource: editor.resource,
|
|
options: {
|
|
...editor.options,
|
|
pinned: true,
|
|
index: targetIndex
|
|
}
|
|
})), targetGroup, { validateTrust: true });
|
|
|
|
// Finish with provided function
|
|
afterDrop(targetGroup);
|
|
}
|
|
|
|
private async handleWorkspaceDrop(resources: URI[]): Promise<boolean> {
|
|
const toOpen: IWindowOpenable[] = [];
|
|
const folderURIs: IWorkspaceFolderCreationData[] = [];
|
|
|
|
await Promise.all(resources.map(async resource => {
|
|
|
|
// Check for Workspace
|
|
if (hasWorkspaceFileExtension(resource)) {
|
|
toOpen.push({ workspaceUri: resource });
|
|
|
|
return;
|
|
}
|
|
|
|
// Check for Folder
|
|
try {
|
|
const stat = await this.fileService.stat(resource);
|
|
if (stat.isDirectory) {
|
|
toOpen.push({ folderUri: stat.resource });
|
|
folderURIs.push({ uri: stat.resource });
|
|
}
|
|
} catch (error) {
|
|
// Ignore error
|
|
}
|
|
}));
|
|
|
|
// Return early if no external resource is a folder or workspace
|
|
if (toOpen.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Pass focus to window
|
|
this.hostService.focus();
|
|
|
|
// Open in separate windows if we drop workspaces or just one folder
|
|
if (toOpen.length > folderURIs.length || folderURIs.length === 1) {
|
|
await this.hostService.openWindow(toOpen);
|
|
}
|
|
|
|
// Add to workspace if we are in a temporary workspace
|
|
else if (isTemporaryWorkspace(this.contextService.getWorkspace())) {
|
|
await this.workspaceEditingService.addFolders(folderURIs);
|
|
}
|
|
|
|
// Finaly, enter untitled workspace when dropping >1 folders
|
|
else {
|
|
await this.workspaceEditingService.createAndEnterWorkspace(folderURIs);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
interface IResourceStat {
|
|
resource: URI;
|
|
isDirectory?: boolean;
|
|
}
|
|
|
|
export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent): void;
|
|
export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent): void;
|
|
export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent): void;
|
|
export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEditors: Array<URI | IResourceStat | IEditorIdentifier>, event: DragMouseEvent | DragEvent): void {
|
|
if (resourcesOrEditors.length === 0 || !event.dataTransfer) {
|
|
return;
|
|
}
|
|
|
|
const textFileService = accessor.get(ITextFileService);
|
|
const editorService = accessor.get(IEditorService);
|
|
const fileService = accessor.get(IFileService);
|
|
const labelService = accessor.get(ILabelService);
|
|
|
|
// Extract resources from URIs or Editors that
|
|
// can be handled by the file service
|
|
const resources = coalesce(resourcesOrEditors.map(resourceOrEditor => {
|
|
if (URI.isUri(resourceOrEditor)) {
|
|
return { resource: resourceOrEditor };
|
|
}
|
|
|
|
if (isEditorIdentifier(resourceOrEditor)) {
|
|
if (URI.isUri(resourceOrEditor.editor.resource)) {
|
|
return { resource: resourceOrEditor.editor.resource };
|
|
}
|
|
|
|
return undefined; // editor without resource
|
|
}
|
|
|
|
return resourceOrEditor;
|
|
}));
|
|
const fileSystemResources = resources.filter(({ resource }) => fileService.hasProvider(resource));
|
|
|
|
// Text: allows to paste into text-capable areas
|
|
const lineDelimiter = isWindows ? '\r\n' : '\n';
|
|
event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter));
|
|
|
|
// Download URL: enables support to drag a tab as file to desktop
|
|
// Requirements:
|
|
// - Chrome/Edge only
|
|
// - only a single file is supported
|
|
// - only file:/ resources are supported
|
|
const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory);
|
|
if (firstFile) {
|
|
const firstFileUri = FileAccess.asFileUri(firstFile.resource); // enforce `file:` URIs
|
|
if (firstFileUri.scheme === Schemas.file) {
|
|
event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [Mimes.binary, basename(firstFile.resource), firstFileUri.toString()].join(':'));
|
|
}
|
|
}
|
|
|
|
// Resource URLs: allows to drop multiple file resources to a target in VS Code
|
|
const files = fileSystemResources.filter(({ isDirectory }) => !isDirectory);
|
|
if (files.length) {
|
|
event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(({ resource }) => resource.toString())));
|
|
}
|
|
|
|
// Contributions
|
|
const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll();
|
|
for (const contribution of contributions) {
|
|
contribution.setData(resources, event);
|
|
}
|
|
|
|
// Editors: enables cross window DND of editors
|
|
// into the editor area while presering UI state
|
|
const draggedEditors: IDraggedResourceEditorInput[] = [];
|
|
|
|
for (const resourceOrEditor of resourcesOrEditors) {
|
|
|
|
// Extract resource editor from provided object or URI
|
|
let editor: IDraggedResourceEditorInput | undefined = undefined;
|
|
if (isEditorIdentifier(resourceOrEditor)) {
|
|
const untypedEditor = resourceOrEditor.editor.toUntyped({ preserveViewState: resourceOrEditor.groupId });
|
|
if (untypedEditor) {
|
|
editor = { ...untypedEditor, resource: EditorResourceAccessor.getCanonicalUri(untypedEditor) };
|
|
}
|
|
} else if (URI.isUri(resourceOrEditor)) {
|
|
const { selection, uri } = extractSelection(resourceOrEditor);
|
|
editor = { resource: uri, options: selection ? { selection } : undefined };
|
|
} else if (!resourceOrEditor.isDirectory) {
|
|
editor = { resource: resourceOrEditor.resource };
|
|
}
|
|
|
|
if (!editor) {
|
|
continue; // skip over editors that cannot be transferred via dnd
|
|
}
|
|
|
|
// Fill in some properties if they are not there already by accessing
|
|
// some well known things from the text file universe.
|
|
// This is not ideal for custom editors, but those have a chance to
|
|
// provide everything from the `toUntyped` method.
|
|
{
|
|
const resource = editor.resource;
|
|
if (resource) {
|
|
const textFileModel = textFileService.files.get(resource);
|
|
if (textFileModel) {
|
|
|
|
// language
|
|
if (typeof editor.languageId !== 'string') {
|
|
editor.languageId = textFileModel.getLanguageId();
|
|
}
|
|
|
|
// encoding
|
|
if (typeof editor.encoding !== 'string') {
|
|
editor.encoding = textFileModel.getEncoding();
|
|
}
|
|
|
|
// contents (only if dirty)
|
|
if (typeof editor.contents !== 'string' && textFileModel.isDirty()) {
|
|
editor.contents = textFileModel.textEditorModel.getValue();
|
|
}
|
|
}
|
|
|
|
// viewState
|
|
if (!editor.options?.viewState) {
|
|
editor.options = {
|
|
...editor.options,
|
|
viewState: (() => {
|
|
for (const visibleEditorPane of editorService.visibleEditorPanes) {
|
|
if (isEqual(visibleEditorPane.input.resource, resource)) {
|
|
const viewState = visibleEditorPane.getViewState();
|
|
if (viewState) {
|
|
return viewState;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
})()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add as dragged editor
|
|
draggedEditors.push(editor);
|
|
}
|
|
|
|
if (draggedEditors.length) {
|
|
event.dataTransfer.setData(CodeDataTransfers.EDITORS, stringify(draggedEditors));
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region DND contributions
|
|
|
|
export interface IDragAndDropContributionRegistry {
|
|
/**
|
|
* Registers a drag and drop contribution.
|
|
*/
|
|
register(contribution: IDragAndDropContribution): void;
|
|
|
|
/**
|
|
* Returns all registered drag and drop contributions.
|
|
*/
|
|
getAll(): IterableIterator<IDragAndDropContribution>;
|
|
}
|
|
|
|
export interface IDragAndDropContribution {
|
|
readonly dataFormatKey: string;
|
|
getEditorInputs(data: string): IDraggedResourceEditorInput[];
|
|
setData(resources: IResourceStat[], event: DragMouseEvent | DragEvent): void;
|
|
}
|
|
|
|
class DragAndDropContributionRegistry implements IDragAndDropContributionRegistry {
|
|
private readonly _contributions = new Map<string, IDragAndDropContribution>();
|
|
|
|
register(contribution: IDragAndDropContribution): void {
|
|
if (this._contributions.has(contribution.dataFormatKey)) {
|
|
throw new Error(`A drag and drop contributiont with key '${contribution.dataFormatKey}' was already registered.`);
|
|
}
|
|
this._contributions.set(contribution.dataFormatKey, contribution);
|
|
}
|
|
|
|
getAll(): IterableIterator<IDragAndDropContribution> {
|
|
return this._contributions.values();
|
|
}
|
|
}
|
|
|
|
export const Extensions = {
|
|
DragAndDropContribution: 'workbench.contributions.dragAndDrop'
|
|
};
|
|
|
|
Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry());
|
|
|
|
//#endregion
|
|
|
|
//#region DND Utilities
|
|
|
|
/**
|
|
* A singleton to store transfer data during drag & drop operations that are only valid within the application.
|
|
*/
|
|
export class LocalSelectionTransfer<T> {
|
|
|
|
private static readonly INSTANCE = new LocalSelectionTransfer();
|
|
|
|
private data?: T[];
|
|
private proto?: T;
|
|
|
|
private constructor() {
|
|
// protect against external instantiation
|
|
}
|
|
|
|
static getInstance<T>(): LocalSelectionTransfer<T> {
|
|
return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T>;
|
|
}
|
|
|
|
hasData(proto: T): boolean {
|
|
return proto && proto === this.proto;
|
|
}
|
|
|
|
clearData(proto: T): void {
|
|
if (this.hasData(proto)) {
|
|
this.proto = undefined;
|
|
this.data = undefined;
|
|
}
|
|
}
|
|
|
|
getData(proto: T): T[] | undefined {
|
|
if (this.hasData(proto)) {
|
|
return this.data;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
setData(data: T[], proto: T): void {
|
|
if (proto) {
|
|
this.data = data;
|
|
this.proto = proto;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean {
|
|
if (!event.dataTransfer) {
|
|
return false;
|
|
}
|
|
|
|
const dragTypes = event.dataTransfer.types;
|
|
const lowercaseDragTypes: string[] = [];
|
|
for (let i = 0; i < dragTypes.length; i++) {
|
|
lowercaseDragTypes.push(dragTypes[i].toLowerCase()); // somehow the types are lowercase
|
|
}
|
|
|
|
for (const dragType of dragTypesToFind) {
|
|
if (lowercaseDragTypes.indexOf(dragType.toLowerCase()) >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Composites DND
|
|
|
|
export type Before2D = {
|
|
readonly verticallyBefore: boolean;
|
|
readonly horizontallyBefore: boolean;
|
|
};
|
|
|
|
export interface ICompositeDragAndDrop {
|
|
drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent, before?: Before2D): void;
|
|
onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean;
|
|
onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean;
|
|
}
|
|
|
|
export interface ICompositeDragAndDropObserverCallbacks {
|
|
onDragEnter?: (e: IDraggedCompositeData) => void;
|
|
onDragLeave?: (e: IDraggedCompositeData) => void;
|
|
onDrop?: (e: IDraggedCompositeData) => void;
|
|
onDragOver?: (e: IDraggedCompositeData) => void;
|
|
onDragStart?: (e: IDraggedCompositeData) => void;
|
|
onDragEnd?: (e: IDraggedCompositeData) => void;
|
|
}
|
|
|
|
export class CompositeDragAndDropData implements IDragAndDropData {
|
|
|
|
constructor(private type: 'view' | 'composite', private id: string) { }
|
|
|
|
update(dataTransfer: DataTransfer): void {
|
|
// no-op
|
|
}
|
|
|
|
getData(): {
|
|
type: 'view' | 'composite';
|
|
id: string;
|
|
} {
|
|
return { type: this.type, id: this.id };
|
|
}
|
|
}
|
|
|
|
export interface IDraggedCompositeData {
|
|
readonly eventData: DragEvent;
|
|
readonly dragAndDropData: CompositeDragAndDropData;
|
|
}
|
|
|
|
export class DraggedCompositeIdentifier {
|
|
|
|
constructor(private compositeId: string) { }
|
|
|
|
get id(): string {
|
|
return this.compositeId;
|
|
}
|
|
}
|
|
|
|
export class DraggedViewIdentifier {
|
|
|
|
constructor(private viewId: string) { }
|
|
|
|
get id(): string {
|
|
return this.viewId;
|
|
}
|
|
}
|
|
|
|
export type ViewType = 'composite' | 'view';
|
|
|
|
export class CompositeDragAndDropObserver extends Disposable {
|
|
|
|
private static instance: CompositeDragAndDropObserver | undefined;
|
|
|
|
static get INSTANCE(): CompositeDragAndDropObserver {
|
|
if (!CompositeDragAndDropObserver.instance) {
|
|
CompositeDragAndDropObserver.instance = new CompositeDragAndDropObserver();
|
|
}
|
|
|
|
return CompositeDragAndDropObserver.instance;
|
|
}
|
|
|
|
private readonly transferData = LocalSelectionTransfer.getInstance<DraggedCompositeIdentifier | DraggedViewIdentifier>();
|
|
|
|
private readonly onDragStart = this._register(new Emitter<IDraggedCompositeData>());
|
|
private readonly onDragEnd = this._register(new Emitter<IDraggedCompositeData>());
|
|
|
|
private constructor() {
|
|
super();
|
|
|
|
this._register(this.onDragEnd.event(e => {
|
|
const id = e.dragAndDropData.getData().id;
|
|
const type = e.dragAndDropData.getData().type;
|
|
const data = this.readDragData(type);
|
|
if (data?.getData().id === id) {
|
|
this.transferData.clearData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
|
|
}
|
|
}));
|
|
}
|
|
|
|
private readDragData(type: ViewType): CompositeDragAndDropData | undefined {
|
|
if (this.transferData.hasData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype)) {
|
|
const data = this.transferData.getData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
|
|
if (data && data[0]) {
|
|
return new CompositeDragAndDropData(type, data[0].id);
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
private writeDragData(id: string, type: ViewType): void {
|
|
this.transferData.setData([type === 'view' ? new DraggedViewIdentifier(id) : new DraggedCompositeIdentifier(id)], type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
|
|
}
|
|
|
|
registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable {
|
|
const disposableStore = new DisposableStore();
|
|
disposableStore.add(new DragAndDropObserver(element, {
|
|
onDragEnd: e => {
|
|
// no-op
|
|
},
|
|
onDragEnter: e => {
|
|
e.preventDefault();
|
|
|
|
if (callbacks.onDragEnter) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (data) {
|
|
callbacks.onDragEnter({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
}
|
|
},
|
|
onDragLeave: e => {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (callbacks.onDragLeave && data) {
|
|
callbacks.onDragLeave({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
},
|
|
onDrop: e => {
|
|
if (callbacks.onDrop) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
callbacks.onDrop({ eventData: e, dragAndDropData: data! });
|
|
|
|
// Fire drag event in case drop handler destroys the dragged element
|
|
this.onDragEnd.fire({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
},
|
|
onDragOver: e => {
|
|
e.preventDefault();
|
|
|
|
if (callbacks.onDragOver) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
callbacks.onDragOver({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
}
|
|
}));
|
|
|
|
if (callbacks.onDragStart) {
|
|
this.onDragStart.event(e => {
|
|
callbacks.onDragStart!(e);
|
|
}, this, disposableStore);
|
|
}
|
|
|
|
if (callbacks.onDragEnd) {
|
|
this.onDragEnd.event(e => {
|
|
callbacks.onDragEnd!(e);
|
|
});
|
|
}
|
|
|
|
return this._register(disposableStore);
|
|
}
|
|
|
|
registerDraggable(element: HTMLElement, draggedItemProvider: () => { type: ViewType; id: string }, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable {
|
|
element.draggable = true;
|
|
|
|
const disposableStore = new DisposableStore();
|
|
|
|
disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => {
|
|
const { id, type } = draggedItemProvider();
|
|
this.writeDragData(id, type);
|
|
|
|
e.dataTransfer?.setDragImage(element, 0, 0);
|
|
|
|
this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! });
|
|
}));
|
|
|
|
disposableStore.add(new DragAndDropObserver(element, {
|
|
onDragEnd: e => {
|
|
const { type } = draggedItemProvider();
|
|
const data = this.readDragData(type);
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
this.onDragEnd.fire({ eventData: e, dragAndDropData: data! });
|
|
},
|
|
onDragEnter: e => {
|
|
if (callbacks.onDragEnter) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
if (data) {
|
|
callbacks.onDragEnter({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
}
|
|
},
|
|
onDragLeave: e => {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
if (callbacks.onDragLeave) {
|
|
callbacks.onDragLeave({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
},
|
|
onDrop: e => {
|
|
if (callbacks.onDrop) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
callbacks.onDrop({ eventData: e, dragAndDropData: data! });
|
|
|
|
// Fire drag event in case drop handler destroys the dragged element
|
|
this.onDragEnd.fire({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
},
|
|
onDragOver: e => {
|
|
if (callbacks.onDragOver) {
|
|
const data = this.readDragData('composite') || this.readDragData('view');
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
callbacks.onDragOver({ eventData: e, dragAndDropData: data! });
|
|
}
|
|
}
|
|
}));
|
|
|
|
if (callbacks.onDragStart) {
|
|
this.onDragStart.event(e => {
|
|
callbacks.onDragStart!(e);
|
|
}, this, disposableStore);
|
|
}
|
|
|
|
if (callbacks.onDragEnd) {
|
|
this.onDragEnd.event(e => {
|
|
callbacks.onDragEnd!(e);
|
|
}, this, disposableStore);
|
|
}
|
|
|
|
return this._register(disposableStore);
|
|
}
|
|
}
|
|
|
|
export function toggleDropEffect(dataTransfer: DataTransfer | null, dropEffect: 'none' | 'copy' | 'link' | 'move', shouldHaveIt: boolean) {
|
|
if (!dataTransfer) {
|
|
return;
|
|
}
|
|
|
|
dataTransfer.dropEffect = shouldHaveIt ? dropEffect : 'none';
|
|
}
|
|
|
|
export class ResourceListDnDHandler<T> implements IListDragAndDrop<T> {
|
|
constructor(
|
|
private readonly toResource: (e: T) => URI | null,
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService
|
|
) { }
|
|
|
|
getDragURI(element: T): string | null {
|
|
const resource = this.toResource(element);
|
|
return resource ? resource.toString() : null;
|
|
}
|
|
|
|
getDragLabel(elements: T[]): string | undefined {
|
|
const resources = coalesce(elements.map(this.toResource));
|
|
return resources.length === 1 ? basename(resources[0]) : resources.length > 1 ? String(resources.length) : undefined;
|
|
}
|
|
|
|
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
|
|
const resources: URI[] = [];
|
|
for (const element of (data as ElementsDragAndDropData<T>).elements) {
|
|
const resource = this.toResource(element);
|
|
if (resource) {
|
|
resources.push(resource);
|
|
}
|
|
}
|
|
if (resources.length) {
|
|
// Apply some datatransfer types to allow for dragging the element outside of the application
|
|
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
|
|
}
|
|
}
|
|
|
|
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
|
|
return false;
|
|
}
|
|
|
|
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void { }
|
|
}
|
|
|
|
//#endregion
|