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,32 @@ 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+ } else if ( keyboardEvent . code === 'KeyA' ) {
143+ // Ctrl+Shift+A selects all
144+ term . selectAll ( ) ;
145+ }
146+ }
147+
148+ return true ;
149+ } ) ;
38150 return newTerm ;
39151 } ) ;
40152
153+ React . useEffect ( ( ) => {
154+ const contextMenuListener = ( event ) => {
155+ event . preventDefault ( ) ;
156+ } ;
157+ window . addEventListener ( 'contextmenu' , contextMenuListener ) ;
158+ return window . removeEventListener ( 'contextmenu' , contextMenuListener ) ;
159+ } ) ;
160+
41161 let resizeTimeout : NodeJS . Timeout = undefined ;
42162
43163 const setXtermjsTheme = ( fontFamily : string , fontSize : number ) => {
@@ -175,7 +295,7 @@ export const TerminalInstance = (props: {
175295 } ,
176296 } ) ;
177297 fitAddon . fit ( ) ;
178- }
298+ } ;
179299
180300 const handleResize = function ( _e : UIEvent ) {
181301 if ( resizeTimeout ) {
@@ -193,10 +313,36 @@ export const TerminalInstance = (props: {
193313 } , [ fitAddon ] ) ;
194314
195315 return (
196- < Box marginY = "8px" marginX = "16px" width = "100%" height = "100%" overflow = 'scroll' >
316+ < Box
317+ onContextMenu = { handleContextMenu }
318+ marginY = "8px"
319+ marginX = "16px"
320+ width = "100%"
321+ height = "100%"
322+ overflow = "scroll"
323+ >
324+ < div
325+ style = { {
326+ zIndex : 1000 ,
327+ position : 'absolute' ,
328+ display : isContextMenuOpen ? 'block' : 'none' ,
329+ } }
330+ ref = { contextMenuRef }
331+ >
332+ < TerminalContextMenu
333+ onCopyHandler = { handleCopy }
334+ onSelectAllHandler = { handleSelectAll }
335+ />
336+ </ div >
197337 < div
198338 { ...{ name : 'terminal-instance' } }
199- style = { { width : '100%' , height : '100%' , display : 'flex' , flexFlow : 'column' , overflow : 'hidden' } }
339+ style = { {
340+ width : '100%' ,
341+ height : '100%' ,
342+ display : 'flex' ,
343+ flexFlow : 'column' ,
344+ overflow : 'hidden' ,
345+ } }
200346 ref = { termRef }
201347 > </ div >
202348 </ Box >
0 commit comments