Skip to content

Commit 9d622dc

Browse files
committed
Add query title heading and matched token to output
- buildMarkdownOutput now prepends '# Results for "query"' (H1) with optional qualifiers (· including archived · excluding templates). In regex mode the query is displayed in backticks. - Each match line appended with the exact matched token in backticks when TextMatchSegment.text is available (e.g. \`: \`useFlag\`\`). - buildJsonOutput adds matchedText field to each match entry alongside the existing line/col fields (omitted when location is unavailable). - repo-only mode also gets the H1 heading before the repo list. - New exported buildQueryTitle(query, options) in src/output.ts. - 78 tests pass (new describe + updated snapshots in output.test.ts). - README.md and docs updated to reflect new output format. Closes #126
1 parent 9c779fa commit 9d622dc

File tree

5 files changed

+140
-16
lines changed

5 files changed

+140
-16
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ github-code-search query "TODO" --org my-org
3636
- **Per-repository aggregation** — results grouped by repo, not as a flat list; fold/unfold each repo to focus on what matters
3737
- **Keyboard-driven TUI** — navigate with arrow keys, toggle selections, filter by file path, confirm with Enter — without leaving the terminal
3838
- **Fine-grained selection** — pick exactly the repos and extracts you want; deselected items are recorded as exclusions in the replay command
39-
- **Structured output**clean Markdown lists with GitHub links, or machine-readable JSON — ready to paste into docs, issues or scripts
39+
- **Structured output** — Markdown document with a `# Results for` query heading, GitHub deeplinks and the exact matched token per extract; or machine-readable JSON with `matchedText`, `line` and `col` fields — ready to paste into docs, issues or scripts
4040
- **Team-prefix grouping** — group results by team prefix (e.g. `platform/`, `data/`) using `--group-by-team-prefix`
4141
- **Replay command** — every session produces a one-liner you can run in CI to reproduce the exact same selection without the UI
42+
- **Regex search** — use `/pattern/flags` syntax for pattern-based searches; the CLI derives a safe API query and filters results locally
4243
- **Syntax highlighting** — code fragments rendered with language-aware coloring (TypeScript, Python, Go, Rust, YAML, JSON and more)
4344

4445
## Use cases
@@ -111,6 +112,7 @@ The official [`gh` CLI](https://cli.github.com/) does support `gh search code`,
111112
| Markdown / JSON output |||
112113
| Replay / CI command |||
113114
| Team-prefix grouping |||
115+
| Regex search |||
114116
| Syntax highlighting in terminal |||
115117
| Pagination (up to 1 000 results) |||
116118

docs/usage/non-interactive-mode.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@ $ CI=true github-code-search "useFeatureFlag" --org fulll
3737
```
3838

3939
```text
40+
# Results for "useFeatureFlag"
41+
4042
3 repos · 5 files selected
4143
4244
- **fulll/auth-service** (2 matches)
43-
- [ ] [src/middlewares/featureFlags.ts:2:19](https://github.com/fulll/auth-service/blob/main/src/middlewares/featureFlags.ts#L2)
44-
- [ ] [tests/unit/featureFlags.test.ts:1:8](https://github.com/fulll/auth-service/blob/main/tests/unit/featureFlags.test.ts#L1)
45+
- [ ] [src/middlewares/featureFlags.ts:2:19](https://github.com/fulll/auth-service/blob/main/src/middlewares/featureFlags.ts#L2): `useFeatureFlag`
46+
- [ ] [tests/unit/featureFlags.test.ts:1:8](https://github.com/fulll/auth-service/blob/main/tests/unit/featureFlags.test.ts#L1): `useFeatureFlag`
4547
- **fulll/billing-api** (2 matches)
46-
- [ ] [src/flags.ts:3:14](https://github.com/fulll/billing-api/blob/main/src/flags.ts#L3)
47-
- [ ] [src/routes/invoices.ts:1:1](https://github.com/fulll/billing-api/blob/main/src/routes/invoices.ts#L1)
48+
- [ ] [src/flags.ts:3:14](https://github.com/fulll/billing-api/blob/main/src/flags.ts#L3): `useFeatureFlag`
49+
- [ ] [src/routes/invoices.ts:1:1](https://github.com/fulll/billing-api/blob/main/src/routes/invoices.ts#L1): `useFeatureFlag`
4850
- **fulll/frontend-app** (1 match)
49-
- [ ] [src/hooks/useFeatureFlag.ts:1:1](https://github.com/fulll/frontend-app/blob/main/src/hooks/useFeatureFlag.ts#L1)
51+
- [ ] [src/hooks/useFeatureFlag.ts:1:1](https://github.com/fulll/frontend-app/blob/main/src/hooks/useFeatureFlag.ts#L1): `useFeatureFlag`
5052
```
5153
5254
<details>

docs/usage/output-formats.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ github-code-search "useFeatureFlag" --org fulll --format markdown --no-interacti
1111
```
1212

1313
```text
14+
# Results for "useFeatureFlag"
15+
1416
3 repos · 4 files selected
1517
1618
- **fulll/auth-service** (2 matches)
17-
- [ ] [src/middlewares/featureFlags.ts:2:19](...)
18-
- [ ] [tests/unit/featureFlags.test.ts:1:8](...)
19+
- [ ] [src/middlewares/featureFlags.ts:2:19](...): `useFeatureFlag`
20+
- [ ] [tests/unit/featureFlags.test.ts:1:8](...): `useFeatureFlag`
1921
- **fulll/billing-api** (1 match)
20-
- [ ] [src/flags.ts:3:14](...)
22+
- [ ] [src/flags.ts:3:14](...): `useFeatureFlag`
2123
- **fulll/frontend-app** (1 match)
22-
- [ ] [src/hooks/useFeatureFlag.ts:1:1](...)
24+
- [ ] [src/hooks/useFeatureFlag.ts:1:1](...): `useFeatureFlag`
2325
```
2426

2527
::: details replay command
@@ -30,7 +32,21 @@ github-code-search "useFeatureFlag" --org fulll --no-interactive
3032

3133
:::
3234

33-
Each extract link points directly to the matching line on GitHub.
35+
Each extract link points directly to the matching line on GitHub. When the GitHub API returns the exact matched token, it is appended inline in backticks (e.g. `` : `useFeatureFlag` ``).
36+
37+
## Query title
38+
39+
Every output (both Markdown and repo-only) is prefixed with a `# Results for` heading that identifies the search query. When active qualifiers are present, they are appended after a `·` separator:
40+
41+
```text
42+
# Results for "useFeatureFlag" · including archived · excluding templates
43+
```
44+
45+
In [regex mode](/usage/search-syntax#regex-mode), the pattern is shown in backticks:
46+
47+
```text
48+
# Results for `/useFeatureFlag/i`
49+
```
3450

3551
## `--format json`
3652

@@ -53,7 +69,8 @@ github-code-search "useFeatureFlag" --org fulll --format json --no-interactive
5369
"path": "src/middlewares/featureFlags.ts",
5470
"url": "https://github.com/fulll/auth-service/blob/main/src/middlewares/featureFlags.ts#L2",
5571
"line": 2,
56-
"col": 19
72+
"col": 19,
73+
"matchedText": "useFeatureFlag"
5774
}
5875
]
5976
}
@@ -83,6 +100,8 @@ github-code-search "useFeatureFlag" --org fulll \
83100
```
84101

85102
```text
103+
# Results for "useFeatureFlag"
104+
86105
fulll/auth-service
87106
fulll/billing-api
88107
fulll/frontend-app

src/output.test.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
buildJsonOutput,
55
buildMarkdownOutput,
66
buildOutput,
7+
buildQueryTitle,
78
buildReplayCommand,
89
buildReplayDetails,
910
shortExtractRef,
@@ -266,6 +267,40 @@ describe("buildReplayDetails", () => {
266267
});
267268
});
268269

270+
describe("buildQueryTitle", () => {
271+
it("wraps a plain query in double quotes", () => {
272+
expect(buildQueryTitle("useFlag")).toBe('# Results for "useFlag"');
273+
});
274+
275+
it("wraps a regex query in backticks", () => {
276+
expect(buildQueryTitle("/useFlag/i")).toBe("# Results for `/useFlag/i`");
277+
});
278+
279+
it("appends · including archived when includeArchived is true", () => {
280+
expect(buildQueryTitle("useFlag", { includeArchived: true })).toBe(
281+
'# Results for "useFlag" · including archived',
282+
);
283+
});
284+
285+
it("appends · excluding templates when excludeTemplates is true", () => {
286+
expect(buildQueryTitle("useFlag", { excludeTemplates: true })).toBe(
287+
'# Results for "useFlag" · excluding templates',
288+
);
289+
});
290+
291+
it("appends both qualifiers in order when both are set", () => {
292+
expect(buildQueryTitle("useFlag", { includeArchived: true, excludeTemplates: true })).toBe(
293+
'# Results for "useFlag" · including archived · excluding templates',
294+
);
295+
});
296+
297+
it("omits qualifiers when neither option is set", () => {
298+
expect(buildQueryTitle("useFlag", { includeArchived: false, excludeTemplates: false })).toBe(
299+
'# Results for "useFlag"',
300+
);
301+
});
302+
});
303+
269304
describe("buildMarkdownOutput", () => {
270305
it("includes selected repo and file link", () => {
271306
const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])];
@@ -274,6 +309,12 @@ describe("buildMarkdownOutput", () => {
274309
expect(out).toContain("[src/foo.ts](https://github.com/myorg/repoA/blob/main/src/foo.ts)");
275310
});
276311

312+
it("starts with # Results for query H1 heading", () => {
313+
const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])];
314+
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set());
315+
expect(out.split("\n")[0]).toBe(`# Results for "${QUERY}"`);
316+
});
317+
277318
it("renders repo as bold bullet", () => {
278319
const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])];
279320
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set());
@@ -388,12 +429,24 @@ describe("buildMarkdownOutput", () => {
388429
expect(out).toBe("");
389430
});
390431

432+
it("repo-only mode prepends H1 query title before repo list", () => {
433+
const groups = [makeGroup("myorg/repoA", ["src/a.ts"])];
434+
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set(), "repo-only");
435+
expect(out).toContain(`# Results for "${QUERY}"`);
436+
const lines = out.split("\n");
437+
expect(lines[0]).toBe(`# Results for "${QUERY}"`);
438+
expect(out.indexOf(`# Results for "${QUERY}"`)).toBeLessThan(out.indexOf("myorg/repoA"));
439+
});
440+
391441
it("prepends selection summary line in repo-and-matches mode", () => {
392442
const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])];
393443
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set());
394444
expect(out).toContain("selected");
395-
const firstLine = out.split("\n")[0];
396-
expect(firstLine).toContain("selected");
445+
const lines = out.split("\n");
446+
// first line is the H1 query title
447+
expect(lines[0]).toMatch(/^# Results for /);
448+
// third line (index 2) is the selection summary
449+
expect(lines[2]).toContain("selected");
397450
});
398451

