From 5f9f26568156e705f1ace8bfdf297d7efee6710a Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 27 Sep 2023 13:19:20 -0400 Subject: [PATCH] Terminal: keybinding and context menu for copy & select all Adds the following keybindings to the OpenShift terminal: - `Ctrl+Shift+C`: copy selection - `Ctrl+Shift+A`: select all text Hide the existing broken context menu (provided by VS Code) and replace it with one with "Select All" and "Copy" items that actually work. This context menu imitates the style of VS Code's context menu. Closes #3353, closes #3137 Signed-off-by: David Thompson --- src/webview/common/vscode-theme.ts | 1 + .../app/terminalInstance.tsx | 158 +++++++++++++++++- .../app/terminalMultiplexer.tsx | 24 ++- 3 files changed, 165 insertions(+), 18 deletions(-) diff --git a/src/webview/common/vscode-theme.ts b/src/webview/common/vscode-theme.ts index f54c8d513..cbf448278 100644 --- a/src/webview/common/vscode-theme.ts +++ b/src/webview/common/vscode-theme.ts @@ -93,6 +93,7 @@ export function createVSCodeTheme(paletteMode: PaletteMode): Theme { variant: 'body1', }, style: { + fontSize: '0.9em', color: computedStyle.getPropertyValue('--vscode-foreground'), }, }, diff --git a/src/webview/openshift-terminal/app/terminalInstance.tsx b/src/webview/openshift-terminal/app/terminalInstance.tsx index a60782a60..35aeb8f1a 100644 --- a/src/webview/openshift-terminal/app/terminalInstance.tsx +++ b/src/webview/openshift-terminal/app/terminalInstance.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { Box } from '@mui/material'; +import { Box, Button, Paper, Stack, Typography } from '@mui/material'; import React from 'react'; import { VSCodeMessage } from './vscodeMessage'; import { Terminal, ITheme } from 'xterm'; @@ -13,6 +13,75 @@ import { WebglAddon } from 'xterm-addon-webgl'; import 'xterm/css/xterm.css'; import '../../common/scrollbar.scss'; +/** + * Clone of VS Code's context menu with "Copy" and "Select All" items. + */ +const TerminalContextMenu = (props: { + onCopyHandler: React.MouseEventHandler; + onSelectAllHandler: React.MouseEventHandler; +}) => { + return ( + + + + + + + ); +}; + /** * Represents a tab in the terminal view. Wraps an instance of xtermjs. */ @@ -24,6 +93,34 @@ export const TerminalInstance = (props: { // Represents a reference to a div where the xtermjs instance is being rendered const termRef = React.useRef(null); + const [isContextMenuOpen, setContextMenuOpen] = React.useState(false); + const contextMenuRef = React.useRef(null); + + const handleContextMenu = (event) => { + event.preventDefault(); + setContextMenuOpen(true); + const { pageX, pageY } = event; + contextMenuRef.current.style.left = `${pageX}px`; + contextMenuRef.current.style.top = `${pageY}px`; + + // Close the context menu when clicking outside of it + const handleOutsideClick = () => { + setContextMenuOpen(false); + }; + + document.addEventListener('click', handleOutsideClick); + }; + + const handleCopy = () => { + void navigator.clipboard.writeText(term.getSelection()); + setContextMenuOpen(false); + }; + + const handleSelectAll = () => { + term.selectAll(); + setContextMenuOpen(false); + }; + // The xtermjs addon that can be used to resize the terminal according to the size of the div const fitAddon = React.useMemo(() => { return new FitAddon(); @@ -35,9 +132,36 @@ export const TerminalInstance = (props: { newTerm.loadAddon(new WebLinksAddon()); newTerm.loadAddon(new WebglAddon()); newTerm.loadAddon(fitAddon); + newTerm.attachCustomKeyEventHandler((keyboardEvent: KeyboardEvent) => { + // Copy/Paste/Select All keybinding handlers + if (keyboardEvent.shiftKey && keyboardEvent.ctrlKey) { + // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values + if (keyboardEvent.code === 'KeyC' && term.hasSelection) { + // Ctrl+Shift+C copies + void navigator.clipboard.writeText(term.getSelection()); + keyboardEvent.stopPropagation(); + return false; + } else if (keyboardEvent.code === 'KeyA') { + // Ctrl+Shift+A selects all + term.selectAll(); + keyboardEvent.stopPropagation(); + return false; + } + } + + return true; + }); return newTerm; }); + React.useEffect(() => { + const contextMenuListener = (event) => { + event.preventDefault(); + }; + window.addEventListener('contextmenu', contextMenuListener); + return window.removeEventListener('contextmenu', contextMenuListener); + }); + let resizeTimeout: NodeJS.Timeout = undefined; const setXtermjsTheme = (fontFamily: string, fontSize: number) => { @@ -175,7 +299,7 @@ export const TerminalInstance = (props: { }, }); fitAddon.fit(); - } + }; const handleResize = function (_e: UIEvent) { if (resizeTimeout) { @@ -193,10 +317,36 @@ export const TerminalInstance = (props: { }, [fitAddon]); return ( - + +
+ +
diff --git a/src/webview/openshift-terminal/app/terminalMultiplexer.tsx b/src/webview/openshift-terminal/app/terminalMultiplexer.tsx index 4ca47403f..04f408624 100644 --- a/src/webview/openshift-terminal/app/terminalMultiplexer.tsx +++ b/src/webview/openshift-terminal/app/terminalMultiplexer.tsx @@ -3,24 +3,24 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import CloseIcon from '@mui/icons-material/Close'; +import TerminalIcon from '@mui/icons-material/Terminal'; import { TabContext, TabList, TabPanel } from '@mui/lab'; import { - PaletteMode, - createTheme, - SvgIcon, - Typography, Box, + PaletteMode, Stack, - styled, + SvgIcon, Tab, - ThemeProvider + ThemeProvider, + Typography, + styled } from '@mui/material'; import React from 'react'; -import { VSCodeMessage } from './vscodeMessage'; import OpenShiftIcon from '../../../../images/openshift_view.svg'; +import { createVSCodeTheme } from '../../common/vscode-theme'; import { TerminalInstance } from './terminalInstance'; -import CloseIcon from '@mui/icons-material/Close'; -import TerminalIcon from '@mui/icons-material/Terminal'; +import { VSCodeMessage } from './vscodeMessage'; /** * Represents the label for the tab that's used in the list of tabs. @@ -75,11 +75,7 @@ export const TerminalMultiplexer = () => { // represents the Material UI theme currently being used by this webview const theme = React.useMemo( () => - createTheme({ - palette: { - mode: themeKind, - }, - }), + createVSCodeTheme(themeKind), [themeKind], );