Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/clipboardUtils.ts
5245 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { IViewModel } from '../../../common/viewModel.js';
6
import { Range } from '../../../common/core/range.js';
7
import { isWindows } from '../../../../base/common/platform.js';
8
import { Mimes } from '../../../../base/common/mime.js';
9
import { ViewContext } from '../../../common/viewModel/viewContext.js';
10
import { ILogService } from '../../../../platform/log/common/log.js';
11
import { EditorOption } from '../../../common/config/editorOptions.js';
12
import { generateUuid } from '../../../../base/common/uuid.js';
13
import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';
14
import { toExternalVSDataTransfer } from '../../dataTransfer.js';
15
16
export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } {
17
const { dataToCopy, metadata } = generateDataToCopy(viewModel);
18
storeMetadataInMemory(dataToCopy.text, metadata, isFirefox);
19
return { dataToCopy, metadata };
20
}
21
22
function storeMetadataInMemory(textToCopy: string, metadata: ClipboardStoredMetadata, isFirefox: boolean): void {
23
InMemoryClipboardMetadataManager.INSTANCE.set(
24
// When writing "LINE\r\n" to the clipboard and then pasting,
25
// Firefox pastes "LINE\n", so let's work around this quirk
26
(isFirefox ? textToCopy.replace(/\r\n/g, '\n') : textToCopy),
27
metadata
28
);
29
}
30
31
function generateDataToCopy(viewModel: IViewModel): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } {
32
const emptySelectionClipboard = viewModel.getEditorOption(EditorOption.emptySelectionClipboard);
33
const copyWithSyntaxHighlighting = viewModel.getEditorOption(EditorOption.copyWithSyntaxHighlighting);
34
const selections = viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection);
35
const dataToCopy = getDataToCopy(viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting);
36
const metadata: ClipboardStoredMetadata = {
37
version: 1,
38
id: generateUuid(),
39
isFromEmptySelection: dataToCopy.isFromEmptySelection,
40
multicursorText: dataToCopy.multicursorText,
41
mode: dataToCopy.mode
42
};
43
return { dataToCopy, metadata };
44
}
45
46
function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy {
47
const { sourceRanges, sourceText } = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows);
48
const newLineCharacter = viewModel.model.getEOL();
49
50
const isFromEmptySelection = (emptySelectionClipboard && modelSelections.length === 1 && modelSelections[0].isEmpty());
51
const multicursorText = (Array.isArray(sourceText) ? sourceText : null);
52
const text = (Array.isArray(sourceText) ? sourceText.join(newLineCharacter) : sourceText);
53
54
let html: string | null | undefined = undefined;
55
let mode: string | null = null;
56
if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && sourceText.length < 65536)) {
57
const richText = viewModel.getRichTextToCopy(modelSelections, emptySelectionClipboard);
58
if (richText) {
59
html = richText.html;
60
mode = richText.mode;
61
}
62
}
63
const dataToCopy: ClipboardDataToCopy = {
64
isFromEmptySelection,
65
sourceRanges,
66
multicursorText,
67
text,
68
html,
69
mode
70
};
71
return dataToCopy;
72
}
73
74
/**
75
* Every time we write to the clipboard, we record a bit of extra metadata here.
76
* Every time we read from the cipboard, if the text matches our last written text,
77
* we can fetch the previous metadata.
78
*/
79
export class InMemoryClipboardMetadataManager {
80
public static readonly INSTANCE = new InMemoryClipboardMetadataManager();
81
82
private _lastState: InMemoryClipboardMetadata | null;
83
84
constructor() {
85
this._lastState = null;
86
}
87
88
public set(lastCopiedValue: string, data: ClipboardStoredMetadata): void {
89
this._lastState = { lastCopiedValue, data };
90
}
91
92
public get(pastedText: string): ClipboardStoredMetadata | null {
93
if (this._lastState && this._lastState.lastCopiedValue === pastedText) {
94
// match!
95
return this._lastState.data;
96
}
97
this._lastState = null;
98
return null;
99
}
100
}
101
102
export interface ClipboardDataToCopy {
103
isFromEmptySelection: boolean;
104
sourceRanges: Range[];
105
multicursorText: string[] | null | undefined;
106
text: string;
107
html: string | null | undefined;
108
mode: string | null;
109
}
110
111
export interface ClipboardStoredMetadata {
112
version: 1;
113
id: string | undefined;
114
isFromEmptySelection: boolean | undefined;
115
multicursorText: string[] | null | undefined;
116
mode: string | null;
117
}
118
119
export const CopyOptions = {
120
forceCopyWithSyntaxHighlighting: false,
121
electronBugWorkaroundCopyEventHasFired: false
122
};
123
124
interface InMemoryClipboardMetadata {
125
lastCopiedValue: string;
126
data: ClipboardStoredMetadata;
127
}
128
129
const ClipboardEventUtils = {
130
131
getTextData(clipboardData: IReadableClipboardData | DataTransfer): [string, ClipboardStoredMetadata | null] {
132
const text = clipboardData.getData(Mimes.text);
133
let metadata: ClipboardStoredMetadata | null = null;
134
const rawmetadata = clipboardData.getData('vscode-editor-data');
135
if (typeof rawmetadata === 'string') {
136
try {
137
metadata = <ClipboardStoredMetadata>JSON.parse(rawmetadata);
138
if (metadata.version !== 1) {
139
metadata = null;
140
}
141
} catch (err) {
142
// no problem!
143
}
144
}
145
if (text.length === 0 && metadata === null && clipboardData.files.length > 0) {
146
// no textual data pasted, generate text from file names
147
const files: File[] = Array.prototype.slice.call(clipboardData.files, 0);
148
return [files.map(file => file.name).join('\n'), null];
149
}
150
return [text, metadata];
151
},
152
153
setTextData(clipboardData: IWritableClipboardData, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void {
154
clipboardData.setData(Mimes.text, text);
155
if (typeof html === 'string') {
156
clipboardData.setData('text/html', html);
157
}
158
clipboardData.setData('vscode-editor-data', JSON.stringify(metadata));
159
}
160
};
161
162
/**
163
* Readable clipboard data for paste operations.
164
*/
165
export interface IReadableClipboardData {
166
/**
167
* All MIME types present in the clipboard.
168
*/
169
types: string[];
170
171
/**
172
* Files from the clipboard (for paste operations).
173
*/
174
readonly files: readonly File[];
175
176
/**
177
* Get data for a specific MIME type.
178
*/
179
getData(type: string): string;
180
}
181
182
/**
183
* Writable clipboard data for copy/cut operations.
184
*/
185
export interface IWritableClipboardData {
186
/**
187
* Set data for a specific MIME type.
188
*/
189
setData(type: string, value: string): void;
190
}
191
192
/**
193
* Event data for clipboard copy/cut events.
194
*/
195
export interface IClipboardCopyEvent {
196
/**
197
* Whether this is a cut operation.
198
*/
199
readonly isCut: boolean;
200
201
/**
202
* The clipboard data to write to.
203
*/
204
readonly clipboardData: IWritableClipboardData;
205
206
/**
207
* The data to be copied to the clipboard.
208
*/
209
readonly dataToCopy: ClipboardDataToCopy;
210
211
/**
212
* Ensure that the clipboard gets the editor data.
213
*/
214
ensureClipboardGetsEditorData(): void;
215
216
/**
217
* Signal that the event has been handled and default processing should be skipped.
218
*/
219
setHandled(): void;
220
221
/**
222
* Whether the event has been marked as handled.
223
*/
224
readonly isHandled: boolean;
225
}
226
227
/**
228
* Event data for clipboard paste events.
229
*/
230
export interface IClipboardPasteEvent {
231
/**
232
* The clipboard data being pasted.
233
*/
234
readonly clipboardData: IReadableClipboardData;
235
236
/**
237
* The metadata stored alongside the clipboard data, if any.
238
*/
239
readonly metadata: ClipboardStoredMetadata | null;
240
241
/**
242
* The text content being pasted.
243
*/
244
readonly text: string;
245
246
/**
247
* The underlying DOM event, if available.
248
* @deprecated Use clipboardData instead. This is provided for backward compatibility.
249
*/
250
readonly browserEvent: ClipboardEvent | undefined;
251
252
toExternalVSDataTransfer(): VSDataTransfer | undefined;
253
254
/**
255
* Signal that the event has been handled and default processing should be skipped.
256
*/
257
setHandled(): void;
258
259
/**
260
* Whether the event has been marked as handled.
261
*/
262
readonly isHandled: boolean;
263
}
264
265
/**
266
* Creates an IClipboardCopyEvent from a DOM ClipboardEvent.
267
*/
268
export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean, context: ViewContext, logService: ILogService, isFirefox: boolean): IClipboardCopyEvent {
269
const { dataToCopy, metadata } = generateDataToCopy(context.viewModel);
270
let handled = false;
271
return {
272
isCut,
273
clipboardData: {
274
setData: (type: string, value: string) => {
275
e.clipboardData?.setData(type, value);
276
},
277
},
278
dataToCopy,
279
ensureClipboardGetsEditorData: (): void => {
280
e.preventDefault();
281
if (e.clipboardData) {
282
ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, metadata);
283
}
284
storeMetadataInMemory(dataToCopy.text, metadata, isFirefox);
285
logService.trace('ensureClipboardGetsEditorSelection with id : ', metadata.id, ' with text.length: ', dataToCopy.text.length);
286
},
287
setHandled: () => {
288
handled = true;
289
e.preventDefault();
290
e.stopImmediatePropagation();
291
},
292
get isHandled() { return handled; },
293
};
294
}
295
296
/**
297
* Creates an IClipboardPasteEvent from a DOM ClipboardEvent.
298
*/
299
export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEvent {
300
let handled = false;
301
let [text, metadata] = e.clipboardData ? ClipboardEventUtils.getTextData(e.clipboardData) : ['', null];
302
metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);
303
return {
304
clipboardData: createReadableClipboardData(e.clipboardData),
305
metadata,
306
text,
307
toExternalVSDataTransfer: () => e.clipboardData ? toExternalVSDataTransfer(e.clipboardData) : undefined,
308
browserEvent: e,
309
setHandled: () => {
310
handled = true;
311
e.preventDefault();
312
e.stopImmediatePropagation();
313
},
314
get isHandled() { return handled; },
315
};
316
}
317
318
export function createReadableClipboardData(dataTransfer: DataTransfer | undefined | null): IReadableClipboardData {
319
return {
320
types: Array.from(dataTransfer?.types ?? []),
321
files: Array.prototype.slice.call(dataTransfer?.files ?? [], 0),
322
getData: (type: string) => dataTransfer?.getData(type) ?? '',
323
};
324
}
325
326
export function createWritableClipboardData(dataTransfer: DataTransfer | undefined | null): IWritableClipboardData {
327
return {
328
setData: (type: string, value: string) => dataTransfer?.setData(type, value),
329
};
330
}
331
332