Skip to content

feat: advanced filter — regex mode, content/repo targets, cursor navigation + scroll fixes (v1.5.0)#65

Merged
shouze merged 6 commits intomainfrom
feat/advanced-filter-targets-and-scroll-fixes
Mar 1, 2026
Merged

feat: advanced filter — regex mode, content/repo targets, cursor navigation + scroll fixes (v1.5.0)#65
shouze merged 6 commits intomainfrom
feat/advanced-filter-targets-and-scroll-fixes

Conversation

@shouze
Copy link
Contributor

@shouze shouze commented Mar 1, 2026

Closes #64


Motivation

The filter bar only matched file paths as plain substrings. Three high-priority roadmap items (#63) called for regex support, fragment-text filtering, and repo-name filtering. Two scroll bugs also blocked navigation to the last repo / its extracts after unfolding.


What changed

🔍 Advanced filter targets

A new FilterTarget type ("path" | "content" | "repo") controls what the filter bar matches against. Press t outside the filter bar — or Shift+Tab inside the filter bar — to cycle through targets; the active target is shown as a badge in the bar.

Target Matches Hidden unit
path (default) File path Individual extract
content Code fragment text Individual extract
repo Repository full name (org/repo) Entire repo + extracts

🔎 Regex mode

Press Tab inside the filter bar to toggle regex mode. A [regex] badge appears. Invalid patterns are handled gracefully — no crash, zero results shown.

⌨️ Filter bar text cursor

The input now behaves like a real text field:

  • / — move cursor
  • ⌥← / ⌥→ (macOS) · Ctrl+← / Ctrl+→ · Alt+b / Alt+f — jump word
  • Backspace — delete char before cursor
  • ⌥⌫ / Ctrl+W — delete word before cursor
  • Paste support (multi-char insert at cursor)

🐛 Scroll bug fixes

isCursorVisible returned true too early — it found the cursor row but didn't check whether its height (h) fit in remaining viewport space. Multi-line extract rows (h=2) were declared visible even when renderGroups would have broken before rendering them. Fix: apply the same condition as renderGroups: usedLines === 0 || usedLines + h <= viewportHeight.

viewportHeight was static — now computed dynamically via getViewportHeight() to account for filter bar height (0–2 extra lines) and the sticky repo header (+1 line when scrolled past the top), so isCursorVisible always uses the same window as renderGroups.


How to test manually

bun github-code-search.ts query --org <your-org> <query>
  1. Path filter (default): press f, type a path substring → only matching extracts visible.
  2. Content filter: press t until [content], press f, type a code keyword → only extracts containing the keyword.
  3. Repo filter: press t until [repo], press f, type a repo name → only matching repos visible.
  4. Regex mode: inside filter bar, press Tab[regex] badge; type .*service.* → regex applied.
  5. Scroll to last repo: unfold the last repo (→) → press ↓ to reach its extracts — should work now.
  6. Second-to-last unfolded: unfold second-to-last repo → press ↓ past it → last repo is reachable.

Validation

bun test               ✅ 398 pass, 0 fail
bun run lint           ✅ 0 errors
bun run format:check   ✅ clean
bun run knip           ✅ no unused exports

…x scroll bugs

- New FilterTarget type (path | content | repo), cycled with 't'
- Regex mode toggled with Tab in filter bar (invalid patterns → no-match)
- src/render/filter-match.ts: makeExtractMatcher, makeRepoMatcher helpers
- Filter bar text cursor: ←/→, ⌥←/⌥→ (word), Backspace, ⌥⌫/Ctrl+W
- Fix isCursorVisible: check usedLines + h <= viewportHeight before returning true
- Fix viewportHeight off-by-one: termHeight - 6 (HEADER_LINES 4 + indicator 2)
- Expand applySelectAll / applySelectNone to respect new filter targets
- Update docs: keyboard-shortcuts, filtering, interactive-mode
- Bump version to 1.5.0

Closes #64
Copilot AI review requested due to automatic review settings March 1, 2026 21:18
@shouze shouze self-assigned this Mar 1, 2026
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

96.69%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts97.25%100%100%97.18%109, 269–270, 313–316, 72
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts98.75%100%100%98.65%43
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

@shouze shouze mentioned this pull request Mar 1, 2026
13 tasks
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an advanced, multi-target filter system to the interactive TUI (path/content/repo) with optional regex matching, improves filter-bar text-editing behavior, and fixes scroll/viewport edge cases so the last repo/extracts remain reachable.

Changes:

  • Introduce FilterTarget (path | content | repo) plus regex matching, shared matcher utilities, and updated row building/selection behavior to respect the active target.
  • Implement a real filter input cursor model (left/right, word jumps, delete word, paste) and render updated filter bar UI (badges + live stats).
  • Fix scroll visibility logic for multi-line rows and correct viewport height computation; update tests/docs/demo accordingly; bump version to 1.5.0.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/types.ts Adds FilterTarget type used across TUI/filter logic.
src/tui.ts Implements target cycling, regex toggle, filter cursor editing, debounced live stats, and viewport height fix.
src/render/filter-match.ts New pure matcher helpers for substring/regex + target-specific matching.
src/render/filter-match.test.ts New unit tests for matcher behavior (substring/regex/invalid regex).
src/render/rows.ts Extends buildRows to support target+regex; fixes isCursorVisible for multi-line row height.
src/render/filter.ts Extends buildFilterStats for target+regex; adds visibleFiles for live stats.
src/render/selection.ts Updates select-all/none to respect filter target + regex.
src/render.ts Updates filter bar rendering (badges, live stats, cursor rendering) and in-list highlighting.
src/render.test.ts Adds/updates tests for new filtering modes, badges, cursor rendering, stats, and scroll regression.
package.json Version bump to 1.5.0.
docs/usage/interactive-mode.md Documents new filter bar behavior, targets, regex mode, and selection semantics under filtering.
docs/usage/filtering.md Updates filtering docs to cover targets + regex mode.
docs/reference/keyboard-shortcuts.md Adds new keybindings for targets, cursor navigation, and regex toggle.
docs/architecture/components.md Updates render-layer diagram/docs to include new matcher component.
demo/demo.tape Updates demo script to exercise new filter behaviors (path + repo filtering).
CONTRIBUTING.md Updates repo structure documentation to include new render filter-matcher files.

- Fix: import TextMatchSegment in render.ts (was causing a TS error)
- Perf: replace applyTextHighlight with makeTextHighlighter factory compiled
  once per renderGroups call, avoiding regex recompilation on every row
- Perf: buildFilterStats now collects visibleFiles in the same pass as
  visibleRepos/visibleMatches, eliminating a second flatMap over all groups
  with freshly-allocated matchers
- Docs: fix regex tip in keyboard-shortcuts.md — invalid patterns match
  nothing (zero results), not silently ignored
- Docs: remove [path] badge from filter bar example in interactive-mode.md
  (renderer omits badge for default path target)
- Docs: fix regex tip in interactive-mode.md — matches nothing on bad pattern
- Docs: fix regex tip in filtering.md — matches nothing on bad pattern
- Docs: reword architecture/components.md section 3b — not all render-layer
  modules are pure render functions living in src/render/

See #65
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

96.67%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts96.88%100%90%97.12%108, 275–276, 319–322, 72
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts98.75%100%100%98.65%43
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

…th] badge

