22// Distributed under the terms of the Apache 2.0 license.
33import React , { useState , useEffect , useRef , useCallback } from "react" ;
44import Ansi from "./Ansi" ;
5+ import clsx from "clsx" ;
56
67export type TerminalHistoryLine =
78 | readonly [ text : string , stream : "stdout" | "stderr" ]
@@ -40,14 +41,15 @@ export default function Terminal({
4041 const [ input , setInput ] = useState < string > ( defaultValue ) ;
4142 const [ history , setHistory ] = useState < readonly TerminalHistoryLine [ ] > ( [ ] ) ;
4243 const [ historyIndex , setHistoryIndex ] = useState < number | undefined > ( void 0 ) ;
43- const [ completions , setCompletions ] = useState < readonly string [ ] > ( [ ] ) ;
44- const [ completionIndex , setCompletionIndex ] = useState < number | undefined > ( void 0 ) ;
44+ const autocomplete = useRef < [ index : number , completions : readonly string [ ] ] | undefined > ( void 0 ) ;
45+
46+ const selectedStdinIndex = typeof historyIndex === "number" ? historyIndex * 2 + 1 : void 0 ;
4547
4648 useEffect ( ( ) => {
4749 if ( appLoaded ) {
4850 if ( ! firstRunComplete ) {
49- setFirstRunComplete ( true ) ;
5051 void ( async ( ) => {
52+ setFirstRunComplete ( true ) ;
5153 const initialHistory = [ ...history ] ;
5254 for ( const input of initialInputs ) {
5355 const lines = await executeInput ( input ) ;
@@ -58,7 +60,7 @@ export default function Terminal({
5860 } ) ( ) ;
5961 }
6062 }
61- } , [ appLoaded , firstRunComplete , input , history , historyIndex , collapsed ] ) ;
63+ } , [ appLoaded , firstRunComplete , initialInputs , executeInput , history ] ) ;
6264
6365 const focusInput = useCallback ( ( ) => {
6466 inputRef . current ?. focus ( ) ;
@@ -68,76 +70,95 @@ export default function Terminal({
6870 setInput ( e . target . value ) ;
6971 } , [ ] ) ;
7072
71- const onKeyDown = useCallback (
72- async ( e : React . KeyboardEvent < HTMLInputElement > ) => {
73- if ( e . key !== "Tab" && typeof completionIndex === "number" ) {
74- setCompletionIndex ( void 0 ) ;
75- }
73+ const onInputSubmit = useCallback ( async ( ) => {
74+ const lines = await executeInput ( input ) ;
75+ setHistory ( [ ...lines , ...history ] ) ;
76+ setHistoryIndex ( void 0 ) ;
77+ setCollapsed ( false ) ;
78+ setInput ( "" ) ;
79+ } , [ input , executeInput , history ] ) ;
7680
77- const inputWithPrefix = `${ commandPrefix } ${ input } ` ;
78- if ( e . key === "Enter" ) {
79- const lines = await executeInput ( input ) ;
80- setHistory ( [ ...lines , ...history ] ) ;
81- setHistoryIndex ( void 0 ) ;
82- setCollapsed ( false ) ;
81+ const setInputToHistoryAtIndex = useCallback (
82+ ( index : number ) => {
83+ const stdinHistory = history . filter ( ( line ) => line [ 1 ] === "stdin" ) ;
84+ const historySelection = stdinHistory [ index ] ;
85+ if ( historySelection ) {
86+ setInput ( historySelection [ 0 ] ) ;
87+ } else {
8388 setInput ( "" ) ;
84- } else if ( e . key === "ArrowUp" ) {
85- setHistoryIndex ( typeof historyIndex === "number" ? historyIndex + 1 : 0 ) ;
86- e . preventDefault ( ) ;
87- } else if ( e . key === "ArrowDown" ) {
88- if ( historyIndex === 0 ) {
89- setInput ( "" ) ;
90- setHistoryIndex ( void 0 ) ;
91- } else if ( typeof historyIndex === "number" ) {
92- setHistoryIndex ( historyIndex - 1 ) ;
93- }
94- e . preventDefault ( ) ;
95- } else if ( e . key === "Tab" && completeInput ) {
96- if ( typeof completionIndex === "number" ) {
97- setCompletionIndex ( completionIndex + 1 ) ;
98- } else {
99- const inputCompletions = await completeInput ( inputWithPrefix ) ;
100- const completions = inputCompletions . map ( ( str ) => str . slice ( commandPrefix . length + 1 ) ) ;
101- setCompletions ( completions ) ;
102- setCompletionIndex ( 0 ) ;
103- }
104- e . preventDefault ( ) ;
10589 }
10690 } ,
107- [ input , history , historyIndex , completionIndex ] ,
91+ [ history ] ,
10892 ) ;
10993
110- useEffect ( ( ) => {
111- if ( typeof historyIndex === "number" ) {
112- const stdinHistory = history . filter ( ( line ) => line [ 1 ] === "stdin" ) ;
113- const historySelection = stdinHistory [ historyIndex ] ;
114- if ( historySelection ) {
115- setInput ( historySelection [ 0 ] ) ;
116- } else {
94+ const onScrollHistoryUp = useCallback (
95+ ( e : React . KeyboardEvent < HTMLInputElement > ) => {
96+ const newIndex = typeof historyIndex === "number" ? ( historyIndex + 1 ) % ( history . length / 2 ) : 0 ;
97+ setHistoryIndex ( newIndex ) ;
98+ setInputToHistoryAtIndex ( newIndex ) ;
99+ e . preventDefault ( ) ;
100+ } ,
101+ [ history , historyIndex , setInputToHistoryAtIndex ] ,
102+ ) ;
103+
104+ const onScrollHistoryDown = useCallback (
105+ ( e : React . KeyboardEvent < HTMLInputElement > ) => {
106+ if ( historyIndex === 0 ) {
117107 setInput ( "" ) ;
118108 setHistoryIndex ( void 0 ) ;
109+ } else if ( typeof historyIndex === "number" ) {
110+ const newIndex = historyIndex - 1 ;
111+ setHistoryIndex ( newIndex ) ;
112+ setInputToHistoryAtIndex ( newIndex ) ;
119113 }
120- }
121- } , [ history , historyIndex ] ) ;
114+ e . preventDefault ( ) ;
115+ } ,
116+ [ historyIndex , setInputToHistoryAtIndex ] ,
117+ ) ;
122118
123- useEffect ( ( ) => {
124- if ( typeof completionIndex === "number" ) {
125- const completionSelection = completions [ completionIndex ] ;
126- if ( completionSelection ) {
127- setInput ( completionSelection ) ;
128- } else if ( completions . length > 0 ) {
129- setCompletionIndex ( completionIndex % completions . length ) ;
119+ const onToggleAutocomplete = useCallback (
120+ async ( e : React . KeyboardEvent < HTMLInputElement > ) => {
121+ let currentIndex : number ;
122+ let completions : readonly string [ ] ;
123+ if ( autocomplete . current ) {
124+ [ currentIndex , completions ] = autocomplete . current ;
130125 } else {
131- setCompletionIndex ( void 0 ) ;
126+ completions = await completeInput ( input ) ;
127+ currentIndex = - 1 ;
128+ autocomplete . current = [ 0 , completions ] ;
132129 }
133- }
134- } , [ completions , completionIndex ] ) ;
130+ const nextIndex = ( currentIndex + 1 ) % completions . length ;
131+ setInput ( completions [ nextIndex ] ) ;
132+ autocomplete . current = [ nextIndex , completions ] ;
133+ e . preventDefault ( ) ;
134+ } ,
135+ [ autocomplete , input , completeInput ] ,
136+ ) ;
137+
138+ const onKeyDown = useCallback (
139+ async ( e : React . KeyboardEvent < HTMLInputElement > ) => {
140+ if ( e . key !== "Tab" && autocomplete ) {
141+ autocomplete . current = void 0 ;
142+ }
143+
144+ if ( e . key === "Enter" ) {
145+ await onInputSubmit ( ) ;
146+ } else if ( e . key === "ArrowUp" ) {
147+ onScrollHistoryUp ( e ) ;
148+ } else if ( e . key === "ArrowDown" ) {
149+ onScrollHistoryDown ( e ) ;
150+ } else if ( e . key === "Tab" ) {
151+ await onToggleAutocomplete ( e ) ;
152+ }
153+ } ,
154+ [ autocomplete , onInputSubmit , onScrollHistoryUp , onScrollHistoryDown , onToggleAutocomplete ] ,
155+ ) ;
135156
136157 const clearHistory = useCallback ( ( ) => {
137158 setHistory ( [ ] ) ;
138159 setHistoryIndex ( void 0 ) ;
139160 setCollapsed ( true ) ;
140- } , [ history , historyIndex , collapsed ] ) ;
161+ } , [ ] ) ;
141162
142163 const height = collapsed ? `${ LINE_HEIGHT_EM } em` : expandedHeight ;
143164
@@ -149,6 +170,7 @@ export default function Terminal({
149170 Clear
150171 </ button >
151172 </ div >
173+ { /* eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events -- Click handler exists to focus on actual input element */ }
152174 < div className = "terminal-output" style = { { height } } onClick = { focusInput } >
153175 < div className = "ansi-block terminal-input" >
154176 < span style = { { display : "inline-flex" } } >
@@ -169,7 +191,13 @@ export default function Terminal({
169191 { history . map ( ( line , i ) => {
170192 const text = line [ 1 ] === "stdin" ? `${ commandPrompt } ${ line [ 2 ] } ${ line [ 0 ] } ` : line [ 0 ] ;
171193 return (
172- < Ansi key = { `terminal-history-line-${ i } ` } className = { `terminal-${ line [ 1 ] } ` } >
194+ < Ansi
195+ key = { `terminal-history-line-${ i } ` }
196+ className = { clsx ( {
197+ [ `terminal-${ line [ 1 ] } ` ] : true ,
198+ [ `terminal-history-selected` ] : i === selectedStdinIndex ,
199+ } ) }
200+ >
173201 { text }
174202 </ Ansi >
175203 ) ;
0 commit comments