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:
@@ -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;
|
||||
|
||||
@@ -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\":{{" ++
|
||||
|
||||
Reference in New Issue
Block a user