lsp: go-to-definition on a member declaration resolves to itself
Cmd+clicking a struct field / method / enum-variant at its own declaration returned null, so nothing happened — while find-references on the same token worked. Resolve a definition-site click to its own location; the editor then surfaces references on a definition-click instead of doing nothing. Member uses still resolve to their definition. Add selfMemberDefAt + a regression test.
This commit is contained in:
@@ -286,6 +286,18 @@ pub const Server = struct {
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
/// The name-token span of a member declaration (struct field, method, enum
|
||||
/// variant) at `offset`, or null when `offset` isn't on a declaration. Lets
|
||||
/// go-to-definition on a definition resolve to itself.
|
||||
fn selfMemberDefAt(sema: *const SemaResult, offset: u32) ?sx.ast.Span {
|
||||
for (sema.member_refs) |mr| {
|
||||
if (!mr.is_def) continue;
|
||||
if (offset < mr.span.start or offset >= mr.span.end) continue;
|
||||
return mr.span;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -304,6 +316,17 @@ pub const Server = struct {
|
||||
return try self.sendResponse(id_json, "null");
|
||||
};
|
||||
|
||||
// 0. Cursor sits on a member's own declaration (struct field, method
|
||||
// name, enum variant). Go-to-definition resolves to itself so the
|
||||
// editor recognises it as a definition — clicking a definition then
|
||||
// surfaces references instead of doing nothing.
|
||||
if (selfMemberDefAt(&sema, offset)) |def_span| {
|
||||
const self_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{doc.path});
|
||||
const loc = try lsp.locationJson(self.allocator, self_uri, spanToRange(doc.source, def_span));
|
||||
const arr = try std.fmt.allocPrint(self.allocator, "[{s}]", .{loc});
|
||||
return try self.sendResponse(id_json, arr);
|
||||
}
|
||||
|
||||
// 1. Qualified name (e.g. "std.print" or UFCS "list.append")
|
||||
if (extractQualifiedName(doc.source, offset)) |qn| {
|
||||
const qn_origin = sx.ast.Span{ .start = qn.full_start, .end = qn.full_end };
|
||||
@@ -3330,6 +3353,28 @@ test "lsp/references: excluding the declaration drops the definition" {
|
||||
try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, without_decl, "\"uri\""));
|
||||
}
|
||||
|
||||
test "lsp/definition: cursor on a member declaration resolves to itself" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
|
||||
const src: [:0]const u8 = "Move :: struct { flag: s64; }";
|
||||
const doc = try store.openOrUpdate("main.sx", src, 1);
|
||||
try store.analyzeDocument(doc);
|
||||
const sema = doc.sema orelse return error.SkipZigTest;
|
||||
|
||||
// On the `flag` field declaration → resolves to itself (so the editor
|
||||
// offers references on a definition-click instead of doing nothing).
|
||||
const flag_off: u32 = @intCast(std.mem.indexOf(u8, src, "flag").?);
|
||||
const span = Server.selfMemberDefAt(&sema, flag_off) orelse return error.TestUnexpectedResult;
|
||||
try std.testing.expectEqual(flag_off, span.start);
|
||||
|
||||
// On the struct name (not a member declaration) → null, so normal
|
||||
// resolution still runs.
|
||||
try std.testing.expect(Server.selfMemberDefAt(&sema, 0) == null);
|
||||
}
|
||||
|
||||
test "lsp/inlayHint: a for-loop capture in a struct method shows its element type" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
Reference in New Issue
Block a user