Skip to content

Commit 83b950c

Browse files
committed
Add regex mode, content/repo filter targets, cursor navigation and fix 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
1 parent a19cd89 commit 83b950c

File tree

17 files changed

+1162
-153
lines changed

17 files changed

+1162
-153
lines changed

CONTRIBUTING.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,24 @@ bun install
2121
github-code-search.ts # CLI entry point (Commander subcommands: query, upgrade)
2222
build.ts # Build script (compiles the standalone binary)
2323
src/
24-
types.ts # Shared TypeScript types
25-
api.ts # GitHub REST API client
24+
types.ts # Shared TypeScript types (TextMatchSegment, CodeMatch, RepoGroup, Row, FilterTarget, …)
25+
api.ts # GitHub REST API client (search, team listing)
26+
api-utils.ts # Shared retry (fetchWithRetry) and pagination (paginatedFetch) helpers
27+
api-utils.test.ts # Unit tests for api-utils.ts
28+
api.test.ts # Unit tests for api.ts
29+
cache.ts # Disk cache for the team list (24 h TTL)
30+
cache.test.ts # Unit tests for cache.ts
2631
aggregate.ts # Result grouping and filtering logic
2732
aggregate.test.ts # Unit tests for aggregate.ts
2833
render.ts # Façade: re-exports sub-modules + TUI renderGroups/renderHelpOverlay
2934
render.test.ts # Unit tests for render.ts (rows, filter, selection, rendering)
3035
render/
3136
highlight.ts # Syntax highlighting (language detection, token rules, highlightFragment)
3237
highlight.test.ts # Unit tests for highlight.ts (per-language tokenizer coverage)
33-
filter.ts # Filter helpers (FilterStats, buildFilterStats)
34-
rows.ts # Row navigation (buildRows, rowTerminalLines, isCursorVisible)
38+
filter.ts # Filter stats (FilterStats, buildFilterStats)
39+
filter-match.ts # Pure pattern matchers (makePatternTest, makeExtractMatcher, makeRepoMatcher)
40+
filter-match.test.ts # Unit tests for filter-match.ts
41+
rows.ts # Row builder (buildRows, rowTerminalLines, isCursorVisible)
3542
summary.ts # Stats labels (buildSummary, buildSummaryFull, buildSelectionSummary)
3643
selection.ts # Selection mutations (applySelectAll, applySelectNone)
3744
output.ts # Text (markdown) and JSON output formatters

demo/demo.gif

145 KB
Loading

demo/demo.tape

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ Down
4141
Sleep 300ms
4242
Down
4343
Sleep 300ms
44-
Down
45-
Sleep 300ms
4644

4745
# ── Toggle selection off on current extract ───────────────────────────────────
4846
Space
4947
Sleep 400ms
5048

51-
# ── Enter filter mode to exclude .ts files ────────────────────────────────────
49+
# ── Filter by file path (default target) ──────────────────────────────────────
5250
Type "f"
5351
Sleep 300ms
5452
Type ".yml"
@@ -64,7 +62,28 @@ Sleep 500ms
6462
Type "r"
6563
Sleep 600ms
6664

67-
# ── Confirm and print output ──────────────────────────────────────────────────
65+
# ── Switch to repo filter mode and filter by repo name ────────────────────────
66+
# Press t twice: path → content → repo
67+
Type "t"
68+
Sleep 300ms
69+
Type "t"
70+
Sleep 400ms
71+
72+
# Enter filter mode and type a repo name fragment
73+
Type "f"
74+
Sleep 300ms
75+
Type "toolkit"
76+
Sleep 700ms
77+
Enter
78+
Sleep 500ms
79+
80+
# Select all repos matching the filter
81+
Type "a"
82+
Sleep 500ms
83+
84+
# ── Reset and confirm ─────────────────────────────────────────────────────────
85+
Type "r"
86+
Sleep 600ms
6887
Enter
6988

7089
Sleep 2s

docs/architecture/components.md

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ C4Component
4141

4242
## 3b — TUI render layer
4343