399452
it("does not prepend selection summary in repo-only mode", () => {
@@ -502,6 +555,13 @@ describe("buildMarkdownOutput — line/col annotation", () => {
502555
expect(out).not.toMatch(/\[src\/foo\.ts:\d+:\d+\]/);
503556
});
504557

558+
it("appends matched token in backticks when location is available", () => {
559+
const groups = [makeGroupWithMatches("myorg/repoA", [{ path: "src/foo.ts", line: 3, col: 5 }])];
560+
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set());
561+
expect(out).toContain("`snippet`");
562+
expect(out).toContain("#L3): `snippet`");
563+
});
564+
505565
it("does not add location suffix in repo-only mode", () => {
506566
const groups = [makeGroupWithMatches("myorg/repoA", [{ path: "src/foo.ts", line: 3, col: 5 }])];
507567
const out = buildMarkdownOutput(groups, QUERY, ORG, new Set(), new Set(), "repo-only");
@@ -525,6 +585,18 @@ describe("buildJsonOutput — line/col fields", () => {
525585
expect(parsed.results[0].matches[0].line).toBeUndefined();
526586
expect(parsed.results[0].matches[0].col).toBeUndefined();
527587
});
588+
589+
it("includes matchedText in match when text match is present", () => {
590+
const groups = [makeGroupWithMatches("myorg/repoA", [{ path: "src/foo.ts", line: 2, col: 8 }])];
591+
const parsed = JSON.parse(buildJsonOutput(groups, QUERY, ORG, new Set(), new Set()));
592+
expect(parsed.results[0].matches[0].matchedText).toBe("snippet");
593+
});
594+
595+
it("omits matchedText when no text matches", () => {
596+
const groups = [makeGroup("myorg/repoA", ["src/foo.ts"])];
597+
const parsed = JSON.parse(buildJsonOutput(groups, QUERY, ORG, new Set(), new Set()));
598+
expect(parsed.results[0].matches[0].matchedText).toBeUndefined();
599+
});
528600
});
529601

530602
// ─── buildOutput dispatcher ──────────────────────────────────────────────────

src/output.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { buildSelectionSummary } from "./render.ts";
2+
import { isRegexQuery } from "./regex.ts";
23
import type { OutputFormat, OutputType, RepoGroup } from "./types.ts";
34

45
// ─── Short-form helpers ───────────────────────────────────────────────────────
@@ -150,6 +151,26 @@ export function buildReplayDetails(
150151
].join("\n");
151152
}
152153

