@@ -107,6 +107,17 @@ function getTerminalBindings() {
107107 return resolvedBindings ( ) . filter ( ( b ) => b . layer === 'terminal' ) ;
108108}
109109
110+ // Browser-style find: amber highlight for all matches, orange for the active
111+ // one. Like a browser, the highlight palette is fixed rather than theme-derived.
112+ // Overview-ruler colors must be solid; match backgrounds carry alpha so the
113+ // underlying glyphs stay legible on both light and dark terminals.
114+ const SEARCH_DECORATIONS = {
115+ matchBackground : 'rgba(255, 213, 79, 0.4)' ,
116+ matchOverviewRuler : '#ffd54f' ,
117+ activeMatchBackground : 'rgba(255, 138, 0, 0.85)' ,
118+ activeMatchColorOverviewRuler : '#ff8a00' ,
119+ } as const ;
120+
110121export function TerminalView ( props : TerminalViewProps ) {
111122 let containerRef ! : HTMLDivElement ;
112123 let term : Terminal | undefined ;
@@ -116,26 +127,30 @@ export function TerminalView(props: TerminalViewProps) {
116127 let searchInputRef : HTMLInputElement | undefined ;
117128 const [ searchOpen , setSearchOpen ] = createSignal ( false ) ;
118129 const [ searchQuery , setSearchQuery ] = createSignal ( '' ) ;
119- const [ searchResultIndex , setSearchResultIndex ] = createSignal ( - 1 ) ;
120- const [ searchResultCount , setSearchResultCount ] = createSignal ( 0 ) ;
121-
122- // Browser-style find: amber highlight for all matches, orange for the active
123- // one. Overview-ruler colors must be solid; match backgrounds carry alpha so
124- // the underlying glyphs stay legible regardless of theme.
125- const SEARCH_DECORATIONS = {
126- matchBackground : 'rgba(255, 213, 79, 0.4)' ,
127- matchOverviewRuler : '#ffd54f' ,
128- activeMatchBackground : 'rgba(255, 138, 0, 0.85)' ,
129- activeMatchColorOverviewRuler : '#ff8a00' ,
130- } as const ;
130+ // resultIndex is -1 when there's no active match (or when xterm's match
131+ // threshold is exceeded); count is the total. They always change together.
132+ const [ searchResult , setSearchResult ] = createSignal ( { index : - 1 , count : 0 } ) ;
133+
134+ const resetSearchResults = ( ) => setSearchResult ( { index : - 1 , count : 0 } ) ;
135+
136+ // The find addon is loaded lazily on first use: most terminals are never
137+ // searched, and an idle addon keeps a per-write listener alive on every pane.
138+ function ensureSearchAddon ( ) : SearchAddon | undefined {
139+ if ( searchAddon || ! term ) return searchAddon ;
140+ searchAddon = new SearchAddon ( ) ;
141+ term . loadAddon ( searchAddon ) ;
142+ searchAddon . onDidChangeResults ( ( { resultIndex, resultCount } ) =>
143+ setSearchResult ( { index : resultIndex , count : resultCount } ) ,
144+ ) ;
145+ return searchAddon ;
146+ }
131147
132148 function runSearch ( direction : 'next' | 'prev' , incremental = false ) {
133149 if ( ! searchAddon ) return ;
134150 const q = searchQuery ( ) ;
135151 if ( ! q ) {
136152 searchAddon . clearDecorations ( ) ;
137- setSearchResultIndex ( - 1 ) ;
138- setSearchResultCount ( 0 ) ;
153+ resetSearchResults ( ) ;
139154 return ;
140155 }
141156 const opts = { incremental, decorations : SEARCH_DECORATIONS } ;
@@ -157,18 +172,18 @@ export function TerminalView(props: TerminalViewProps) {
157172 searchInputRef ?. select ( ) ;
158173 return ;
159174 }
175+ ensureSearchAddon ( ) ;
160176 // Seed from a single-line selection, like a browser's Find does.
161177 const sel = term . getSelection ( ) ;
162- if ( sel && ! sel . includes ( '\n' ) && sel . length <= 200 ) setSearchQuery ( sel ) ;
178+ if ( sel && ! sel . includes ( '\n' ) ) setSearchQuery ( sel ) ;
163179 setSearchOpen ( true ) ;
164180 if ( searchQuery ( ) ) runSearch ( 'next' ) ;
165181 }
166182
167183 function closeSearch ( ) {
168184 setSearchOpen ( false ) ;
169185 searchAddon ?. clearDecorations ( ) ;
170- setSearchResultIndex ( - 1 ) ;
171- setSearchResultCount ( 0 ) ;
186+ resetSearchResults ( ) ;
172187 term ?. focus ( ) ;
173188 }
174189
@@ -216,14 +231,6 @@ export function TerminalView(props: TerminalViewProps) {
216231 term . loadAddon ( fitAddon ) ;
217232 term . loadAddon ( new WebLinksAddon ( openTerminalHttpLinkWithModifier ) ) ;
218233
219- searchAddon = new SearchAddon ( ) ;
220- term . loadAddon ( searchAddon ) ;
221- // Keep the find-bar counter in sync with xterm's match results.
222- searchAddon . onDidChangeResults ( ( { resultIndex, resultCount } ) => {
223- setSearchResultIndex ( resultIndex ) ;
224- setSearchResultCount ( resultCount ) ;
225- } ) ;
226-
227234 term . open ( containerRef ) ;
228235
229236 // Block direct PTY keyboard input only after self-landing has removed the
@@ -830,6 +837,8 @@ export function TerminalView(props: TerminalViewProps) {
830837 onOutput . cleanup ?.( ) ;
831838 webglAddon ?. dispose ( ) ;
832839 webglAddon = undefined ;
840+ searchAddon ?. dispose ( ) ;
841+ searchAddon = undefined ;
833842 unregisterTerminal ( agentId ) ;
834843 if ( ptyPaused && ! taskPtyDetached ( ) ) {
835844 fireAndForget ( IPC . ResumeAgent , { agentId } ) ;
@@ -880,8 +889,8 @@ export function TerminalView(props: TerminalViewProps) {
880889 < Show when = { searchOpen ( ) } >
881890 < TerminalSearchOverlay
882891 query = { searchQuery ( ) }
883- resultIndex = { searchResultIndex ( ) }
884- resultCount = { searchResultCount ( ) }
892+ resultIndex = { searchResult ( ) . index }
893+ resultCount = { searchResult ( ) . count }
885894 onInput = { onSearchInput }
886895 onNext = { ( ) => runSearch ( 'next' ) }
887896 onPrev = { ( ) => runSearch ( 'prev' ) }
0 commit comments