Skip to content
68 changes: 54 additions & 14 deletions src/explore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1256,22 +1256,63 @@ pub const Explorer = struct {
var result_list: std.ArrayList(SymbolResult) = .empty;
errdefer result_list.deinit(allocator);

// Scan outlines for all symbols by name (catches all kinds including imports).
// Track (path, line_start) pairs already appended. symbol_index can be
// incomplete after fast-snapshot restore (outlines are populated before
// rebuildSymbolIndexFor runs on every file), so we must still fall
// through to the outline scan — and dedupe against what the index
// already supplied. Keys are "<path>:<line>" allocated from the caller
// allocator, freed at end of call.
var seen = std.StringHashMap(void).init(allocator);
defer {
var sit = seen.keyIterator();
while (sit.next()) |k| allocator.free(k.*);
seen.deinit();
}

if (self.symbol_index.get(name)) |locs| {
for (locs.items) |loc| {
var detail: ?[]const u8 = null;
if (self.outlines.getPtr(loc.path)) |outline| {
Comment on lines +1272 to +1275
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fallback scan when symbol index is incomplete

This new early return assumes symbol_index is authoritative, but that is not always true after fast snapshot restore: restored files are inserted into outlines without rebuilding symbol_index, and later watcher/edit updates repopulate the map only for touched files. In that mixed state, symbols present in untouched files are silently omitted because the outline scan fallback never runs once get(name) is non-null. Keep scanning for missing hits or rebuild the full symbol index after fast restore.

Useful? React with 👍 / 👎.

for (outline.symbols.items) |sym| {
if (sym.line_start == loc.line_start and std.mem.eql(u8, sym.name, name)) {
detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null;
break;
}
}
}
try result_list.append(allocator, .{
.path = try allocator.dupe(u8, loc.path),
.symbol = .{
.name = try allocator.dupe(u8, name),
.kind = loc.kind,
.line_start = loc.line_start,
.line_end = loc.line_end,
.detail = detail,
},
});
const key = try std.fmt.allocPrint(allocator, "{s}:{d}", .{ loc.path, loc.line_start });
seen.put(key, {}) catch allocator.free(key);
}
}

// Safety scan: append any outline symbols the index missed.
var iter = self.outlines.iterator();
while (iter.next()) |entry| {
for (entry.value_ptr.symbols.items) |sym| {
if (std.mem.eql(u8, sym.name, name)) {
try result_list.append(allocator, .{
.path = try allocator.dupe(u8, entry.key_ptr.*),
.symbol = .{
.name = try allocator.dupe(u8, sym.name),
.kind = sym.kind,
.line_start = sym.line_start,
.line_end = sym.line_end,
.detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null,
},
});
}
if (!std.mem.eql(u8, sym.name, name)) continue;
var key_buf: [std.fs.max_path_bytes + 32]u8 = undefined;
const key = std.fmt.bufPrint(&key_buf, "{s}:{d}", .{ entry.key_ptr.*, sym.line_start }) catch continue;
if (seen.contains(key)) continue;
try result_list.append(allocator, .{
.path = try allocator.dupe(u8, entry.key_ptr.*),
.symbol = .{
.name = try allocator.dupe(u8, sym.name),
.kind = sym.kind,
.line_start = sym.line_start,
.line_end = sym.line_end,
.detail = if (sym.detail) |d| try allocator.dupe(u8, d) else null,
},
});
}
}
return result_list.toOwnedSlice(allocator);
Expand Down Expand Up @@ -2684,7 +2725,6 @@ pub const Explorer = struct {
fn rebuildSymbolIndexFor(self: *Explorer, path: []const u8, outline: *FileOutline) void {
self.removeSymbolIndexFor(path);
for (outline.symbols.items) |sym| {
if (sym.kind == .import or sym.kind == .comment_block) continue;
const gop = self.symbol_index.getOrPut(sym.name) catch continue;
if (!gop.found_existing) {
gop.value_ptr.* = std.ArrayList(SymbolLocation).empty;
Expand Down
14 changes: 12 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn mainImpl() !void {
const stdout = cio.File.stdout();
const use_color = stdout.isTty();
const s = sty.style(use_color);
const out = Out{ .file = stdout, .alloc = allocator };
var out = Out{ .file = stdout, .alloc = allocator };

const args = try cio.argsAlloc(allocator);
defer cio.argsFree(allocator, args);
Expand Down Expand Up @@ -106,6 +106,13 @@ fn mainImpl() !void {
std.process.exit(1);
}

// MCP stdio reserves stdout for JSON-RPC — route status/error output to
// stderr so startup/failure paths don't corrupt the protocol stream.
// See #304.
if (std.mem.eql(u8, cmd, "mcp")) {
out.file = cio.File.stderr();
}

// Handle --version early (no root needed)
if (std.mem.eql(u8, cmd, "--version") or std.mem.eql(u8, cmd, "-v") or std.mem.eql(u8, cmd, "version")) {
out.p("codedb {s}\n", .{release_info.semver});
Expand Down Expand Up @@ -550,7 +557,10 @@ fn mainImpl() !void {
s.reset,
});
} else if (std.mem.eql(u8, cmd, "serve")) {
const port: u16 = 7719;
const port: u16 = blk: {
const raw = cio.posixGetenv("CODEDB_PORT") orelse break :blk 6767;
break :blk std.fmt.parseInt(u16, raw, 10) catch 6767;
};
var agents = AgentRegistry.init(allocator);
defer agents.deinit();
_ = try agents.register("__filesystem__");
Expand Down
Loading
Loading