154+
// ─── Query title ─────────────────────────────────────────────────────────────
155+
156+
/**
157+
* Builds a first-level heading that identifies the query and any active
158+
* qualifiers (e.g. `--include-archived`, `--exclude-template-repositories`).
159+
*
160+
* Examples:
161+
* # Results for "useFlag"
162+
* # Results for `useFlag/i`
163+
* # Results for "axios" · including archived · excluding templates
164+
*/
165+
export function buildQueryTitle(query: string, options: ReplayOptions = {}): string {
166+
const queryDisplay = isRegexQuery(query) ? `\`${query}\`` : `"${query}"`;
167+
const qualifiers: string[] = [];
168+
if (options.includeArchived) qualifiers.push("including archived");
169+
if (options.excludeTemplates) qualifiers.push("excluding templates");
170+
const suffix = qualifiers.length > 0 ? ` · ${qualifiers.join(" · ")}` : "";
171+
return `# Results for ${queryDisplay}${suffix}`;
172+
}
173+
153174
// ─── Selected matches helper ─────────────────────────────────────────────────
154175

155176
function selectedMatches(group: RepoGroup) {
@@ -174,6 +195,8 @@ export function buildMarkdownOutput(
174195
.map((g) => g.repoFullName);
175196
if (repos.length === 0) return "";
176197
return (
198+
buildQueryTitle(query, options) +
199+
"\n\n" +
177200
repos.join("\n") +
178201
"\n\n" +
179202
buildReplayDetails(groups, query, org, excludedRepos, excludedExtractRefs, options) +
@@ -183,6 +206,8 @@ export function buildMarkdownOutput(
183206

184207
const lines: string[] = [];
185208

209+
lines.push(buildQueryTitle(query, options));
210+
lines.push("");
186211
lines.push(buildSelectionSummary(groups));
187212
lines.push("");
188213

@@ -193,6 +218,7 @@ export function buildMarkdownOutput(
193218

194219
// Section header (emitted before the first repo in a new team section)
195220
if (group.sectionLabel !== undefined) {
221+
lines.push("");
196222
lines.push(`## ${group.sectionLabel}`);
197223
lines.push("");
198224
}
@@ -206,7 +232,10 @@ export function buildMarkdownOutput(
206232
// absolute line numbers).
207233
const seg = m.textMatches[0]?.matches[0];
208234
if (seg) {
209-
lines.push(` - [ ] [${m.path}:${seg.line}:${seg.col}](${m.htmlUrl}#L${seg.line})`);
235+
const matchedText = seg.text ? `: \`${seg.text}\`` : "";
236+
lines.push(
237+
` - [ ] [${m.path}:${seg.line}:${seg.col}](${m.htmlUrl}#L${seg.line})${matchedText}`,
238+
);
210239
} else {
211240
lines.push(` - [ ] [${m.path}](${m.htmlUrl})`);
212241
}
@@ -240,7 +269,7 @@ export function buildJsonOutput(
240269
return {
241270
path: m.path,
242271
url: m.htmlUrl,
243-
...(seg !== undefined ? { line: seg.line, col: seg.col } : {}),
272+
...(seg !== undefined ? { line: seg.line, col: seg.col, matchedText: seg.text } : {}),
244273
};
245274
});
246275
return { ...base, matches };

0 commit comments

Comments
 (0)