44-
The six pure render functions called by the TUI on every redraw. All six live in
44+
The seven pure render functions called by the TUI on every redraw. All seven live in
4545
`src/render/` and are re-exported through the `src/render.ts` façade.
4646

4747
```mermaid
@@ -56,10 +56,11 @@ C4Component
5656
Container_Boundary(render, "src/render/ — pure functions") {
5757
Component(rows, "Row builder", "src/render/rows.ts", "buildRows()<br/>rowTerminalLines()<br/>isCursorVisible()")
5858
Component(summary, "Summary builder", "src/render/summary.ts", "buildSummary()<br/>buildSummaryFull()<br/>buildSelectionSummary()")
59-
Component(filter, "Filter stats", "src/render/filter.ts", "buildFilterStats()<br/>visible vs total rows")
59+
Component(filter, "Filter stats", "src/render/filter.ts", "buildFilterStats()<br/>FilterStats — visible/hidden counts")
6060
Component(selection, "Selection helpers", "src/render/selection.ts", "applySelectAll()<br/>applySelectNone()")
6161
Component(highlight, "Syntax highlighter", "src/render/highlight.ts", "highlightFragment()<br/>ANSI token colouring")
6262
Component(outputFn, "Output formatter", "src/output.ts", "buildOutput()<br/>markdown or JSON")
63+
Component(filterMatch, "Pattern matchers", "src/render/filter-match.ts", "makePatternTest()<br/>makeExtractMatcher()<br/>makeRepoMatcher()")
6364
}
6465
6566
Rel(tui, rows, "Build terminal<br/>rows")
@@ -80,31 +81,42 @@ C4Component
8081
Rel(tui, outputFn, "Format<br/>on Enter")
8182
UpdateRelStyle(tui, outputFn, $offsetX="17", $offsetY="160")
8283
84+
Rel(rows, filterMatch, "Uses pattern<br/>matchers")
85+
UpdateRelStyle(rows, filterMatch, $offsetX="-5", $offsetY="-5")
86+
87+
Rel(filter, filterMatch, "Uses pattern<br/>matchers")
88+
UpdateRelStyle(filter, filterMatch, $offsetX="45", $offsetY="-5")
89+
90+
Rel(selection, filterMatch, "Uses pattern<br/>matchers")
91+
UpdateRelStyle(selection, filterMatch, $offsetX="165", $offsetY="-25")
92+
8393
UpdateElementStyle(tui, $bgColor="#FFCC33", $borderColor="#0000CC", $fontColor="#000000")
8494
UpdateElementStyle(rows, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
8595
UpdateElementStyle(summary, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
8696
UpdateElementStyle(filter, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
97+
UpdateElementStyle(filterMatch, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
8798
UpdateElementStyle(selection, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
8899
UpdateElementStyle(highlight, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
89100
UpdateElementStyle(outputFn, $bgColor="#9933FF", $borderColor="#0000CC", $fontColor="#ffffff")
90101
```
91102

92103
## Component descriptions
93104

