Skip to content

Commit 48861ea

Browse files
committed
fix: highlighting of log searches
1 parent 14ddc85 commit 48861ea

2 files changed

Lines changed: 137 additions & 96 deletions

File tree

src/app.tsx

Lines changed: 121 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function App(props: AppProps) {
7575
filteredLogs,
7676
searchQuery,
7777
setSearchQuery,
78-
searchMatches,
78+
getSearchMatches,
7979
currentMatchIndex,
8080
setCurrentMatchIndex,
8181
nextMatch,
@@ -122,6 +122,28 @@ export function App(props: AppProps) {
122122
return filteredLogs();
123123
};
124124

125+
// Compute search matches based on visible logs
126+
const searchMatches = () => getSearchMatches(visibleLogs());
127+
128+
// Clear search when switching services or view mode
129+
const [lastSelectedName, setLastSelectedName] = createSignal<string | null>(null);
130+
const [lastViewMode, setLastViewMode] = createSignal<"single" | "all">("single");
131+
132+
createEffect(() => {
133+
const currentName = selectedName();
134+
const currentViewMode = viewMode();
135+
const prevName = lastSelectedName();
136+
const prevViewMode = lastViewMode();
137+
138+
// Clear search if service or view mode changed
139+
if ((currentName !== prevName || currentViewMode !== prevViewMode) && isSearchActive()) {
140+
clearSearch();
141+
}
142+
143+
setLastSelectedName(currentName);
144+
setLastViewMode(currentViewMode);
145+
});
146+
125147
// Update scroll info from scrollbox
126148
const updateScrollInfo = () => {
127149
if (scrollboxRef) {
@@ -219,12 +241,14 @@ export function App(props: AppProps) {
219241
if (query) {
220242
setSearchQuery(query);
221243
setCurrentMatchIndex(0);
222-
// Scroll to first match
223-
const matches = searchMatches();
224-
if (matches.length > 0 && scrollboxRef) {
225-
scrollboxRef.scrollTo(matches[0]!.logIndex);
226-
setFollowing(false);
227-
}
244+
// Scroll to first match after query is set
245+
setTimeout(() => {
246+
const matches = searchMatches();
247+
if (matches.length > 0 && scrollboxRef) {
248+
scrollboxRef.scrollTo(matches[0]!.logIndex);
249+
setFollowing(false);
250+
}
251+
}, 0);
228252
}
229253
setSearchMode(false);
230254
setSearchInput("");
@@ -487,13 +511,16 @@ export function App(props: AppProps) {
487511
case "n":
488512
// Next search match
489513
if (isSearchActive()) {
490-
nextMatch();
491514
const matches = searchMatches();
492-
const idx = currentMatchIndex();
493-
if (matches.length > 0 && scrollboxRef) {
494-
scrollboxRef.scrollTo(matches[idx]!.logIndex);
495-
setFollowing(false);
496-
}
515+
nextMatch(matches.length);
516+
// Get updated index after nextMatch
517+
setTimeout(() => {
518+
const idx = currentMatchIndex();
519+
if (matches.length > 0 && scrollboxRef && matches[idx]) {
520+
scrollboxRef.scrollTo(matches[idx]!.logIndex);
521+
setFollowing(false);
522+
}
523+
}, 0);
497524
}
498525
event.preventDefault();
499526
break;
@@ -502,13 +529,16 @@ export function App(props: AppProps) {
502529
if (event.shift) {
503530
// Previous match (N in vim)
504531
if (isSearchActive()) {
505-
prevMatch();
506532
const matches = searchMatches();
507-
const idx = currentMatchIndex();
508-
if (matches.length > 0 && scrollboxRef) {
509-
scrollboxRef.scrollTo(matches[idx]!.logIndex);
510-
setFollowing(false);
511-
}
533+
prevMatch(matches.length);
534+
// Get updated index after prevMatch
535+
setTimeout(() => {
536+
const idx = currentMatchIndex();
537+
if (matches.length > 0 && scrollboxRef && matches[idx]) {
538+
scrollboxRef.scrollTo(matches[idx]!.logIndex);
539+
setFollowing(false);
540+
}
541+
}, 0);
512542
}
513543
event.preventDefault();
514544
}
@@ -667,10 +697,48 @@ export function App(props: AppProps) {
667697
<box flexDirection="column">
668698
<For each={visibleLogs()}>
669699
{(log, logIdx) => {
670-
// Check if this log line has matches
671-
const matches = searchMatches().filter((m) => m.logIndex === logIdx());
672-
const currentMatch = searchMatches()[currentMatchIndex()];
673-
const isCurrentMatchLine = currentMatch?.logIndex === logIdx();
700+
// Reactive accessors for search matches - must be functions for SolidJS reactivity
701+
const matches = () => searchMatches().filter((m) => m.logIndex === logIdx());
702+
const currentMatch = () => searchMatches()[currentMatchIndex()];
703+
const isCurrentMatchLine = () => currentMatch()?.logIndex === logIdx();
704+
705+
// Compute highlighted parts reactively
706+
const highlightedParts = () => {
707+
const m = matches();
708+
if (m.length === 0) return null;
709+
710+
const content = log.content;
711+
const parts: { text: string; highlight: boolean; isCurrent: boolean }[] = [];
712+
let lastEnd = 0;
713+
const current = currentMatch();
714+
const isCurrentLine = isCurrentMatchLine();
715+
716+
m.forEach((match) => {
717+
if (match.startIndex > lastEnd) {
718+
parts.push({
719+
text: content.slice(lastEnd, match.startIndex),
720+
highlight: false,
721+
isCurrent: false,
722+
});
723+
}
724+
parts.push({
725+
text: content.slice(match.startIndex, match.endIndex),
726+
highlight: true,
727+
isCurrent: isCurrentLine && match === current,
728+
});
729+
lastEnd = match.endIndex;
730+
});
731+
732+
if (lastEnd < content.length) {
733+
parts.push({
734+
text: content.slice(lastEnd),
735+
highlight: false,
736+
isCurrent: false,
737+
});
738+
}
739+
740+
return parts;
741+
};
674742

675743
return (
676744
<box height={1} paddingLeft={1} flexDirection="row">
@@ -680,60 +748,36 @@ export function App(props: AppProps) {
680748
<text fg="cyan">{log.service}</text>
681749
<text fg="gray"> | </text>
682750
</Show>
683-
<Show when={matches.length > 0} fallback={
684-
<text fg={log.stream === "stderr" ? "red" : undefined}>{log.content}</text>
685-
}>
686-
{/* Render with highlighted matches */}
687-
{(() => {
688-
const content = log.content;
689-
const parts: { text: string; highlight: boolean; isCurrent: boolean }[] = [];
690-
let lastEnd = 0;
691-
692-
matches.forEach((match) => {
693-
if (match.startIndex > lastEnd) {
694-
parts.push({
695-
text: content.slice(lastEnd, match.startIndex),
696-
highlight: false,
697-
isCurrent: false,
698-
});
699-
}
700-
parts.push({
701-
text: content.slice(match.startIndex, match.endIndex),
702-
highlight: true,
703-
isCurrent: isCurrentMatchLine && match === currentMatch,
704-
});
705-
lastEnd = match.endIndex;
706-
});
707-
708-
if (lastEnd < content.length) {
709-
parts.push({
710-
text: content.slice(lastEnd),
711-
highlight: false,
712-
isCurrent: false,
713-
});
714-
}
715-
716-
return (
717-
<box flexDirection="row">
718-
<For each={parts}>
719-
{(part) => (
720-
<Show
721-
when={part.highlight}
722-
fallback={
723-
<text fg={log.stream === "stderr" ? "red" : undefined}>
724-
{part.text}
725-
</text>
726-
}
727-
>
728-
<box backgroundColor={part.isCurrent ? "#ffff00" : "#555500"}>
729-
<text fg="black">{part.text}</text>
730-
</box>
731-
</Show>
732-
)}
733-
</For>
734-
</box>
735-
);
736-
})()}
751+
<Show
752+
when={highlightedParts()}
753+
fallback={
754+
<text fg={log.stream === "stderr" ? "red" : undefined}>
755+
{log.content}
756+
</text>
757+
}
758+
>
759+
{(
760+
parts: () => { text: string; highlight: boolean; isCurrent: boolean }[],
761+
) => (
762+
<box flexDirection="row">
763+
<For each={parts()}>
764+
{(part) => (
765+
<Show
766+
when={part.highlight}
767+
fallback={
768+
<text fg={log.stream === "stderr" ? "red" : undefined}>
769+
{part.text}
770+
</text>
771+
}
772+
>
773+
<box backgroundColor={part.isCurrent ? "#ffff00" : "#555500"}>
774+
<text fg="black">{part.text}</text>
775+
</box>
776+
</Show>
777+
)}
778+
</For>
779+
</box>
780+
)}
737781
</Show>
738782
</box>
739783
);

src/ui/hooks/useLogs.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export interface UseLogsReturn {
2222
// Search functionality
2323
searchQuery: () => string;
2424
setSearchQuery: (query: string) => void;
25-
searchMatches: () => SearchMatch[];
25+
getSearchMatches: (logsToSearch: LogLine[]) => SearchMatch[];
2626
currentMatchIndex: () => number;
2727
setCurrentMatchIndex: (index: number) => void;
28-
nextMatch: () => void;
29-
prevMatch: () => void;
28+
nextMatch: (totalMatches: number) => void;
29+
prevMatch: (totalMatches: number) => void;
3030
clearSearch: () => void;
3131
isSearchActive: () => boolean;
3232
// Export functionality
@@ -123,13 +123,12 @@ export function useLogs(manager: ProcessManager, bufferSize = DEFAULT_BUFFER_SIZ
123123
);
124124
};
125125

126-
// Compute search matches
127-
const searchMatches = createMemo((): SearchMatch[] => {
126+
// Compute search matches for given logs
127+
const getSearchMatches = (logsToSearch: LogLine[]): SearchMatch[] => {
128128
const query = searchQuery();
129129
if (!query) return [];
130130

131131
const matches: SearchMatch[] = [];
132-
const allLogs = logs();
133132

134133
// Try as regex first, fall back to literal search
135134
let regex: RegExp;
@@ -140,7 +139,9 @@ export function useLogs(manager: ProcessManager, bufferSize = DEFAULT_BUFFER_SIZ
140139
regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
141140
}
142141

143-
allLogs.forEach((log, logIndex) => {
142+
logsToSearch.forEach((log, logIndex) => {
143+
// Reset regex lastIndex for each log line
144+
regex.lastIndex = 0;
144145
let match;
145146
while ((match = regex.exec(log.content)) !== null) {
146147
matches.push({
@@ -152,22 +153,18 @@ export function useLogs(manager: ProcessManager, bufferSize = DEFAULT_BUFFER_SIZ
152153
});
153154

154155
return matches;
155-
});
156+
};
156157

157158
const isSearchActive = () => searchQuery().length > 0;
158159

159-
const nextMatch = () => {
160-
const matches = searchMatches();
161-
if (matches.length === 0) return;
162-
163-
setCurrentMatchIndex((prev) => (prev + 1) % matches.length);
160+
const nextMatch = (totalMatches: number) => {
161+
if (totalMatches === 0) return;
162+
setCurrentMatchIndex((prev) => (prev + 1) % totalMatches);
164163
};
165164

166-
const prevMatch = () => {
167-
const matches = searchMatches();
168-
if (matches.length === 0) return;
169-
170-
setCurrentMatchIndex((prev) => (prev - 1 + matches.length) % matches.length);
165+
const prevMatch = (totalMatches: number) => {
166+
if (totalMatches === 0) return;
167+
setCurrentMatchIndex((prev) => (prev - 1 + totalMatches) % totalMatches);
171168
};
172169

173170
const clearSearch = () => {
@@ -218,7 +215,7 @@ export function useLogs(manager: ProcessManager, bufferSize = DEFAULT_BUFFER_SIZ
218215
// Search functionality
219216
searchQuery,
220217
setSearchQuery,
221-
searchMatches,
218+
getSearchMatches,
222219
currentMatchIndex,
223220
setCurrentMatchIndex,
224221
nextMatch,

0 commit comments

Comments
 (0)