1515 */
1616
1717import React from "react"
18+ import { Events } from "@kui-shell/core"
1819import { ITheme , Terminal } from "xterm"
1920import { FitAddon } from "xterm-addon-fit"
20- import { Events } from "@kui-shell/core"
21+ import { SearchAddon , ISearchOptions } from "xterm-addon-search"
22+ import { Toolbar , ToolbarContent , ToolbarItem , SearchInput } from "@patternfly/react-core"
23+
24+ import "../../web/scss/components/Terminal/_index.scss"
2125
2226type WatchInit = ( ) => {
2327 /**
@@ -44,7 +48,17 @@ interface Props {
4448}
4549
4650interface State {
51+ /** Ouch, something bad happened during the render */
52+ catastrophicError ?: Error
53+
54+ /** Controller for streaming output */
4755 streamer ?: ReturnType < WatchInit >
56+
57+ /** Current search filter */
58+ filter ?: string
59+
60+ /** Current search results */
61+ searchResults ?: { resultIndex : number ; resultCount : number } | void
4862}
4963
5064export default class XTerm extends React . PureComponent < Props , State > {
@@ -53,9 +67,24 @@ export default class XTerm extends React.PureComponent<Props, State> {
5367 scrollback : 5000 ,
5468 } )
5569
70+ private searchAddon = new SearchAddon ( )
71+
5672 private readonly cleaners : ( ( ) => void ) [ ] = [ ]
5773 private readonly container = React . createRef < HTMLDivElement > ( )
5874
75+ public constructor ( props : Props ) {
76+ super ( props )
77+ this . state = { }
78+ }
79+
80+ public static getDerivedStateFromError ( error : Error ) {
81+ return { catastrophicError : error }
82+ }
83+
84+ public componentDidCatch ( error : Error , errorInfo : React . ErrorInfo ) {
85+ console . error ( "catastrophic error in Scalar" , error , errorInfo )
86+ }
87+
5988 public componentDidMount ( ) {
6089 this . mountTerminal ( )
6190
@@ -78,6 +107,7 @@ export default class XTerm extends React.PureComponent<Props, State> {
78107 private unmountTerminal ( ) {
79108 if ( this . terminal ) {
80109 this . terminal . dispose ( )
110+ this . searchAddon . dispose ( )
81111 }
82112 }
83113
@@ -89,6 +119,10 @@ export default class XTerm extends React.PureComponent<Props, State> {
89119
90120 const fitAddon = new FitAddon ( )
91121 this . terminal . loadAddon ( fitAddon )
122+ setTimeout ( ( ) => {
123+ this . terminal . loadAddon ( this . searchAddon )
124+ this . searchAddon . onDidChangeResults ( this . searchResults )
125+ } , 100 )
92126
93127 const inject = ( ) => this . injectTheme ( this . terminal , xtermContainer )
94128 inject ( )
@@ -97,8 +131,14 @@ export default class XTerm extends React.PureComponent<Props, State> {
97131
98132 if ( this . props . initialContent ) {
99133 // @starpit i don't know why we have to split the newlines...
100- this . props . initialContent . split ( / \n / ) . forEach ( ( line ) => this . terminal . writeln ( line ) )
101- // this.terminal.write(this.props.initialContent)
134+ // versus: this.terminal.write(this.props.initialContent)
135+ this . props . initialContent . split ( / \n / ) . forEach ( ( line , idx , A ) => {
136+ if ( idx === A . length - 1 && line . length === 0 ) {
137+ // skip trailing blank line resulting from the split
138+ } else {
139+ this . terminal . writeln ( line )
140+ }
141+ } )
102142 }
103143
104144 this . terminal . open ( xtermContainer )
@@ -177,6 +217,14 @@ export default class XTerm extends React.PureComponent<Props, State> {
177217 xterm . setOption ( "theme" , itheme )
178218 xterm . setOption ( "fontFamily" , val ( "monospace" , "font" ) )
179219
220+ // strange. these values don't seem to have any effect
221+ this . searchOptions . decorations = {
222+ activeMatchBackground : val ( "var(--color-base09)" ) ,
223+ matchBackground : val ( "var(--color-base02)" ) ,
224+ matchOverviewRuler : val ( "var(--color-base05)" ) ,
225+ activeMatchColorOverviewRuler : val ( "var(--color-base05)" ) ,
226+ }
227+
180228 try {
181229 const standIn = document . querySelector ( "body .repl .repl-input input" )
182230 if ( standIn ) {
@@ -202,7 +250,82 @@ export default class XTerm extends React.PureComponent<Props, State> {
202250 }
203251 }
204252
253+ private readonly searchResults = ( searchResults : State [ "searchResults" ] ) => {
254+ this . setState ( { searchResults } )
255+ }
256+
257+ /** Note: decorations need to be enabled in order for our `onSearch` handler to be called */
258+ private searchOptions : ISearchOptions = {
259+ regex : true ,
260+ decorations : { matchOverviewRuler : "orange" , activeMatchColorOverviewRuler : "green" } , // placeholder; see injectTheme above
261+ }
262+
263+ private readonly onSearch = ( filter : string ) => {
264+ this . setState ( { filter } )
265+ this . searchAddon . findNext ( filter , this . searchOptions )
266+ }
267+
268+ private readonly onSearchClear = ( ) => {
269+ this . setState ( { filter : undefined } )
270+ this . searchAddon . clearDecorations ( )
271+ }
272+
273+ private readonly onSearchNext = ( ) => {
274+ if ( this . state . filter ) {
275+ this . searchAddon . findNext ( this . state . filter , this . searchOptions )
276+ }
277+ }
278+
279+ private readonly onSearchPrevious = ( ) => {
280+ if ( this . state . filter ) {
281+ this . searchAddon . findPrevious ( this . state . filter , this . searchOptions )
282+ }
283+ }
284+
285+ /** @return "n/m" text to represent the current search results, for UI */
286+ private resultsCount ( ) {
287+ if ( this . state . searchResults ) {
288+ return `${ this . state . searchResults . resultIndex + 1 } /${ this . state . searchResults . resultCount } `
289+ }
290+ }
291+
292+ private searchInput ( ) {
293+ return (
294+ < SearchInput
295+ aria-label = "Search output"
296+ placeholder = "Enter search text"
297+ value = { this . state . filter }
298+ onChange = { this . onSearch }
299+ onClear = { this . onSearchClear }
300+ onNextClick = { this . onSearchNext . bind ( this ) }
301+ onPreviousClick = { this . onSearchPrevious . bind ( this ) }
302+ resultsCount = { this . resultsCount ( ) }
303+ />
304+ )
305+ }
306+
307+ private toolbar ( ) {
308+ return (
309+ < Toolbar className = "codeflare--toolbar" >
310+ < ToolbarContent className = "flex-fill" >
311+ < ToolbarItem variant = "search-filter" className = "flex-fill" >
312+ { this . searchInput ( ) }
313+ </ ToolbarItem >
314+ </ ToolbarContent >
315+ </ Toolbar >
316+ )
317+ }
318+
205319 public render ( ) {
206- return < div ref = { this . container } className = "xterm-container" onKeyUp = { this . onKeyUp } />
320+ if ( this . state . catastrophicError ) {
321+ return "InternalError"
322+ } else {
323+ return (
324+ < div className = "flex-layout flex-column flex-align-stretch flex-fill" >
325+ < div ref = { this . container } className = "xterm-container" onKeyUp = { this . onKeyUp } />
326+ { this . toolbar ( ) }
327+ </ div >
328+ )
329+ }
207330 }
208331}
0 commit comments