3
3
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4
4
*-----------------------------------------------------------------------------------------------*/
5
5
6
- import { Box } from '@mui/material' ;
6
+ import { Box , Button , Paper , Stack , Typography } from '@mui/material' ;
7
7
import React from 'react' ;
8
8
import { VSCodeMessage } from './vscodeMessage' ;
9
9
import { Terminal , ITheme } from 'xterm' ;
@@ -13,6 +13,75 @@ import { WebglAddon } from 'xterm-addon-webgl';
13
13
import 'xterm/css/xterm.css' ;
14
14
import '../../common/scrollbar.scss' ;
15
15
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
+
16
85
/**
17
86
* Represents a tab in the terminal view. Wraps an instance of xtermjs.
18
87
*/
@@ -24,6 +93,34 @@ export const TerminalInstance = (props: {
24
93
// Represents a reference to a div where the xtermjs instance is being rendered
25
94
const termRef = React . useRef ( null ) ;
26
95
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
+
27
124
// The xtermjs addon that can be used to resize the terminal according to the size of the div
28
125
const fitAddon = React . useMemo ( ( ) => {
29
126
return new FitAddon ( ) ;
@@ -35,9 +132,36 @@ export const TerminalInstance = (props: {
35
132
newTerm . loadAddon ( new WebLinksAddon ( ) ) ;
36
133
newTerm . loadAddon ( new WebglAddon ( ) ) ;
37
134
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
+ } ) ;
38
154
return newTerm ;
39
155
} ) ;
40
156
157
+ React . useEffect ( ( ) => {
158
+ const contextMenuListener = ( event ) => {
159
+ event . preventDefault ( ) ;
160
+ } ;
161
+ window . addEventListener ( 'contextmenu' , contextMenuListener ) ;
162
+ return window . removeEventListener ( 'contextmenu' , contextMenuListener ) ;
163
+ } ) ;
164
+
41
165
let resizeTimeout : NodeJS . Timeout = undefined ;
42
166
43
167
const setXtermjsTheme = ( fontFamily : string , fontSize : number ) => {
@@ -175,7 +299,7 @@ export const TerminalInstance = (props: {
175
299
} ,
176
300
} ) ;
177
301
fitAddon . fit ( ) ;
178
- }
302
+ } ;
179
303
180
304
const handleResize = function ( _e : UIEvent ) {
181
305
if ( resizeTimeout ) {
@@ -193,10 +317,36 @@ export const TerminalInstance = (props: {
193
317
} , [ fitAddon ] ) ;
194
318
195
319
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 >
197
341
< div
198
342
{ ...{ 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
+ } }
200
350
ref = { termRef }
201
351
> </ div >
202
352
</ Box >
0 commit comments