Path: blob/main/src/vs/editor/browser/controller/editContext/clipboardUtils.ts
5245 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { IViewModel } from '../../../common/viewModel.js';5import { Range } from '../../../common/core/range.js';6import { isWindows } from '../../../../base/common/platform.js';7import { Mimes } from '../../../../base/common/mime.js';8import { ViewContext } from '../../../common/viewModel/viewContext.js';9import { ILogService } from '../../../../platform/log/common/log.js';10import { EditorOption } from '../../../common/config/editorOptions.js';11import { generateUuid } from '../../../../base/common/uuid.js';12import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';13import { toExternalVSDataTransfer } from '../../dataTransfer.js';1415export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } {16const { dataToCopy, metadata } = generateDataToCopy(viewModel);17storeMetadataInMemory(dataToCopy.text, metadata, isFirefox);18return { dataToCopy, metadata };19}2021function storeMetadataInMemory(textToCopy: string, metadata: ClipboardStoredMetadata, isFirefox: boolean): void {22InMemoryClipboardMetadataManager.INSTANCE.set(23// When writing "LINE\r\n" to the clipboard and then pasting,24// Firefox pastes "LINE\n", so let's work around this quirk25(isFirefox ? textToCopy.replace(/\r\n/g, '\n') : textToCopy),26metadata27);28}2930function generateDataToCopy(viewModel: IViewModel): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } {31const emptySelectionClipboard = viewModel.getEditorOption(EditorOption.emptySelectionClipboard);32const copyWithSyntaxHighlighting = viewModel.getEditorOption(EditorOption.copyWithSyntaxHighlighting);33const selections = viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection);34const dataToCopy = getDataToCopy(viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting);35const metadata: ClipboardStoredMetadata = {36version: 1,37id: generateUuid(),38isFromEmptySelection: dataToCopy.isFromEmptySelection,39multicursorText: dataToCopy.multicursorText,40mode: dataToCopy.mode41};42return { dataToCopy, metadata };43}4445function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy {46const { sourceRanges, sourceText } = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows);47const newLineCharacter = viewModel.model.getEOL();4849const isFromEmptySelection = (emptySelectionClipboard && modelSelections.length === 1 && modelSelections[0].isEmpty());50const multicursorText = (Array.isArray(sourceText) ? sourceText : null);51const text = (Array.isArray(sourceText) ? sourceText.join(newLineCharacter) : sourceText);5253let html: string | null | undefined = undefined;54let mode: string | null = null;55if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && sourceText.length < 65536)) {56const richText = viewModel.getRichTextToCopy(modelSelections, emptySelectionClipboard);57if (richText) {58html = richText.html;59mode = richText.mode;60}61}62const dataToCopy: ClipboardDataToCopy = {63isFromEmptySelection,64sourceRanges,65multicursorText,66text,67html,68mode69};70return dataToCopy;71}7273/**74* Every time we write to the clipboard, we record a bit of extra metadata here.75* Every time we read from the cipboard, if the text matches our last written text,76* we can fetch the previous metadata.77*/78export class InMemoryClipboardMetadataManager {79public static readonly INSTANCE = new InMemoryClipboardMetadataManager();8081private _lastState: InMemoryClipboardMetadata | null;8283constructor() {84this._lastState = null;85}8687public set(lastCopiedValue: string, data: ClipboardStoredMetadata): void {88this._lastState = { lastCopiedValue, data };89}9091public get(pastedText: string): ClipboardStoredMetadata | null {92if (this._lastState && this._lastState.lastCopiedValue === pastedText) {93// match!94return this._lastState.data;95}96this._lastState = null;97return null;98}99}100101export interface ClipboardDataToCopy {102isFromEmptySelection: boolean;103sourceRanges: Range[];104multicursorText: string[] | null | undefined;105text: string;106html: string | null | undefined;107mode: string | null;108}109110export interface ClipboardStoredMetadata {111version: 1;112id: string | undefined;113isFromEmptySelection: boolean | undefined;114multicursorText: string[] | null | undefined;115mode: string | null;116}117118export const CopyOptions = {119forceCopyWithSyntaxHighlighting: false,120electronBugWorkaroundCopyEventHasFired: false121};122123interface InMemoryClipboardMetadata {124lastCopiedValue: string;125data: ClipboardStoredMetadata;126}127128const ClipboardEventUtils = {129130getTextData(clipboardData: IReadableClipboardData | DataTransfer): [string, ClipboardStoredMetadata | null] {131const text = clipboardData.getData(Mimes.text);132let metadata: ClipboardStoredMetadata | null = null;133const rawmetadata = clipboardData.getData('vscode-editor-data');134if (typeof rawmetadata === 'string') {135try {136metadata = <ClipboardStoredMetadata>JSON.parse(rawmetadata);137if (metadata.version !== 1) {138metadata = null;139}140} catch (err) {141// no problem!142}143}144if (text.length === 0 && metadata === null && clipboardData.files.length > 0) {145// no textual data pasted, generate text from file names146const files: File[] = Array.prototype.slice.call(clipboardData.files, 0);147return [files.map(file => file.name).join('\n'), null];148}149return [text, metadata];150},151152setTextData(clipboardData: IWritableClipboardData, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void {153clipboardData.setData(Mimes.text, text);154if (typeof html === 'string') {155clipboardData.setData('text/html', html);156}157clipboardData.setData('vscode-editor-data', JSON.stringify(metadata));158}159};160161/**162* Readable clipboard data for paste operations.163*/164export interface IReadableClipboardData {165/**166* All MIME types present in the clipboard.167*/168types: string[];169170/**171* Files from the clipboard (for paste operations).172*/173readonly files: readonly File[];174175/**176* Get data for a specific MIME type.177*/178getData(type: string): string;179}180181/**182* Writable clipboard data for copy/cut operations.183*/184export interface IWritableClipboardData {185/**186* Set data for a specific MIME type.187*/188setData(type: string, value: string): void;189}190191/**192* Event data for clipboard copy/cut events.193*/194export interface IClipboardCopyEvent {195/**196* Whether this is a cut operation.197*/198readonly isCut: boolean;199200/**201* The clipboard data to write to.202*/203readonly clipboardData: IWritableClipboardData;204205/**206* The data to be copied to the clipboard.207*/208readonly dataToCopy: ClipboardDataToCopy;209210/**211* Ensure that the clipboard gets the editor data.212*/213ensureClipboardGetsEditorData(): void;214215/**216* Signal that the event has been handled and default processing should be skipped.217*/218setHandled(): void;219220/**221* Whether the event has been marked as handled.222*/223readonly isHandled: boolean;224}225226/**227* Event data for clipboard paste events.228*/229export interface IClipboardPasteEvent {230/**231* The clipboard data being pasted.232*/233readonly clipboardData: IReadableClipboardData;234235/**236* The metadata stored alongside the clipboard data, if any.237*/238readonly metadata: ClipboardStoredMetadata | null;239240/**241* The text content being pasted.242*/243readonly text: string;244245/**246* The underlying DOM event, if available.247* @deprecated Use clipboardData instead. This is provided for backward compatibility.248*/249readonly browserEvent: ClipboardEvent | undefined;250251toExternalVSDataTransfer(): VSDataTransfer | undefined;252253/**254* Signal that the event has been handled and default processing should be skipped.255*/256setHandled(): void;257258/**259* Whether the event has been marked as handled.260*/261readonly isHandled: boolean;262}263264/**265* Creates an IClipboardCopyEvent from a DOM ClipboardEvent.266*/267export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean, context: ViewContext, logService: ILogService, isFirefox: boolean): IClipboardCopyEvent {268const { dataToCopy, metadata } = generateDataToCopy(context.viewModel);269let handled = false;270return {271isCut,272clipboardData: {273setData: (type: string, value: string) => {274e.clipboardData?.setData(type, value);275},276},277dataToCopy,278ensureClipboardGetsEditorData: (): void => {279e.preventDefault();280if (e.clipboardData) {281ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, metadata);282}283storeMetadataInMemory(dataToCopy.text, metadata, isFirefox);284logService.trace('ensureClipboardGetsEditorSelection with id : ', metadata.id, ' with text.length: ', dataToCopy.text.length);285},286setHandled: () => {287handled = true;288e.preventDefault();289e.stopImmediatePropagation();290},291get isHandled() { return handled; },292};293}294295/**296* Creates an IClipboardPasteEvent from a DOM ClipboardEvent.297*/298export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEvent {299let handled = false;300let [text, metadata] = e.clipboardData ? ClipboardEventUtils.getTextData(e.clipboardData) : ['', null];301metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);302return {303clipboardData: createReadableClipboardData(e.clipboardData),304metadata,305text,306toExternalVSDataTransfer: () => e.clipboardData ? toExternalVSDataTransfer(e.clipboardData) : undefined,307browserEvent: e,308setHandled: () => {309handled = true;310e.preventDefault();311e.stopImmediatePropagation();312},313get isHandled() { return handled; },314};315}316317export function createReadableClipboardData(dataTransfer: DataTransfer | undefined | null): IReadableClipboardData {318return {319types: Array.from(dataTransfer?.types ?? []),320files: Array.prototype.slice.call(dataTransfer?.files ?? [], 0),321getData: (type: string) => dataTransfer?.getData(type) ?? '',322};323}324325export function createWritableClipboardData(dataTransfer: DataTransfer | undefined | null): IWritableClipboardData {326return {327setData: (type: string, value: string) => dataTransfer?.setData(type, value),328};329}330331332