94-
| Component | Source file | Key exports |
95-
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96-
| **Filter & aggregation** | `src/aggregate.ts` | `aggregate()` — filters `CodeMatch[]` by repository and extract exclusion lists; normalises both `repoName` and `org/repoName` forms. |
97-
| **Team grouping** | `src/group.ts` | `groupByTeamPrefix()` — groups `RepoGroup[]` into `TeamSection[]` keyed by team slug; `flattenTeamSections()` — converts back to a flat list for the TUI row builder. |
98-
| **Row builder** | `src/render/rows.ts` | `buildRows()` — converts `RepoGroup[]` into `Row[]` with expanded/collapsed state; `rowTerminalLines()` — measures wrapped height; `isCursorVisible()` — viewport clipping. |
99-
| **Summary builder** | `src/render/summary.ts` | `buildSummary()` — compact header line; `buildSummaryFull()` — detailed counts; `buildSelectionSummary()` — "N files selected" footer. |
100-
| **Filter stats** | `src/render/filter.ts` | `buildFilterStats()` — produces the `FilterStats` object (visible count, total count, active filter string) used by the TUI status bar. |
101-
| **Selection helpers** | `src/render/selection.ts` | `applySelectAll()` — marks all visible rows as selected; `applySelectNone()` — deselects all. |
102-
| **Syntax highlighter** | `src/render/highlight.ts` | `highlightFragment()` — maps file extension to a language token ruleset and applies ANSI escape sequences. Falls back to plain text for unknown extensions. |
103-
| **Output formatter** | `src/output.ts` | `buildOutput()` — entry point for both `--format markdown` and `--format json` serialisation of the confirmed selection. |
105+
| Component | Source file | Key exports |
106+
| ------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107+
| **Filter & aggregation** | `src/aggregate.ts` | `aggregate()` — filters `CodeMatch[]` by repository and extract exclusion lists; normalises both `repoName` and `org/repoName` forms. |
108+
| **Team grouping** | `src/group.ts` | `groupByTeamPrefix()` — groups `RepoGroup[]` into `TeamSection[]` keyed by team slug; `flattenTeamSections()` — converts back to a flat list for the TUI row builder. |
109+
| **Row builder** | `src/render/rows.ts` | `buildRows()` — converts `RepoGroup[]` into `Row[]` filtered by the active target (path / content / repo); `rowTerminalLines()` — measures wrapped height; `isCursorVisible()` — viewport clipping. |
110+
| **Summary builder** | `src/render/summary.ts` | `buildSummary()` — compact header line; `buildSummaryFull()` — detailed counts; `buildSelectionSummary()` — "N files selected" footer. |
111+
| **Filter stats** | `src/render/filter.ts` | `buildFilterStats()` — produces the `FilterStats` object (visible repos, files, matches) used by the TUI filter bar live counter. |
112+
| **Pattern matchers** | `src/render/filter-match.ts` | `makePatternTest()` — builds a case-insensitive substring or RegExp test function; `makeExtractMatcher()` — wraps it for path or content targets; `makeRepoMatcher()` — wraps it for repo-name matching. |
113+
| **Selection helpers** | `src/render/selection.ts` | `applySelectAll()` — marks all visible rows as selected (respects filter target); `applySelectNone()` — deselects all visible rows. |
114+
| **Syntax highlighter** | `src/render/highlight.ts` | `highlightFragment()` — maps file extension to a language token ruleset and applies ANSI escape sequences. Falls back to plain text for unknown extensions. |
115+
| **Output formatter** | `src/output.ts` | `buildOutput()` — entry point for both `--format markdown` and `--format json` serialisation of the confirmed selection. |
104116

105117
## Design principles
106118

107119
- **No I/O.** Every component in this layer is a pure function: given the same inputs it always returns the same outputs. This makes them straightforward to test with Bun's built-in test runner.
108120
- **Single responsibility.** Each component owns exactly one concern (rows, summary, selection, …). The TUI composes them at render time rather than duplicating logic.
109-
- **`types.ts` as the contract.** All components share the interfaces defined in `src/types.ts` (`TextMatchSegment`, `TextMatch`, `CodeMatch`, `RepoGroup`, `Row`, `TeamSection`, `OutputFormat`, `OutputType`). Changes to these types require updating all components.
121+
- **`types.ts` as the contract.** All components share the interfaces defined in `src/types.ts` (`TextMatchSegment`, `TextMatch`, `CodeMatch`, `RepoGroup`, `Row`, `TeamSection`, `OutputFormat`, `OutputType`, `FilterTarget`). Changes to these types require updating all components.
110122
- **`render.ts` as façade.** External consumers import from `src/render.ts`, which re-exports all symbols from the `src/render/` sub-modules plus the top-level `renderGroups()` and `renderHelpOverlay()` functions.

docs/reference/keyboard-shortcuts.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,40 @@ Section header rows (shown when `--group-by-team-prefix` is active) are skipped
2323

2424
## Filtering
2525

