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