Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/clipboard/browser/clipboard.ts
5332 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
6
import * as browser from '../../../../base/browser/browser.js';
7
import { getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js';
8
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
9
import * as platform from '../../../../base/common/platform.js';
10
import * as nls from '../../../../nls.js';
11
import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
12
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
13
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
14
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
15
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
16
import { ILogService } from '../../../../platform/log/common/log.js';
17
import { CopyOptions, generateDataToCopyAndStoreInMemory, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js';
18
import { NativeEditContextRegistry } from '../../../browser/controller/editContext/native/nativeEditContextRegistry.js';
19
import { IActiveCodeEditor, ICodeEditor } from '../../../browser/editorBrowser.js';
20
import { Command, EditorAction, MultiCommand, registerEditorAction } from '../../../browser/editorExtensions.js';
21
import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';
22
import { EditorOption } from '../../../common/config/editorOptions.js';
23
import { Handler } from '../../../common/editorCommon.js';
24
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
25
import { CopyPasteController } from '../../dropOrPasteInto/browser/copyPasteController.js';
26
27
const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste';
28
29
const supportsCut = (platform.isNative || document.queryCommandSupported('cut'));
30
const supportsCopy = (platform.isNative || document.queryCommandSupported('copy'));
31
// Firefox only supports navigator.clipboard.readText() in browser extensions.
32
// See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility
33
// When loading over http, navigator.clipboard can be undefined. See https://github.com/microsoft/monaco-editor/issues/2313
34
const supportsPaste = (typeof navigator.clipboard === 'undefined' || browser.isFirefox) ? document.queryCommandSupported('paste') : true;
35
36
function registerCommand<T extends Command>(command: T): T {
37
command.register();
38
return command;
39
}
40
41
export const CutAction = supportsCut ? registerCommand(new MultiCommand({
42
id: 'editor.action.clipboardCutAction',
43
precondition: undefined,
44
kbOpts: (
45
// Do not bind cut keybindings in the browser,
46
// since browsers do that for us and it avoids security prompts
47
platform.isNative ? {
48
primary: KeyMod.CtrlCmd | KeyCode.KeyX,
49
win: { primary: KeyMod.CtrlCmd | KeyCode.KeyX, secondary: [KeyMod.Shift | KeyCode.Delete] },
50
weight: KeybindingWeight.EditorContrib
51
} : undefined
52
),
53
menuOpts: [{
54
menuId: MenuId.MenubarEditMenu,
55
group: '2_ccp',
56
title: nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "Cu&&t"),
57
order: 1
58
}, {
59
menuId: MenuId.EditorContext,
60
group: CLIPBOARD_CONTEXT_MENU_GROUP,
61
title: nls.localize('actions.clipboard.cutLabel', "Cut"),
62
when: EditorContextKeys.writable,
63
order: 1,
64
}, {
65
menuId: MenuId.CommandPalette,
66
group: '',
67
title: nls.localize('actions.clipboard.cutLabel', "Cut"),
68
order: 1
69
}, {
70
menuId: MenuId.SimpleEditorContext,
71
group: CLIPBOARD_CONTEXT_MENU_GROUP,
72
title: nls.localize('actions.clipboard.cutLabel', "Cut"),
73
when: EditorContextKeys.writable,
74
order: 1,
75
}]
76
})) : undefined;
77
78
export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({
79
id: 'editor.action.clipboardCopyAction',
80
precondition: undefined,
81
kbOpts: (
82
// Do not bind copy keybindings in the browser,
83
// since browsers do that for us and it avoids security prompts
84
platform.isNative ? {
85
primary: KeyMod.CtrlCmd | KeyCode.KeyC,
86
win: { primary: KeyMod.CtrlCmd | KeyCode.KeyC, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] },
87
weight: KeybindingWeight.EditorContrib
88
} : undefined
89
),
90
menuOpts: [{
91
menuId: MenuId.MenubarEditMenu,
92
group: '2_ccp',
93
title: nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "&&Copy"),
94
order: 2
95
}, {
96
menuId: MenuId.EditorContext,
97
group: CLIPBOARD_CONTEXT_MENU_GROUP,
98
title: nls.localize('actions.clipboard.copyLabel', "Copy"),
99
order: 2,
100
}, {
101
menuId: MenuId.CommandPalette,
102
group: '',
103
title: nls.localize('actions.clipboard.copyLabel', "Copy"),
104
order: 1
105
}, {
106
menuId: MenuId.SimpleEditorContext,
107
group: CLIPBOARD_CONTEXT_MENU_GROUP,
108
title: nls.localize('actions.clipboard.copyLabel', "Copy"),
109
order: 2,
110
}]
111
})) : undefined;
112
113
MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { submenu: MenuId.MenubarCopy, title: nls.localize2('copy as', "Copy As"), group: '2_ccp', order: 3 });
114
MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextCopy, title: nls.localize2('copy as', "Copy As"), group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 });
115
MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus) });
116
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 });
117
118
export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({
119
id: 'editor.action.clipboardPasteAction',
120
precondition: undefined,
121
kbOpts: (
122
// Do not bind paste keybindings in the browser,
123
// since browsers do that for us and it avoids security prompts
124
platform.isNative ? {
125
primary: KeyMod.CtrlCmd | KeyCode.KeyV,
126
win: { primary: KeyMod.CtrlCmd | KeyCode.KeyV, secondary: [KeyMod.Shift | KeyCode.Insert] },
127
linux: { primary: KeyMod.CtrlCmd | KeyCode.KeyV, secondary: [KeyMod.Shift | KeyCode.Insert] },
128
weight: KeybindingWeight.EditorContrib
129
} : undefined
130
),
131
menuOpts: [{
132
menuId: MenuId.MenubarEditMenu,
133
group: '2_ccp',
134
title: nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste"),
135
order: 4
136
}, {
137
menuId: MenuId.EditorContext,
138
group: CLIPBOARD_CONTEXT_MENU_GROUP,
139
title: nls.localize('actions.clipboard.pasteLabel', "Paste"),
140
when: EditorContextKeys.writable,
141
order: 4,
142
}, {
143
menuId: MenuId.CommandPalette,
144
group: '',
145
title: nls.localize('actions.clipboard.pasteLabel', "Paste"),
146
order: 1
147
}, {
148
menuId: MenuId.SimpleEditorContext,
149
group: CLIPBOARD_CONTEXT_MENU_GROUP,
150
title: nls.localize('actions.clipboard.pasteLabel', "Paste"),
151
when: EditorContextKeys.writable,
152
order: 4,
153
}]
154
})) : undefined;
155
156
class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction {
157
158
constructor() {
159
super({
160
id: 'editor.action.clipboardCopyWithSyntaxHighlightingAction',
161
label: nls.localize2('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy with Syntax Highlighting"),
162
precondition: undefined,
163
kbOpts: {
164
kbExpr: EditorContextKeys.textInputFocus,
165
primary: 0,
166
weight: KeybindingWeight.EditorContrib
167
}
168
});
169
}
170
171
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
172
const logService = accessor.get(ILogService);
173
const clipboardService = accessor.get(IClipboardService);
174
logService.trace('ExecCommandCopyWithSyntaxHighlightingAction#run');
175
if (!editor.hasModel()) {
176
return;
177
}
178
179
const emptySelectionClipboard = editor.getOption(EditorOption.emptySelectionClipboard);
180
181
if (!emptySelectionClipboard && editor.getSelection().isEmpty()) {
182
return;
183
}
184
185
CopyOptions.forceCopyWithSyntaxHighlighting = true;
186
editor.focus();
187
logService.trace('ExecCommandCopyWithSyntaxHighlightingAction (before execCommand copy)');
188
executeClipboardCopyWithWorkaround(editor, clipboardService);
189
logService.trace('ExecCommandCopyWithSyntaxHighlightingAction (after execCommand copy)');
190
CopyOptions.forceCopyWithSyntaxHighlighting = false;
191
}
192
}
193
194
function executeClipboardCopyWithWorkaround(editor: IActiveCodeEditor, clipboardService: IClipboardService) {
195
// !!!!!
196
// This is a workaround for what we think is an Electron bug where
197
// execCommand('copy') does not always work (it does not fire a clipboard event)
198
// We will use this as a signal that we have executed a copy command
199
// !!!!!
200
CopyOptions.electronBugWorkaroundCopyEventHasFired = false;
201
editor.getContainerDomNode().ownerDocument.execCommand('copy');
202
if (platform.isNative && CopyOptions.electronBugWorkaroundCopyEventHasFired === false) {
203
// We have encountered the Electron bug!
204
// As a workaround, we will write (only the plaintext data) to the clipboard in a different way
205
// We will use the clipboard service (which in the native case will go to electron's clipboard API)
206
const { dataToCopy } = generateDataToCopyAndStoreInMemory(editor._getViewModel(), undefined, browser.isFirefox);
207
clipboardService.writeText(dataToCopy.text);
208
}
209
}
210
211
function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy'): void {
212
if (!target) {
213
return;
214
}
215
216
// 1. handle case when focus is in editor.
217
target.addImplementation(10000, 'code-editor', (accessor: ServicesAccessor, args: unknown) => {
218
const logService = accessor.get(ILogService);
219
const clipboardService = accessor.get(IClipboardService);
220
logService.trace('registerExecCommandImpl (addImplementation code-editor for : ', browserCommand, ')');
221
// Only if editor text focus (i.e. not if editor has widget focus).
222
const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
223
if (focusedEditor && focusedEditor.hasTextFocus() && focusedEditor.hasModel()) {
224
// Do not execute if there is no selection and empty selection clipboard is off
225
const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard);
226
const selection = focusedEditor.getSelection();
227
if (selection && selection.isEmpty() && !emptySelectionClipboard) {
228
return true;
229
}
230
// TODO this is very ugly. The entire copy/paste/cut system needs a complete refactoring.
231
if (focusedEditor.getOption(EditorOption.effectiveEditContext) && browserCommand === 'cut') {
232
logCopyCommand(focusedEditor);
233
// execCommand(copy) works for edit context, but not execCommand(cut).
234
logService.trace('registerExecCommandImpl (before execCommand copy)');
235
executeClipboardCopyWithWorkaround(focusedEditor, clipboardService);
236
focusedEditor.trigger(undefined, Handler.Cut, undefined);
237
logService.trace('registerExecCommandImpl (after execCommand copy)');
238
} else {
239
logCopyCommand(focusedEditor);
240
logService.trace('registerExecCommandImpl (before execCommand ' + browserCommand + ')');
241
if (browserCommand === 'copy') {
242
executeClipboardCopyWithWorkaround(focusedEditor, clipboardService);
243
} else {
244
focusedEditor.getContainerDomNode().ownerDocument.execCommand(browserCommand);
245
}
246
logService.trace('registerExecCommandImpl (after execCommand ' + browserCommand + ')');
247
}
248
return true;
249
}
250
return false;
251
});
252
253
// 2. (default) handle case when focus is somewhere else.
254
target.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: unknown) => {
255
const logService = accessor.get(ILogService);
256
logService.trace('registerExecCommandImpl (addImplementation generic-dom for : ', browserCommand, ')');
257
logService.trace('registerExecCommandImpl (before execCommand ' + browserCommand + ')');
258
getActiveDocument().execCommand(browserCommand);
259
logService.trace('registerExecCommandImpl (after execCommand ' + browserCommand + ')');
260
return true;
261
});
262
}
263
264
function logCopyCommand(editor: ICodeEditor) {
265
const editContextEnabled = editor.getOption(EditorOption.effectiveEditContext);
266
if (editContextEnabled) {
267
const nativeEditContext = NativeEditContextRegistry.get(editor.getId());
268
if (nativeEditContext) {
269
nativeEditContext.handleWillCopy();
270
}
271
}
272
}
273
274
registerExecCommandImpl(CutAction, 'cut');
275
registerExecCommandImpl(CopyAction, 'copy');
276
277
if (PasteAction) {
278
// 1. Paste: handle case when focus is in editor.
279
PasteAction.addImplementation(10000, 'code-editor', (accessor: ServicesAccessor, args: unknown) => {
280
const logService = accessor.get(ILogService);
281
logService.trace('registerExecCommandImpl (addImplementation code-editor for : paste)');
282
const codeEditorService = accessor.get(ICodeEditorService);
283
const clipboardService = accessor.get(IClipboardService);
284
285
// Only if editor text focus (i.e. not if editor has widget focus).
286
const focusedEditor = codeEditorService.getFocusedCodeEditor();
287
if (focusedEditor && focusedEditor.hasModel() && focusedEditor.hasTextFocus()) {
288
// execCommand(paste) does not work with edit context
289
const editContextEnabled = focusedEditor.getOption(EditorOption.effectiveEditContext);
290
if (editContextEnabled) {
291
const nativeEditContext = NativeEditContextRegistry.get(focusedEditor.getId());
292
if (nativeEditContext) {
293
nativeEditContext.handleWillPaste();
294
}
295
}
296
297
logService.trace('registerExecCommandImpl (before triggerPaste)');
298
const triggerPaste = clipboardService.triggerPaste(getActiveWindow().vscodeWindowId);
299
if (triggerPaste) {
300
logService.trace('registerExecCommandImpl (triggerPaste defined)');
301
return triggerPaste.then(async () => {
302
logService.trace('registerExecCommandImpl (after triggerPaste)');
303
return CopyPasteController.get(focusedEditor)?.finishedPaste() ?? Promise.resolve();
304
});
305
} else {
306
logService.trace('registerExecCommandImpl (triggerPaste undefined)');
307
}
308
if (platform.isWeb) {
309
logService.trace('registerExecCommandImpl (Paste handling on web)');
310
// Use the clipboard service if document.execCommand('paste') was not successful
311
return (async () => {
312
const clipboardText = await clipboardService.readText();
313
if (clipboardText !== '') {
314
const metadata = InMemoryClipboardMetadataManager.INSTANCE.get(clipboardText);
315
let pasteOnNewLine = false;
316
let multicursorText: string[] | null = null;
317
let mode: string | null = null;
318
if (metadata) {
319
pasteOnNewLine = (focusedEditor.getOption(EditorOption.emptySelectionClipboard) && !!metadata.isFromEmptySelection);
320
multicursorText = (typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null);
321
mode = metadata.mode;
322
}
323
logService.trace('registerExecCommandImpl (clipboardText.length : ', clipboardText.length, ' id : ', metadata?.id, ')');
324
focusedEditor.trigger('keyboard', Handler.Paste, {
325
text: clipboardText,
326
pasteOnNewLine,
327
multicursorText,
328
mode
329
});
330
}
331
})();
332
}
333
return true;
334
}
335
return false;
336
});
337
338
// 2. Paste: (default) handle case when focus is somewhere else.
339
PasteAction.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: unknown) => {
340
const logService = accessor.get(ILogService);
341
logService.trace('registerExecCommandImpl (addImplementation generic-dom for : paste)');
342
const triggerPaste = accessor.get(IClipboardService).triggerPaste(getActiveWindow().vscodeWindowId);
343
return triggerPaste ?? false;
344
});
345
}
346
347
if (supportsCopy) {
348
registerEditorAction(ExecCommandCopyWithSyntaxHighlightingAction);
349
}
350
351