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