@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
268 lines (267 loc) • 9.32 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DataTransfers } from '@sussudio/base/browser/dnd.mjs';
import { coalesce } from '@sussudio/base/common/arrays.mjs';
import { DeferredPromise } from '@sussudio/base/common/async.mjs';
import { VSBuffer } from '@sussudio/base/common/buffer.mjs';
import { ResourceMap } from '@sussudio/base/common/map.mjs';
import { parse } from '@sussudio/base/common/marshalling.mjs';
import { Schemas } from '@sussudio/base/common/network.mjs';
import { isWeb } from '@sussudio/base/common/platform.mjs';
import Severity from '@sussudio/base/common/severity.mjs';
import { withNullAsUndefined } from '@sussudio/base/common/types.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { localize } from 'vscode-nls.mjs';
import { IDialogService } from '../../dialogs/common/dialogs.mjs';
import { HTMLFileSystemProvider } from '../../files/browser/htmlFileSystemProvider.mjs';
import { WebFileSystemAccess } from '../../files/browser/webFileSystemAccess.mjs';
import { ByteSize, IFileService } from '../../files/common/files.mjs';
import { IInstantiationService } from '../../instantiation/common/instantiation.mjs';
import { extractSelection } from '../../opener/common/opener.mjs';
import { Registry } from '../../registry/common/platform.mjs';
//#region Editor / Resources DND
export const CodeDataTransfers = {
EDITORS: 'CodeEditors',
FILES: 'CodeFiles',
};
export function extractEditorsDropData(e) {
const editors = [];
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 && 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 = JSON.parse(rawCodeFiles);
for (const codeFile of codeFiles) {
editors.push({ resource: URI.file(codeFile), isExternal: true, allowWorkspaceOpen: true });
}
} catch (error) {
// Invalid transfer
}
}
// Workbench contributions
const contributions = Registry.as(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
}
}
}
}
// Prevent duplicates: it is possible that we end up with the same
// dragged editor multiple times because multiple data transfers
// are being used (https://github.com/microsoft/vscode/issues/128925)
const coalescedEditors = [];
const seen = new ResourceMap();
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, e) {
const editors = extractEditorsDropData(e);
// Web: Check for file transfer
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) {
const editors = [];
if (rawResourcesData) {
const resourcesRaw = 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;
}
async function extractFilesDropData(accessor, event) {
// 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, items) {
const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file);
if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) {
return []; // only supported when running in web
}
const results = [];
for (let i = 0; i < items.length; i++) {
const file = items[i];
if (file) {
const result = new DeferredPromise();
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, files) {
const dialogService = accessor.get(IDialogService);
const results = [];
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();
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)));
}
//#endregion
export function containsDragType(event, ...dragTypesToFind) {
if (!event.dataTransfer) {
return false;
}
const dragTypes = event.dataTransfer.types;
const lowercaseDragTypes = [];
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;
}
class DragAndDropContributionRegistry {
_contributions = new Map();
register(contribution) {
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() {
return this._contributions.values();
}
}
export const Extensions = {
DragAndDropContribution: 'workbench.contributions.dragAndDrop',
};
Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry());
//#endregion