@@ -899,7 +899,9 @@ function createLineElementPooled(line: LogLine): HTMLDivElement {
899899 // Create content using innerHTML for speed (single parse)
900900 const lineNumHtml = `<span class="line-number">${ line . lineNumber + 1 } </span>` ;
901901 const displayText = applyColumnFilter ( line . text ) ;
902- const contentHtml = `<span class="line-content">${ applyHighlights ( displayText ) } </span>` ;
902+ // Apply search highlights and manual highlights together
903+ const searchResult = applySearchHighlightsRaw ( displayText , line . lineNumber ) ;
904+ const contentHtml = `<span class="line-content">${ applyHighlightsWithSearch ( displayText , searchResult . searchRanges ) } </span>` ;
903905 div . innerHTML = lineNumHtml + contentHtml ;
904906
905907 return div ;
@@ -937,11 +939,13 @@ function createLineElement(line: LogLine): HTMLDivElement {
937939 const contentSpan = document . createElement ( 'span' ) ;
938940 contentSpan . className = 'line-content' ;
939941
940- // Apply column filter, then highlights, then search matches
942+ // Apply column filter, then search highlights, then manual highlights
941943 const displayText = applyColumnFilter ( line . text ) ;
942- let highlightedText = applyHighlights ( displayText ) ;
943- highlightedText = applySearchHighlights ( highlightedText , line . lineNumber ) ;
944- contentSpan . innerHTML = highlightedText ;
944+ // Apply search highlights first (on raw text), returns { html, hasSearchMatch }
945+ const searchResult = applySearchHighlightsRaw ( displayText , line . lineNumber ) ;
946+ // Then apply manual highlights (escapes HTML and adds highlight spans)
947+ const finalHtml = applyHighlightsWithSearch ( displayText , searchResult . searchRanges ) ;
948+ contentSpan . innerHTML = finalHtml ;
945949
946950 div . appendChild ( lineNumSpan ) ;
947951 div . appendChild ( contentSpan ) ;
@@ -1035,45 +1039,176 @@ function applyHighlights(text: string): string {
10351039 return result ;
10361040}
10371041
1038- function applySearchHighlights ( html : string , lineNumber : number ) : string {
1042+ interface SearchRange {
1043+ start : number ;
1044+ end : number ;
1045+ isCurrent : boolean ;
1046+ }
1047+
1048+ function applySearchHighlightsRaw ( text : string , lineNumber : number ) : { searchRanges : SearchRange [ ] } {
10391049 // Find search matches for this line
10401050 const lineMatches = state . searchResults . filter ( m => m . lineNumber === lineNumber ) ;
10411051 if ( lineMatches . length === 0 || ! elements . searchInput . value ) {
1042- return html ;
1052+ return { searchRanges : [ ] } ;
10431053 }
10441054
10451055 // Get the search pattern
10461056 const pattern = elements . searchInput . value ;
10471057 const isRegex = elements . searchRegex . checked ;
10481058 const matchCase = elements . searchCase . checked ;
10491059
1060+ const searchRanges : SearchRange [ ] = [ ] ;
1061+
10501062 try {
10511063 let searchRegex : RegExp ;
10521064 if ( isRegex ) {
1053- searchRegex = new RegExp ( `( ${ pattern } )` , matchCase ? 'g' : 'gi' ) ;
1065+ searchRegex = new RegExp ( pattern , matchCase ? 'g' : 'gi' ) ;
10541066 } else {
10551067 // Escape special regex chars for literal search
10561068 const escaped = pattern . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
1057- searchRegex = new RegExp ( `( ${ escaped } )` , matchCase ? 'g' : 'gi' ) ;
1069+ searchRegex = new RegExp ( escaped , matchCase ? 'g' : 'gi' ) ;
10581070 }
10591071
1060- // Only apply to text content, not HTML tags
1061- // Split by HTML tags, apply highlighting to text parts only
1062- const parts = html . split ( / ( < [ ^ > ] + > ) / ) ;
1063- const highlighted = parts . map ( part => {
1064- if ( part . startsWith ( '<' ) ) {
1065- return part ; // Keep HTML tags unchanged
1066- }
1067- // Check if current search result is on this line
1068- const isCurrent = state . currentSearchIndex >= 0 &&
1069- state . searchResults [ state . currentSearchIndex ] ?. lineNumber === lineNumber ;
1070- const matchClass = isCurrent ? 'search-match current' : 'search-match' ;
1071- return part . replace ( searchRegex , `<span class="${ matchClass } ">$1</span>` ) ;
1072- } ) ;
1073- return highlighted . join ( '' ) ;
1072+ // Check if current search result is on this line
1073+ const isCurrent = state . currentSearchIndex >= 0 &&
1074+ state . searchResults [ state . currentSearchIndex ] ?. lineNumber === lineNumber ;
1075+
1076+ let match ;
1077+ while ( ( match = searchRegex . exec ( text ) ) !== null ) {
1078+ searchRanges . push ( {
1079+ start : match . index ,
1080+ end : match . index + match [ 0 ] . length ,
1081+ isCurrent,
1082+ } ) ;
1083+ }
10741084 } catch {
1075- return html ;
1085+ // Invalid regex, return empty
1086+ }
1087+
1088+ return { searchRanges } ;
1089+ }
1090+
1091+ function applyHighlightsWithSearch ( text : string , searchRanges : SearchRange [ ] ) : string {
1092+ interface HighlightRange {
1093+ start : number ;
1094+ end : number ;
1095+ type : 'search' | 'highlight' ;
1096+ className : string ;
1097+ }
1098+
1099+ const ranges : HighlightRange [ ] = [ ] ;
1100+
1101+ // Add search ranges
1102+ for ( const sr of searchRanges ) {
1103+ const className = sr . isCurrent ? 'search-match current' : 'search-match' ;
1104+ ranges . push ( { start : sr . start , end : sr . end , type : 'search' , className } ) ;
1105+ }
1106+
1107+ // Add manual highlight ranges
1108+ for ( const config of state . highlights ) {
1109+ try {
1110+ let flags = config . highlightAll ? 'g' : '' ;
1111+ if ( ! config . matchCase ) flags += 'i' ;
1112+
1113+ let pattern = config . pattern ;
1114+ if ( ! config . isRegex ) {
1115+ pattern = escapeRegex ( pattern ) ;
1116+ }
1117+
1118+ if ( config . includeWhitespace ) {
1119+ pattern = `(\\s*)${ pattern } (\\s*)` ;
1120+ }
1121+
1122+ if ( config . wholeWord && ! config . includeWhitespace ) {
1123+ pattern = `\\b${ pattern } \\b` ;
1124+ }
1125+
1126+ const regex = new RegExp ( pattern , flags || undefined ) ;
1127+ let match ;
1128+
1129+ if ( config . highlightAll ) {
1130+ while ( ( match = regex . exec ( text ) ) !== null ) {
1131+ ranges . push ( {
1132+ start : match . index ,
1133+ end : match . index + match [ 0 ] . length ,
1134+ type : 'highlight' ,
1135+ className : `highlight highlight-${ config . backgroundColor } ` ,
1136+ } ) ;
1137+ }
1138+ } else {
1139+ match = regex . exec ( text ) ;
1140+ if ( match ) {
1141+ ranges . push ( {
1142+ start : match . index ,
1143+ end : match . index + match [ 0 ] . length ,
1144+ type : 'highlight' ,
1145+ className : `highlight highlight-${ config . backgroundColor } ` ,
1146+ } ) ;
1147+ }
1148+ }
1149+ } catch {
1150+ // Invalid regex, skip
1151+ }
1152+ }
1153+
1154+ // If no ranges, just escape and return
1155+ if ( ranges . length === 0 ) {
1156+ return escapeHtml ( text ) ;
1157+ }
1158+
1159+ // Sort by start position, search matches have priority (rendered on top)
1160+ ranges . sort ( ( a , b ) => {
1161+ if ( a . start !== b . start ) return a . start - b . start ;
1162+ // Search matches render after highlights (so they appear on top)
1163+ return a . type === 'search' ? 1 : - 1 ;
1164+ } ) ;
1165+
1166+ // Build result by iterating through text and applying ranges
1167+ // We'll use a simpler non-overlapping approach: search matches take precedence
1168+ let result = '' ;
1169+ let pos = 0 ;
1170+
1171+ // Remove overlapping highlight ranges where search matches exist
1172+ const finalRanges : HighlightRange [ ] = [ ] ;
1173+ for ( const range of ranges ) {
1174+ if ( range . type === 'search' ) {
1175+ finalRanges . push ( range ) ;
1176+ } else {
1177+ // Check if this highlight overlaps with any search range
1178+ const overlapsSearch = searchRanges . some (
1179+ sr => ! ( range . end <= sr . start || range . start >= sr . end )
1180+ ) ;
1181+ if ( ! overlapsSearch ) {
1182+ finalRanges . push ( range ) ;
1183+ }
1184+ }
1185+ }
1186+
1187+ // Re-sort and remove overlapping ranges (first one wins)
1188+ finalRanges . sort ( ( a , b ) => a . start - b . start ) ;
1189+ const nonOverlapping : HighlightRange [ ] = [ ] ;
1190+ for ( const range of finalRanges ) {
1191+ if ( nonOverlapping . length === 0 || range . start >= nonOverlapping [ nonOverlapping . length - 1 ] . end ) {
1192+ nonOverlapping . push ( range ) ;
1193+ }
10761194 }
1195+
1196+ for ( const range of nonOverlapping ) {
1197+ // Add text before this range
1198+ if ( range . start > pos ) {
1199+ result += escapeHtml ( text . slice ( pos , range . start ) ) ;
1200+ }
1201+ // Add the highlighted range
1202+ result += `<span class="${ range . className } ">${ escapeHtml ( text . slice ( range . start , range . end ) ) } </span>` ;
1203+ pos = range . end ;
1204+ }
1205+
1206+ // Add remaining text
1207+ if ( pos < text . length ) {
1208+ result += escapeHtml ( text . slice ( pos ) ) ;
1209+ }
1210+
1211+ return result ;
10771212}
10781213
10791214function escapeHtml ( text : string ) : string {
@@ -2148,6 +2283,10 @@ async function terminalCdToFile(filePath: string): Promise<void> {
21482283 if ( lastSlash > 0 ) {
21492284 const dir = filePath . substring ( 0 , lastSlash ) ;
21502285 await window . api . terminalCd ( dir ) ;
2286+ // Clear terminal and show fresh prompt
2287+ if ( terminal ) {
2288+ terminal . clear ( ) ;
2289+ }
21512290 }
21522291}
21532292
@@ -2517,6 +2656,7 @@ async function performSearch(): Promise<void> {
25172656 state . currentSearchIndex = result . matches . length > 0 ? 0 : - 1 ;
25182657 updateSearchUI ( ) ;
25192658 renderMinimapMarkers ( ) ; // Update minimap with search markers
2659+ renderVisibleLines ( ) ; // Re-render to show search highlights
25202660
25212661 if ( state . currentSearchIndex >= 0 ) {
25222662 goToSearchResult ( state . currentSearchIndex ) ;
@@ -2543,6 +2683,7 @@ function goToSearchResult(index: number): void {
25432683 const result = state . searchResults [ index ] ;
25442684 goToLine ( result . lineNumber ) ;
25452685 updateSearchUI ( ) ;
2686+ renderVisibleLines ( ) ; // Update current match highlight
25462687}
25472688
25482689function goToLine ( lineNumber : number ) : void {
@@ -4045,6 +4186,9 @@ async function switchToTab(tabId: string): Promise<void> {
40454186
40464187 // Mark as loaded
40474188 tab . isLoaded = true ;
4189+
4190+ // Change terminal directory to new file's folder
4191+ terminalCdToFile ( tab . filePath ) ;
40484192 }
40494193 } finally {
40504194 hideProgress ( ) ;
0 commit comments