lsp: implement textDocument/references (find references)

cmd-clicking a definition (or any use) now lists all references. Same-file matches are precise (by symbol index); cross-file matches a top-level name across loaded documents. Advertises referencesProvider. Verified: references to a free function resolve across files (rules.sx def + internal calls + main.sx caller).
This commit is contained in:
agra
2026-05-31 11:51:06 +03:00
parent 0cb4a5a342
commit 28e02a8372
2 changed files with 80 additions and 1 deletions

View File

@@ -68,6 +68,8 @@ pub const Server = struct {
if (params) |p| self.handleDidClose(p);
} else if (std.mem.eql(u8, method, "textDocument/definition")) {
if (params) |p| self.handleDefinition(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/references")) {
if (params) |p| self.handleReferences(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/hover")) {
if (params) |p| self.handleHover(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/documentSymbol")) {
@@ -173,6 +175,83 @@ pub const Server = struct {
// ---- Go to definition ----
fn appendRefLoc(self: *Server, buf: *std.ArrayList(u8), first: *bool, target_doc: *const Document, span: sx.ast.Span) !void {
if (!first.*) try buf.append(self.allocator, ',');
first.* = false;
const uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{target_doc.path});
const range = spanToRange(target_doc.source, span);
const loc = try lsp.locationJson(self.allocator, uri, range);
try buf.appendSlice(self.allocator, loc);
}
/// textDocument/references — all uses of the symbol under the cursor (a
/// reference or a definition). Same-file matches are precise (by symbol
/// index); cross-file matches a top-level name (functions/types/globals).
fn handleReferences(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
const ctx = try self.extractRequest(id, params) orelse return;
const pos = extractPosition(params) orelse return;
const id_json = ctx.id_json;
const file_path = uriToFilePath(ctx.uri) orelse "";
const doc = self.documents.get(file_path) orelse return try self.sendResponse(id_json, "null");
const sema = doc.sema orelse doc.last_good_sema orelse return try self.sendResponse(id_json, "null");
const offset = positionToOffset(doc.source, pos.line, pos.character) orelse return try self.sendResponse(id_json, "null");
var include_decl = true;
if (jsonGet(params, "context")) |c| {
if (jsonGet(c, "includeDeclaration")) |v| {
if (v == .bool) include_decl = v.bool;
}
}
// Resolve the target symbol: a reference at the cursor, or a definition.
var target_idx: ?u32 = null;
if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ri| {
target_idx = sema.references[ri].symbol_index;
} else if (findSymbolNameAtOffset(sema.symbols, doc.source, offset)) |si| {
target_idx = @intCast(si);
}
if (target_idx == null) return try self.sendResponse(id_json, "[]");
const target = sema.symbols[target_idx.?];
const cross_file = target.scope_depth == 0; // only top-level names span files
var buf = std.ArrayList(u8).empty;
defer buf.deinit(self.allocator);
try buf.append(self.allocator, '[');
var first = true;
// Current document — precise by symbol index.
if (include_decl) try self.appendRefLoc(&buf, &first, doc, target.def_span);
for (sema.references) |ref| {
if (ref.symbol_index == target_idx.?) try self.appendRefLoc(&buf, &first, doc, ref.span);
}
// Other documents — match a top-level name by string.
if (cross_file) {
var it = self.documents.by_path.iterator();
while (it.next()) |entry| {
const odoc = entry.value_ptr.*;
if (std.mem.eql(u8, odoc.path, doc.path)) continue;
const osema = odoc.sema orelse continue;
if (include_decl) {
for (osema.symbols) |sym| {
if (sym.scope_depth == 0 and sym.origin == null and std.mem.eql(u8, sym.name, target.name)) {
try self.appendRefLoc(&buf, &first, odoc, sym.def_span);
}
}
}
for (osema.references) |ref| {
const rs = osema.symbols[ref.symbol_index];
if (rs.scope_depth == 0 and std.mem.eql(u8, rs.name, target.name)) {
try self.appendRefLoc(&buf, &first, odoc, ref.span);
}
}
}
}
try buf.append(self.allocator, ']');
try self.sendResponse(id_json, buf.items);
}
fn handleDefinition(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
const ctx = try self.extractRequest(id, params) orelse return;
const pos = extractPosition(params) orelse return;

View File

@@ -102,7 +102,7 @@ fn writeJsonValue(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value:
/// Build the initialize result JSON.
pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
return std.fmt.allocPrint(allocator,
"{{\"capabilities\":{{\"textDocumentSync\":1,\"definitionProvider\":true,\"hoverProvider\":true,\"documentSymbolProvider\":true," ++
"{{\"capabilities\":{{\"textDocumentSync\":1,\"definitionProvider\":true,\"referencesProvider\":true,\"hoverProvider\":true,\"documentSymbolProvider\":true," ++
"\"completionProvider\":{{\"triggerCharacters\":[\".\"]}}," ++
"\"signatureHelpProvider\":{{\"triggerCharacters\":[\"(\",\",\"]}}," ++
"\"semanticTokensProvider\":{{\"legend\":{{" ++