import { DataTransfers } from '../../../base/browser/dnd.js';
import { mainWindow } from '../../../base/browser/window.js';
import { DragMouseEvent } from '../../../base/browser/mouseEvent.js';
import { coalesce } from '../../../base/common/arrays.js';
import { DeferredPromise } from '../../../base/common/async.js';
import { VSBuffer } from '../../../base/common/buffer.js';
import { ResourceMap } from '../../../base/common/map.js';
import { parse } from '../../../base/common/marshalling.js';
import { Schemas } from '../../../base/common/network.js';
import { isNative, isWeb } from '../../../base/common/platform.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { IDialogService } from '../../dialogs/common/dialogs.js';
import { IBaseTextResourceEditorInput, ITextEditorSelection } from '../../editor/common/editor.js';
import { HTMLFileSystemProvider } from '../../files/browser/htmlFileSystemProvider.js';
import { WebFileSystemAccess } from '../../files/browser/webFileSystemAccess.js';
import { ByteSize, IFileService } from '../../files/common/files.js';
import { IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';
import { extractSelection } from '../../opener/common/opener.js';
import { Registry } from '../../registry/common/platform.js';
import { IMarker } from '../../markers/common/markers.js';
export const CodeDataTransfers = {
EDITORS: 'CodeEditors',
FILES: 'CodeFiles',
SYMBOLS: 'application/vnd.code.symbols',
MARKERS: 'application/vnd.code.diagnostics',
NOTEBOOK_CELL_OUTPUT: 'notebook-cell-output',
SCM_HISTORY_ITEM: 'scm-history-item',
};
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
resource: URI | undefined;
isExternal?: boolean;
allowWorkspaceOpen?: boolean;
}
export function extractEditorsDropData(e: DragEvent): Array<IDraggedResourceEditorInput> {
const editors: IDraggedResourceEditorInput[] = [];
if (e.dataTransfer && e.dataTransfer.types.length > 0) {
const rawEditorsData = e.dataTransfer.getData(CodeDataTransfers.EDITORS);
if (rawEditorsData) {
try {
editors.push(...parse(rawEditorsData));
} catch (error) {
}
}
else {
try {
const rawResourcesData = e.dataTransfer.getData(DataTransfers.RESOURCES);
editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData));
} catch (error) {
}
}
if (e.dataTransfer?.files) {
for (let i = 0; i < e.dataTransfer.files.length; i++) {
const file = e.dataTransfer.files[i];
if (file && getPathForFile(file)) {
try {
editors.push({ resource: URI.file(getPathForFile(file)!), isExternal: true, allowWorkspaceOpen: true });
} catch (error) {
}
}
}
}
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) {
}
}
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) {
}
}
}
}
const coalescedEditors: IDraggedResourceEditorInput[] = [];
const seen = new ResourceMap<boolean>();
for (const editor of editors) {
if (!editor.resource) {
coalescedEditors.push(editor);
} else if (!seen.has(editor.resource)) {
coalescedEditors.push(editor);
seen.set(editor.resource, true);
}
}
return coalescedEditors;
}
export async function extractEditorsAndFilesDropData(accessor: ServicesAccessor, e: DragEvent): Promise<Array<IDraggedResourceEditorInput>> {
const editors = extractEditorsDropData(e);
if (e.dataTransfer && 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 });
}
}
}
return editors;
}
export 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) {
const { selection, uri } = extractSelection(URI.parse(resourceRaw));
editors.push({ resource: uri, options: { selection } });
}
}
}
return editors;
}
interface IFileTransferData {
resource: URI;
isDirectory?: boolean;
contents?: VSBuffer;
}
async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent): Promise<IFileTransferData[]> {
if (WebFileSystemAccess.supported(mainWindow)) {
const items = event.dataTransfer?.items;
if (items) {
return extractFileTransferData(accessor, items);
}
}
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 [];
}
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) {
if (file.size > 100 * ByteSize.MB) {
dialogService.warn(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 = event.target?.result ?? undefined;
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))
});
};
reader.readAsArrayBuffer(file);
}
}
return coalesce(await Promise.all(results.map(result => result.p)));
}
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());
}
for (const dragType of dragTypesToFind) {
if (lowercaseDragTypes.indexOf(dragType.toLowerCase()) >= 0) {
return true;
}
}
return false;
}
export interface IResourceStat {
readonly resource: URI;
readonly isDirectory?: boolean;
readonly selection?: ITextEditorSelection;
}
export interface IDragAndDropContributionRegistry {
register(contribution: IDragAndDropContribution): void;
getAll(): IterableIterator<IDragAndDropContribution>;
}
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());
export class LocalSelectionTransfer<T> {
private static readonly INSTANCE = new LocalSelectionTransfer();
private data?: T[];
private proto?: T;
private constructor() {
}
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 interface DocumentSymbolTransferData {
name: string;
fsPath: string;
range: {
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
};
kind: number;
}
export interface NotebookCellOutputTransferData {
outputId: string;
}
function setDataAsJSON(e: DragEvent, kind: string, data: unknown) {
e.dataTransfer?.setData(kind, JSON.stringify(data));
}
function getDataAsJSON<T>(e: DragEvent, kind: string, defaultValue: T): T {
const rawSymbolsData = e.dataTransfer?.getData(kind);
if (rawSymbolsData) {
try {
return JSON.parse(rawSymbolsData);
} catch (error) {
}
}
return defaultValue;
}
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
return getDataAsJSON(e, CodeDataTransfers.SYMBOLS, []);
}
export function fillInSymbolsDragData(symbolsData: readonly DocumentSymbolTransferData[], e: DragEvent): void {
setDataAsJSON(e, CodeDataTransfers.SYMBOLS, symbolsData);
}
export type MarkerTransferData = IMarker | { uri: UriComponents };
export function extractMarkerDropData(e: DragEvent): MarkerTransferData[] | undefined {
return getDataAsJSON(e, CodeDataTransfers.MARKERS, undefined);
}
export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragEvent): void {
setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData);
}
export function extractNotebookCellOutputDropData(e: DragEvent): NotebookCellOutputTransferData | undefined {
return getDataAsJSON(e, CodeDataTransfers.NOTEBOOK_CELL_OUTPUT, undefined);
}
export function getPathForFile(file: File): string | undefined {
if (isNative && typeof (globalThis as any).vscode?.webUtils?.getPathForFile === 'function') {
return (globalThis as any).vscode.webUtils.getPathForFile(file);
}
return undefined;
}