diff --git a/src/explore.zig b/src/explore.zig index a879088d..94209fa3 100644 --- a/src/explore.zig +++ b/src/explore.zig @@ -3931,6 +3931,31 @@ pub const Explorer = struct { // OOM building the offset table → bail to the full searchContent // path (caller falls through), which renders the same results. const n_spans = self.line_offsets.lineSpans(stats.path, content, target_lines[0..target_count], &spans) orelse return false; + // Def-line-first: within a file that defines the query symbol, render the + // definition line(s) before mere mentions — stable-moves matching spans to + // the front (reorders the OUTPUT spans, not lineSpans' sorted input). + if (stats.defines) { + if (self.outlines.get(stats.path)) |outline| { + var def_w: usize = 0; + var i: usize = 0; + while (i < n_spans) : (i += 1) { + var is_def = false; + for (outline.symbols.items) |sym| { + if (sym.line_start == spans[i].line and asciiEqlIgnoreCase(sym.name, query)) { + is_def = true; + break; + } + } + if (is_def) { + const tmp = spans[i]; + var j: usize = i; + while (j > def_w) : (j -= 1) spans[j] = spans[j - 1]; + spans[def_w] = tmp; + def_w += 1; + } + } + } + } for (spans[0..n_spans]) |line_span| { rendered += 1; diff --git a/src/test_search.zig b/src/test_search.zig index 075711dd..2abab2a4 100644 --- a/src/test_search.zig +++ b/src/test_search.zig @@ -1900,6 +1900,28 @@ test "audit: searchContent tier0 use_line_hits early-return skips rerank basenam // src/explore.zig renderPlainSearch — the MCP codedb_search fast-path rendered in raw // hit-count order with no basename prior, so a noise file outranked the canonical match. +test "def-first: renderPlainSearch surfaces the definition line before mentions in the same file" { + var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY); + defer explorer.deinit(); + + // A comment mentioning `gadget` appears BEFORE its definition; several call + // sites follow. Line-order rendering would show the line-1 comment first. + try explorer.indexFile("src/g.zig", + "// gadget helper\nconst a = gadget;\nconst b = gadget;\npub fn gadget() void {}\ngadget();\ngadget();\n"); + + var out: std.ArrayList(u8) = .empty; + defer out.deinit(testing.allocator); + + const rendered = try explorer.renderPlainSearch("gadget", testing.allocator, &out, 6, false); + try testing.expect(rendered); + + // the def line (`pub fn gadget`, line 4) must render before the comment mention (line 1) + const def_i = std.mem.indexOf(u8, out.items, "src/g.zig:4:"); + const com_i = std.mem.indexOf(u8, out.items, "src/g.zig:1:"); + try testing.expect(def_i != null and com_i != null); + try testing.expect(def_i.? < com_i.?); +} + test "def-first: renderPlainSearch ranks the defining file above higher-count mentions" { var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY); defer explorer.deinit();