26-
| Key | Action |
27-
| --- | -------------------------------------------------------------------------------------- |
28-
| `f` | Open the filter bar — type a path substring to narrow visible files (case-insensitive) |
29-
| `r` | Reset the active filter and show all repos / extracts |
26+
| Key | Action |
27+
| --- | -------------------------------------------------------------------------------------------------------- |
28+
| `f` | Open the filter bar and enter filter mode |
29+
| `t` | Cycle the **filter target**: `path``content``repo``path`. Works outside and inside filter mode. |
30+
| `r` | Reset the active filter and return to showing all repos / extracts |
31+
32+
### Filter targets
33+
34+
| Target | What is matched | Shown / hidden unit |
35+
| --------- | ----------------------------------------------------------------------------- | ---------------------- |
36+
| `path` | File path substring (default). Case-insensitive. | Individual extracts |
37+
| `content` | Code fragment text (the snippet returned by GitHub Search). Case-insensitive. | Individual extracts |
38+
| `repo` | Repository full name (`org/repo`). Case-insensitive. | Entire repo + extracts |
39+
40+
The active target is always shown in the filter bar badge, e.g. `[content]` or `[repo]`.
3041

3142
### Filter mode bindings
3243

3344
When the filter bar is open (after pressing `f`):
3445

35-
| Key | Action |
36-
| -------------------- | -------------------------------------------- |
37-
| Printable characters | Append character to the filter term |
38-
| `Backspace` | Delete the last character of the filter term |
39-
| `Enter` | Confirm the filter and apply it |
40-
| `Esc` | Cancel without applying the filter |
46+
| Key | Action |
47+
| --------------------------------------- | ------------------------------------------------------- |
48+
| Printable characters / paste | Insert character(s) at the cursor position |
49+
| `` / `` | Move the text cursor one character left / right |
50+
| `⌥←` / `⌥→` (macOS) · `Alt+←` / `Alt+→` | Jump one word left / right |
51+
| `Backspace` | Delete the character before the cursor |
52+
| `⌥⌫` (macOS) · `Ctrl+W` | Delete the word before the cursor |
53+
| `Tab` | Toggle **regex** mode (badge shows `[…·regex]` when on) |
54+
| `Enter` | Confirm the filter and apply it |
55+
| `Esc` | Cancel without applying the filter |
56+
57+
::: tip
58+
Invalid regex patterns are silently ignored (no results hidden, no crash). The badge turns yellow when a valid regex is active.
59+
:::
4160

4261
## Help and exit
4362

docs/usage/filtering.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,28 @@ github-code-search "useFeatureFlag" --org fulll \
7575
--exclude-extracts billing-api:src/flags.ts:0
7676
```
7777

78-
## In-TUI filtering (file path filter)
78+
## In-TUI filtering
7979

80-
In addition to pre-query exclusions, the [interactive mode](/usage/interactive-mode) offers a live **path filter** (press `f`) to narrow the displayed extracts by file path substring without permanently excluding anything.
80+
In addition to pre-query exclusions, the [interactive mode](/usage/interactive-mode) offers a live **filter bar** (press `f`) to narrow the displayed results without permanently excluding anything.
8181

82-
Use the path filter for **exploration** (no side effects), and `--exclude-extracts` / `--exclude-repositories` for **reproducible** exclusions in replay commands.
82+
### Three filter targets
83+
84+
Press `t` to cycle between matching modes:
85+
86+
| Target | Filters on | Unit visible/hidden |
87+
| --------- | ---------------------------------------------------------- | ------------------- |
88+
| `path` | File path substring (default, case-insensitive) | Individual file |
89+
| `content` | Code fragment returned by GitHub Search (case-insensitive) | Individual file |
90+
| `repo` | Full repository name `org/repo` (case-insensitive) | Entire repo |
91+
92+
With `repo` mode the matching portion of the repository name is highlighted in yellow in the result list.
93+
94+
### Regex mode
95+
96+
Press `Tab` inside the filter bar to enable regex matching. The badge updates to `[path·regex]` (or `[content·regex]`, etc.). Invalid expressions are silently ignored.
97+
98+
### Filter vs. exclusions: when to use which
99+
100+
Use the **TUI filter** for **exploration** — it has no side effects and can be reset instantly with `r`.
101+
102+
Use `--exclude-repositories` / `--exclude-extracts` for **reproducible** exclusions that should be encoded in the replay command (e.g. for CI pipelines).

0 commit comments

Comments
 (0)