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