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' ;
77import React from 'react' ;
88import { VSCodeMessage } from './vscodeMessage' ;
99import { Terminal , ITheme } from 'xterm' ;
@@ -13,6 +13,75 @@ import { WebglAddon } from 'xterm-addon-webgl';
1313import 'xterm/css/xterm.css' ;
1414import '../../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 >
0 commit comments