From c22d00829b1e8a0f8a63febe0d48cda084276c2f Mon Sep 17 00:00:00 2001 From: dan mackinlay Date: Sun, 5 Apr 2026 10:08:57 +0000 Subject: [PATCH] Add line to JSON search output --- CHANGELOG.md | 2 ++ src/cli/formatter.ts | 6 +++++- test/formatter.test.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e56c27..e534bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - Sync stale `bun.lock` (`better-sqlite3` 11.x → 12.x). CI and release script now use `--frozen-lockfile` to prevent recurrence. #386 (thanks @Mic92) +- Include `line` in `--json` search output so editor integrations can jump + directly to `file:line`. Closes #505 (thanks @danmackinlay) ## [2.0.1] - 2026-03-10 diff --git a/src/cli/formatter.ts b/src/cli/formatter.ts index 14586a8..81d69bb 100644 --- a/src/cli/formatter.ts +++ b/src/cli/formatter.ts @@ -101,8 +101,11 @@ export function searchResultsToJson( const query = opts.query || ""; const output = results.map(row => { const bodyStr = row.body || ""; + const snippetInfo = bodyStr + ? extractSnippet(bodyStr, query, 300, row.chunkPos, undefined, opts.intent) + : undefined; let body = opts.full ? bodyStr : undefined; - let snippet = !opts.full ? extractSnippet(bodyStr, query, 300, row.chunkPos, undefined, opts.intent).snippet : undefined; + let snippet = !opts.full ? snippetInfo?.snippet : undefined; if (opts.lineNumbers) { if (body) body = addLineNumbers(body); @@ -113,6 +116,7 @@ export function searchResultsToJson( docid: `#${row.docid}`, score: Math.round(row.score * 100) / 100, file: row.displayPath, + ...(snippetInfo && { line: snippetInfo.line }), title: row.title, ...(row.context && { context: row.context }), ...(body && { body }), diff --git a/test/formatter.test.ts b/test/formatter.test.ts index 601682e..eb07dd0 100644 --- a/test/formatter.test.ts +++ b/test/formatter.test.ts @@ -95,6 +95,20 @@ describe("search results include context in all formats", () => { expect(parsed[0].context).toBe(TEST_CONTEXT); }); + test("JSON format includes line", () => { + const output = searchResultsToJson(results, { query: "keynote" }); + const parsed = JSON.parse(output); + expect(parsed[0].line).toBeTypeOf("number"); + expect(parsed[0].line).toBeGreaterThan(0); + }); + + test("JSON format includes line with --full", () => { + const output = searchResultsToJson(results, { query: "keynote", full: true }); + const parsed = JSON.parse(output); + expect(parsed[0].line).toBeTypeOf("number"); + expect(parsed[0].line).toBeGreaterThan(0); + }); + test("CSV format includes context", () => { const output = searchResultsToCsv(results, { query: "keynote" }); // Header should have context column