feat(console): filter match highlight + match count

- Highlight the matched substring (case-insensitive) in every visible line with a
  translucent yellow behind the text, drawn per wrap-segment like the selection highlight.
- Show a live match count next to the filter input ("N matches", red when zero).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:34:18 -05:00
parent e0d6d6bc4d
commit 4ed69d4e9f
3 changed files with 33 additions and 0 deletions

View File

@@ -553,6 +553,12 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size); float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
ImGui::SetNextItemWidth(filterW); ImGui::SetNextItemWidth(filterW);
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_)); ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
if (filter_text_[0] != '\0') {
ImGui::SameLine(0, Layout::spacingSm());
std::string mc = std::to_string(filter_match_count_) + " " + TR("console_matches");
Type().textColored(TypeStyle::Caption,
filter_match_count_ > 0 ? OnSurfaceMedium() : Error(), mc.c_str());
}
// Zoom +/- buttons (right side of toolbar) // Zoom +/- buttons (right side of toolbar)
ImGui::SameLine(); ImGui::SameLine();
@@ -612,6 +618,13 @@ void ConsoleTab::renderOutput()
s_app_messages_enabled, s_app_messages_enabled,
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC}; COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
bool has_text_filter = !outputFilter.text.empty(); bool has_text_filter = !outputFilter.text.empty();
// Lowercased needle for in-line match highlighting.
std::string filter_lower;
if (has_text_filter) {
filter_lower = std::string(filter_text_);
std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
}
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled || bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled || !outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
outputFilter.errorsOnly; outputFilter.errorsOnly;
@@ -628,6 +641,7 @@ void ConsoleTab::renderOutput()
} }
} }
int visible_count = static_cast<int>(visible_indices_.size()); int visible_count = static_cast<int>(visible_indices_.size());
filter_match_count_ = has_text_filter ? visible_count : 0;
// Calculate wrapped heights AND build sub-row segments for each visible line. // Calculate wrapped heights AND build sub-row segments for each visible line.
// Each segment records which bytes of the source text appear on that visual // Each segment records which bytes of the source text appear on that visual
@@ -881,6 +895,23 @@ void ConsoleTab::renderOutput()
selColor); selColor);
} }
// Filter match highlight (case-insensitive) within this segment.
if (has_text_filter && !filter_lower.empty() && seg.byteStart < seg.byteEnd) {
std::string segLower(segStart, segEnd);
std::transform(segLower.begin(), segLower.end(), segLower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
size_t pos = 0;
while ((pos = segLower.find(filter_lower, pos)) != std::string::npos) {
float xs = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + static_cast<int>(pos)).x;
float xe = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart,
segStart + static_cast<int>(pos + filter_lower.size())).x;
dl->AddRectFilled(ImVec2(lineOrigin.x + xs, rowY),
ImVec2(lineOrigin.x + xe, rowY + seg.height),
IM_COL32(255, 210, 0, 70));
pos += filter_lower.size();
}
}
// Render text segment // Render text segment
if (seg.byteStart < seg.byteEnd) { if (seg.byteStart < seg.byteEnd) {
dl->AddText(font, fontSize, dl->AddText(font, fontSize,

View File

@@ -152,6 +152,7 @@ private:
// Output filter // Output filter
char filter_text_[128] = {0}; char filter_text_[128] = {0};
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
mutable std::vector<int> visible_indices_; // Cached for selection mapping mutable std::vector<int> visible_indices_; // Cached for selection mapping
// Wrapped line height caching (for variable-height text wrapping) // Wrapped line height caching (for variable-height text wrapping)

View File

@@ -983,6 +983,7 @@ void I18n::loadBuiltinEnglish()
strings_["console_help_stop"] = " stop - Stop the daemon"; strings_["console_help_stop"] = " stop - Stop the daemon";
strings_["console_line_count"] = "%zu lines"; strings_["console_line_count"] = "%zu lines";
strings_["console_new_lines"] = "new"; strings_["console_new_lines"] = "new";
strings_["console_matches"] = "matches";
strings_["console_new_lines"] = "%d new lines"; strings_["console_new_lines"] = "%d new lines";
strings_["console_no_daemon"] = "No daemon"; strings_["console_no_daemon"] = "No daemon";
strings_["console_not_connected"] = "Error: Not connected to daemon"; strings_["console_not_connected"] = "Error: Not connected to daemon";