Skip to content

Commit c6fcdba

Browse files
committed
Address PR #114 review: absolute line numbers, invalid-regex fatal error, completions --regex-hint, fix comment
1 parent ae8fa10 commit c6fcdba

5 files changed

Lines changed: 40 additions & 6 deletions

File tree

github-code-search.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ async function searchAction(
280280
let regexFilter: RegExp | undefined;
281281
if (isRegexQuery(query)) {
282282
const { apiQuery, regexFilter: rf, warn } = buildApiQuery(query);
283+
if (rf === null) {
284+
// Compile error — always fatal, even if --regex-hint is provided,
285+
// because no local regex filter can be applied.
286+
console.error(pc.yellow(`⚠ Regex mode — ${warn}`));
287+
process.exit(1);
288+
}
283289
if (warn && !opts.regexHint) {
284290
console.error(
285291
pc.yellow(`⚠ Regex mode — ${warn}\n Provide a manual hint with --regex-hint <term>.`),

src/aggregate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ describe("aggregate — regexFilter", () => {
241241

242242
it("recomputes segments to point at the actual regex match (not the API literal)", () => {
243243
// Simulate: regex /axios": "1\.12/, API literal "axios", API gives segment
244-
// at [8,13] (pointing at "axios" only). After aggregation the segment must
244+
// at [9,14] (pointing at "axios" only). After aggregation the segment must
245245
// cover the full regex match.
246246
//
247247
// Fragment offsets: d e p s : \n " a x i o s " : " 1 . 1 2 . 0 "

src/aggregate.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ export function extractRef(repoFullName: string, path: string, matchIndex: numbe
3434
* match — replacing the API-provided segments (which point at the literal
3535
* search term) with the actual regex match positions.
3636
*
37-
* Line (1-based) and col (1-based) are computed from the fragment text so
38-
* that `highlightFragment` can map them to the correct terminal lines.
37+
* `fragmentStartLine` is the 1-based absolute file line where the fragment
38+
* starts (derived from the existing API segments or falling back to 1). It is
39+
* used to produce absolute `line` values that match those stored by the API
40+
* path so that `output.ts` generates correct `#L{line}` GitHub anchors.
3941
*/
40-
function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[] {
42+
function recomputeSegments(
43+
fragment: string,
44+
regex: RegExp,
45+
fragmentStartLine: number,
46+
): TextMatchSegment[] {
4147
// Force global flag so exec() advances; strip g and y first to avoid double-g
4248
// and to prevent sticky mode from anchoring every match at lastIndex.
4349
const re = new RegExp(regex.source, regex.flags.replace(/[gy]/g, "") + "g");
@@ -60,7 +66,9 @@ function recomputeSegments(fragment: string, regex: RegExp): TextMatchSegment[]
6066
if (newlines[mid] < start) lo = mid + 1;
6167
else hi = mid;
6268
}
63-
const line = lo + 1; // 1-based
69+
// `lo` = number of fragment-local lines before `start` (0-based offset).
70+
// Add `fragmentStartLine - 1` to make it absolute.
71+
const line = fragmentStartLine + lo;
6472
const col = (lo === 0 ? start : start - newlines[lo - 1] - 1) + 1; // 1-based
6573
segments.push({ text: m[0], indices: [start, end], line, col });
6674
if (m[0].length === 0) re.lastIndex++; // guard against zero-width matches
@@ -91,7 +99,18 @@ export function aggregate(
9199
const savedLastIndex = regexFilter.lastIndex;
92100
const updatedTextMatches: TextMatch[] = m.textMatches
93101
.map((tm) => {
94-
const segs = recomputeSegments(tm.fragment, regexFilter);
102+
// Derive the absolute start line of this fragment from the first API
103+
// segment. If no API segment is available, fall back to 1 so that
104+
// recomputeSegments emits fragment-relative lines (which equal
105+
// absolute lines when the fragment starts at line 1).
106+
let fragmentStartLine = 1;
107+
const firstApiSeg = tm.matches[0];
108+
if (firstApiSeg) {
109+
const before = tm.fragment.slice(0, firstApiSeg.indices[0]);
110+
const fragLine = (before.match(/\n/g)?.length ?? 0) + 1;
111+
fragmentStartLine = firstApiSeg.line - fragLine + 1;
112+
}
113+
const segs = recomputeSegments(tm.fragment, regexFilter, fragmentStartLine);
95114
return segs.length > 0 ? { fragment: tm.fragment, matches: segs } : null;
96115
})
97116
.filter((tm): tm is TextMatch => tm !== null);

src/completions.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("generateCompletion", () => {
2828
expect(script).toContain("--format");
2929
expect(script).toContain("--output-type");
3030
expect(script).toContain("--no-interactive");
31+
expect(script).toContain("--regex-hint");
3132
});
3233

3334
it("contains format values (markdown, json)", () => {
@@ -71,6 +72,7 @@ describe("generateCompletion", () => {
7172
expect(script).toContain("--org");
7273
expect(script).toContain("--format");
7374
expect(script).toContain("--output-type");
75+
expect(script).toContain("--regex-hint");
7476
});
7577

7678
it("contains a 'compdef' directive (zsh-style)", () => {
@@ -102,6 +104,7 @@ describe("generateCompletion", () => {
102104
expect(script).toContain("org");
103105
expect(script).toContain("format");
104106
expect(script).toContain("output-type");
107+
expect(script).toContain("regex-hint");
105108
});
106109

107110
it("uses fish 'complete -c' syntax", () => {

src/completions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ const OPTIONS = [
6363
takesArg: false,
6464
values: [],
6565
},
66+
{
67+
flag: "regex-hint",
68+
description: "Override the API search term for regex queries",
69+
takesArg: true,
70+
values: [],
71+
},
6672
] as const;
6773

6874
// ─── Bash completion script ───────────────────────────────────────────────────

0 commit comments

Comments
 (0)