- tui.ts: replace 't' handler in filter mode with KEY_SHIFT_TAB (\x1b[Z) so
  the letter t can be freely typed in the filter input; normal mode 't' is
  preserved for convenience outside the filter bar
- render.ts: targetBadge is now always rendered ([path], [content], [repo],
  or their ·regex variants) — the active target is always explicit
- render.ts: update filter-mode hint line to reflect Shift+Tab for target
- render.ts: update help overlay filter-mode section likewise
- render.test.ts: flip the 'does not show [path] badge' test to assert that
  [path] IS now always shown
- docs(keyboard-shortcuts): 't' scoped to outside-filter-mode only; Shift+Tab
  added to filter-mode bindings table; badge description updated
- docs(interactive-mode): example shows [path] badge; table replaces _(none)_
  with [path]; cycling-key description updated
- docs(filtering): cycling-key description updated

See #65
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

96.66%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts96.83%100%90%97.08%108, 272–273, 316–319, 72
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts98.75%100%100%98.65%43
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

shouze added a commit that referenced this pull request Mar 1, 2026
- Add Alt+Arrow (\x1b[1;3D/C) key sequences as additional word-jump bindings
  in filter mode, covering terminals that send these for Option+←/→ instead
  of Alt+b/f (e.g. iTerm2/xterm with 'Use Option as Meta key')
- Update help overlay and keyboard-shortcuts.md to document all three
  word-jump variants (⌥←→, Ctrl+←→, Alt+b/Alt+f)
- Make viewportHeight dynamic: getViewportHeight() accounts for filterBarLines
  (0/1/2) so isCursorVisible uses the same viewport as renderGroups, fixing
  scroll stopping early when filter bar or filter badge is visible
- Fix extract cursor row: apply highlightText on path even when isCursor,
  consistent with the repo cursor row (search-match highlighting on magenta bg)
- Remove redundant UpdateElementStyle(filterMatch) from Mermaid diagram
- Bump package.json version to 1.5.0 with blog post and CHANGELOG entries

See PR #65
@shouze shouze requested a review from Copilot March 1, 2026 22:09
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

96.67%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts96.86%100%90.91%97.10%108, 272–273, 316–319, 72
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts98.75%100%100%98.65%43
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

- Add Alt+Arrow (\x1b[1;3D/C) key sequences as additional word-jump bindings
  in filter mode, covering terminals that send these for Option+←/→ instead
  of Alt+b/f (e.g. iTerm2/xterm with 'Use Option as Meta key')
- Update help overlay and keyboard-shortcuts.md to document all three
  word-jump variants (⌥←→, Ctrl+←→, Alt+b/Alt+f)
- Make viewportHeight dynamic: getViewportHeight() accounts for filterBarLines
  (0/1/2) so isCursorVisible uses the same viewport as renderGroups, fixing
  scroll stopping early when filter bar or filter badge is visible
- Fix extract cursor row: apply highlightText on path even when isCursor,
  consistent with the repo cursor row (search-match highlighting on magenta bg)
- Remove redundant UpdateElementStyle(filterMatch) from Mermaid diagram
- Bump package.json version to 1.5.0 with blog post and CHANGELOG entries

See PR #65
@shouze shouze force-pushed the feat/advanced-filter-targets-and-scroll-fixes branch from 9a97c5b to 3e09d80 Compare March 1, 2026 22:11
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

96.67%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts96.86%100%90.91%97.10%108, 272–273, 316–319, 72
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts98.75%100%100%98.65%43
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 6 comments.

- getViewportHeight: subtract stickyHeaderLines (scrollOffset > 0) to match
  the actual viewport renderGroups uses when the sticky repo header is shown
- buildFilterStats: key visibleFileSet by repoFullName+path to avoid collisions
  across repos that share the same relative path (e.g. src/index.ts)
- buildRows: propagate pending section label so remaining repos in a section
  still get their header even when the first repo of that section is filtered
  out — applies to both repo-target and path/content-target branches
- mergeSegments: new helper in render.ts that sorts and coalesces overlapping
  TextMatchSegments before passing to highlightFragment, preventing garbled
  output when GitHub query matches and content-filter matches overlap
- docs/blog/release-v1-5-0.md: fix filter-targets table (path is default, not
  content) and document t key (outside) vs Shift+Tab (inside filter bar)
- Update PR #65 description to reflect the actual t / Shift+Tab bindings

See PR #65
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

95.96%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts93.05%100%84.62%93.43%100, 102–103, 130, 294–295, 338–341, 72, 91–96, 98–99
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts97.78%100%100%97.62%49–50
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Tests added first (src/render.test.ts) as regression guards for the three
tui.ts-level state bugs — they exercise the pure functions involved and
document the correct behavior that the tui.ts fixes must preserve:

- isCursorVisible: stale scrollOffset > cursor (result of filter shrinking
  rows without clamping scrollOffset) always returns false, confirming the
  cursor is above the viewport and scrollOffset must be clamped
- buildRows + isCursorVisible: invalid regex after toggle produces 0 rows;
  cursor at former position is out-of-range and must be re-clamped to 0
  (Math.min(cursor, Math.max(0, rows.length - 1)))
- renderGroups: sticky repo header (\u25b2) is NOT shown when cursor is on a repo
  row even when scrollOffset > 0 (only shown on extract rows whose repo row
  has scrolled above the viewport)

Fixes (src/tui.ts):

1. stickyHeaderLines condition in getViewportHeight(): change
   \`scrollOffset > 0 ? 1 : 0\` to \`scrollOffset > 0 && cursor >= scrollOffset ? 1 : 0\`
   so the viewport is only shrunk by one when a sticky header will actually
   be rendered by renderGroups, preventing premature scroll-stop on repo rows.

2. scrollOffset not clamped after filter reduces rows: add
   \`scrollOffset = Math.min(scrollOffset, cursor)\` after cursor clamping in
   Ctrl+W/Alt+Backspace, Backspace, Del, and printable-char (paste) handlers.
   Without this, a previously-scrolled viewport could remain beyond the end
   of the shrunken row list, producing an empty groups area with no recovery.

3. Tab toggle (filterRegex) did not rebuild rows or clamp cursor/scrollOffset:
   after \`filterRegex = !filterRegex\`, now calls buildRows and applies the
   same clamping as all other filter-state-changing handlers.

See PR #65
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Coverage after merging feat/advanced-filter-targets-and-scroll-fixes into main will be

95.96%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   aggregate.ts100%100%100%100%
   api-utils.ts97.50%100%100%97.18%52, 60
   api.ts93.43%100%100%92.51%235–239, 295, 312, 63–69
   cache.ts98.08%100%100%97.87%28
   group.ts100%100%100%100%
   output.ts99.12%100%94.74%99.52%58
   render.ts93.05%100%84.62%93.43%100, 102–103, 130, 294–295, 338–341, 72, 91–96, 98–99
   upgrade.ts86.52%100%93.33%85.89%117–118, 138–145, 148–154, 159, 164, 200–203
src/render
   filter-match.ts97.44%100%92.31%100%
   filter.ts100%100%100%100%
   highlight.ts99.29%100%100%99.01%184–185
   rows.ts97.78%100%100%97.62%49–50
   selection.ts100%100%100%100%
   summary.ts100%100%100%100%

@shouze shouze merged commit 4248228 into main Mar 1, 2026
8 checks passed
@shouze shouze deleted the feat/advanced-filter-targets-and-scroll-fixes branch March 1, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: advanced filter — regex mode, content/repo targets, cursor navigation + scroll fixes

2 participants