Skip to content

Commit 24e4830

Browse files
committed
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 <[email protected]>
1 parent 881c2fe commit 24e4830

File tree

3 files changed

+165
-18
lines changed

3 files changed

+165
-18
lines changed

src/webview/common/vscode-theme.ts

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export function createVSCodeTheme(paletteMode: PaletteMode): Theme {
9393
variant: 'body1',
9494
},
9595
style: {
96+
fontSize: '0.9em',
9697
color: computedStyle.getPropertyValue('--vscode-foreground'),
9798
},
9899
},

src/webview/openshift-terminal/app/terminalInstance.tsx

+154-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6-
import { Box } from '@mui/material';
6+
import { Box, Button, Paper, Stack, Typography } from '@mui/material';
77
import React from 'react';
88
import { VSCodeMessage } from './vscodeMessage';
99
import { Terminal, ITheme } from 'xterm';
@@ -13,6 +13,75 @@ import { WebglAddon } from 'xterm-addon-webgl';
1313
import 'xterm/css/xterm.css';
1414
import '../../common/scrollbar.scss';
1515

16+
/**
17+
* Clone of VS Code's context menu with "Copy" and "Select All" items.
18+
*/
19+
const TerminalContextMenu = (props: {
20+
onCopyHandler: React.MouseEventHandler<HTMLButtonElement>;
21+
onSelectAllHandler: React.MouseEventHandler<HTMLButtonElement>;
22+
}) => {
23+
return (
24+
<Paper
25+
variant="outlined"
26+
sx={{
27+
borderRadius: '6px',
28+
backgroundColor: 'var(--vscode-editor-background)',
29+
borderColor: 'var(--vscode-menu-border)',
30+
boxShadow: '0px 0px 8px var(--vscode-widget-shadow)',
31+
}}
32+
>
33+
<Stack direction="column" minWidth="200px" marginX="4px" marginY="3px">
34+
<Button
35+
variant="text"
36+
onClick={props.onCopyHandler}
37+
sx={{
38+
width: '100%',
39+
textTransform: 'none',
40+
'&:hover': {
41+
backgroundColor:
42+
'color-mix(in srgb, var(--vscode-button-background) 50%, black)',
43+
},
44+
paddingY: '4px',
45+
}}
46+
>
47+
<Stack
48+
direction="row"
49+
justifyContent="space-between"
50+
marginX="13px"
51+
style={{ width: '100%' }}
52+
>
53+
<Typography variant="body1">Copy</Typography>
54+
<Typography variant="body1">Ctrl+Shift+C</Typography>
55+
</Stack>
56+
</Button>
57+
<Button
58+
variant="text"
59+
onClick={props.onSelectAllHandler}
60+
sx={{
61+
width: '100%',
62+
textTransform: 'none',
63+
'&:hover': {
64+
backgroundColor:
65+
'color-mix(in srgb, var(--vscode-button-background) 50%, black)',
66+
},
67+
paddingY: '4px',
68+
}}
69+
>
70+
<Stack
71+
direction="row"
72+
justifyContent="space-between"
73+
marginX="13px"
74+
style={{ width: '100%' }}
75+
>
76+
<Typography variant="body1">Select All</Typography>
77+
<Typography variant="body1">Ctrl+Shift+A</Typography>
78+
</Stack>
79+
</Button>
80+
</Stack>
81+
</Paper>
82+
);
83+
};
84+
1685
/**
1786
* Represents a tab in the terminal view. Wraps an instance of xtermjs.
1887
*/
@@ -24,6 +93,34 @@ export const TerminalInstance = (props: {
2493
// Represents a reference to a div where the xtermjs instance is being rendered
2594
const termRef = React.useRef(null);
2695

96+
const [isContextMenuOpen, setContextMenuOpen] = React.useState(false);
97+
const contextMenuRef = React.useRef(null);
98+
99+
const handleContextMenu = (event) => {
100+
event.preventDefault();
101+
setContextMenuOpen(true);
102+
const { pageX, pageY } = event;
103+
contextMenuRef.current.style.left = `${pageX}px`;
104+
contextMenuRef.current.style.top = `${pageY}px`;
105+
106+
// Close the context menu when clicking outside of it
107+
const handleOutsideClick = () => {
108+
setContextMenuOpen(false);
109+
};
110+
111+
document.addEventListener('click', handleOutsideClick);
112+
};
113+
114+
const handleCopy = () => {
115+
void navigator.clipboard.writeText(term.getSelection());
116+
setContextMenuOpen(false);
117+
};
118+
119+
const handleSelectAll = () => {
120+
term.selectAll();
121+
setContextMenuOpen(false);
122+
};
123+
27124
// The xtermjs addon that can be used to resize the terminal according to the size of the div
28125
const fitAddon = React.useMemo(() => {
29126
return new FitAddon();
@@ -35,9 +132,36 @@ export const TerminalInstance = (props: {
35132
newTerm.loadAddon(new WebLinksAddon());
36133
newTerm.loadAddon(new WebglAddon());
37134
newTerm.loadAddon(fitAddon);
135+
newTerm.attachCustomKeyEventHandler((keyboardEvent: KeyboardEvent) => {
136+
// Copy/Paste/Select All keybinding handlers
137+
if (keyboardEvent.shiftKey && keyboardEvent.ctrlKey) {
138+
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
139+
if (keyboardEvent.code === 'KeyC' && term.hasSelection) {
140+
// Ctrl+Shift+C copies
141+
void navigator.clipboard.writeText(term.getSelection());
142+
keyboardEvent.stopPropagation();
143+
return false;
144+
} else if (keyboardEvent.code === 'KeyA') {
145+
// Ctrl+Shift+A selects all
146+
term.selectAll();
147+
keyboardEvent.stopPropagation();
148+
return false;
149+
}
150+
}
151+
152+
return true;
153+
});
38154
return newTerm;
39155
});
40156

157+
React.useEffect(() => {
158+
const contextMenuListener = (event) => {
159+
event.preventDefault();
160+
};
161+
window.addEventListener('contextmenu', contextMenuListener);
162+
return window.removeEventListener('contextmenu', contextMenuListener);
163+
});
164+
41165
let resizeTimeout: NodeJS.Timeout = undefined;
42166

43167
const setXtermjsTheme = (fontFamily: string, fontSize: number) => {
@@ -175,7 +299,7 @@ export const TerminalInstance = (props: {
175299
},
176300
});
177301
fitAddon.fit();
178-
}
302+
};
179303

180304
const handleResize = function (_e: UIEvent) {
181305
if (resizeTimeout) {
@@ -193,10 +317,36 @@ export const TerminalInstance = (props: {
193317
}, [fitAddon]);
194318

195319
return (
196-
<Box marginY="8px" marginX="16px" width="100%" height="100%" overflow='scroll'>
320+
<Box
321+
onContextMenu={handleContextMenu}
322+
marginY="8px"
323+
marginX="16px"
324+
width="100%"
325+
height="100%"
326+
overflow="scroll"
327+
>
328+
<div
329+
style={{
330+
zIndex: 1000,
331+
position: 'absolute',
332+
display: isContextMenuOpen ? 'block' : 'none',
333+
}}
334+
ref={contextMenuRef}
335+
>
336+
<TerminalContextMenu
337+
onCopyHandler={handleCopy}
338+
onSelectAllHandler={handleSelectAll}
339+
/>
340+
</div>
197341
<div
198342
{...{ name: 'terminal-instance' }}
199-
style={{ width: '100%', height: '100%', display: 'flex', flexFlow: 'column', overflow: 'hidden' }}
343+
style={{
344+
width: '100%',
345+
height: '100%',
346+
display: 'flex',
347+
flexFlow: 'column',
348+
overflow: 'hidden',
349+
}}
200350
ref={termRef}
201351
></div>
202352
</Box>

src/webview/openshift-terminal/app/terminalMultiplexer.tsx

+10-14
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6+
import CloseIcon from '@mui/icons-material/Close';
7+
import TerminalIcon from '@mui/icons-material/Terminal';
68
import { TabContext, TabList, TabPanel } from '@mui/lab';
79
import {
8-
PaletteMode,
9-
createTheme,
10-
SvgIcon,
11-
Typography,
1210
Box,
11+
PaletteMode,
1312
Stack,
14-
styled,
13+
SvgIcon,
1514
Tab,
16-
ThemeProvider
15+
ThemeProvider,
16+
Typography,
17+
styled
1718
} from '@mui/material';
1819
import React from 'react';
19-
import { VSCodeMessage } from './vscodeMessage';
2020
import OpenShiftIcon from '../../../../images/openshift_view.svg';
21+
import { createVSCodeTheme } from '../../common/vscode-theme';
2122
import { TerminalInstance } from './terminalInstance';
22-
import CloseIcon from '@mui/icons-material/Close';
23-
import TerminalIcon from '@mui/icons-material/Terminal';
23+
import { VSCodeMessage } from './vscodeMessage';
2424

2525
/**
2626
* Represents the label for the tab that's used in the list of tabs.
@@ -75,11 +75,7 @@ export const TerminalMultiplexer = () => {
7575
// represents the Material UI theme currently being used by this webview
7676
const theme = React.useMemo(
7777
() =>
78-
createTheme({
79-
palette: {
80-
mode: themeKind,
81-
},
82-
}),
78+
createVSCodeTheme(themeKind),
8379
[themeKind],
8480
);
8581

0 commit comments

Comments
 (0)