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);
ImGui::SetNextItemWidth(filterW);
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)
ImGui::SameLine();
@@ -612,6 +618,13 @@ void ConsoleTab::renderOutput()
s_app_messages_enabled,
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
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 ||
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
outputFilter.errorsOnly;
@@ -628,6 +641,7 @@ void ConsoleTab::renderOutput()
}
}
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.
// Each segment records which bytes of the source text appear on that visual
@@ -881,6 +895,23 @@ void ConsoleTab::renderOutput()
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
if (seg.byteStart < seg.byteEnd) {
dl->AddText(font, fontSize,

View File

@@ -152,6 +152,7 @@ private:
// Output filter
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
// Wrapped line height caching (for variable-height text wrapping)