Skip to content

Commit 67c25e4

Browse files
ozgesolidkeyclaude
andcommitted
Fix search highlighting and improve terminal behavior
- Fix search pattern highlighting not appearing in results - Refactor highlight system to apply search highlights on raw text - Search highlights now properly display without mixing with manual highlights - Terminal auto-cds when switching between tabs - Terminal clears after changing directory for fresh view Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bbb4e35 commit 67c25e4

1 file changed

Lines changed: 168 additions & 24 deletions

File tree

src/renderer/renderer.ts

Lines changed: 168 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

10791214
function 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

25482689
function 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

Comments
 (0)