This commit is contained in:
agra
2026-02-14 21:27:51 +02:00
parent 0e777e9d2e
commit e7d2abdf0c
5 changed files with 338 additions and 78 deletions

View File

@@ -462,36 +462,32 @@ pub const Server = struct {
var items = std.ArrayList(lsp.CompletionItem).empty;
if (extractDotPrefix(doc.source, cursor_offset)) |prefix| {
const sema = doc.sema orelse {
const items_json = try lsp.completionListJson(self.allocator, items.items);
return try self.sendResponse(id_json, items_json);
};
if (doc.sema) |sema| {
// Check if prefix is a namespace — offer imported doc's declarations
if (self.findImportByNs(doc, prefix)) |imp| {
if (self.documents.get(imp.path)) |imp_doc| {
if (imp_doc.root) |imp_root| {
if (imp_root.data == .root) {
try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls);
}
}
}
} else if (doc.root) |root| {
// Try as type name directly (e.g. Vec2., Color.)
try self.collectMemberCompletions(&items, sema, root, prefix);
// Check if prefix is a namespace — offer imported doc's declarations
if (self.findImportByNs(doc, prefix)) |imp| {
if (self.documents.get(imp.path)) |imp_doc| {
if (imp_doc.root) |imp_root| {
if (imp_root.data == .root) {
try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls);
// Try as variable name — resolve to type and offer fields + UFCS methods
if (items.items.len == 0) {
if (resolveVariableType(sema, prefix)) |type_name| {
try self.collectMemberCompletions(&items, sema, root, type_name);
try self.collectUfcsCompletions(&items, root, type_name);
}
}
}
} else {
const root = doc.root orelse {
const items_json = try lsp.completionListJson(self.allocator, items.items);
return try self.sendResponse(id_json, items_json);
};
// Try as type name directly (e.g. Vec2., Color.)
try self.collectMemberCompletions(&items, sema, root, prefix);
// Try as variable name — resolve to type and offer fields + UFCS methods
if (items.items.len == 0) {
if (resolveVariableType(sema, prefix)) |type_name| {
try self.collectMemberCompletions(&items, sema, root, type_name);
try self.collectUfcsCompletions(&items, root, type_name);
}
}
}
// Fallback: try resolving as a match arm capture variable (works without current parse)
if (items.items.len == 0) {
try self.collectCaptureCompletions(&items, doc, cursor_offset, prefix);
}
} else {
// Bare dot (no prefix) — check if we're inside a match expression
@@ -504,62 +500,235 @@ pub const Server = struct {
}
fn collectMatchEnumCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32) !void {
const root = doc.root orelse return;
const sema = doc.sema orelse return;
// Text-based approach: scan backward from cursor to find enclosing "SUBJECT == {"
// This works even when the file has parse errors (user is mid-typing)
const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return;
// Find enclosing match expression's subject
const subject = sx.sema.findEnclosingMatchSubject(root, cursor_offset) orelse return;
// Use sema (current or last successful) to resolve the subject's type
const sema = doc.sema orelse doc.last_good_sema orelse return;
// Resolve the subject to an enum type name
const enum_name: ?[]const u8 = switch (subject.data) {
.identifier => |id| blk: {
// Look up variable type, then check if it's an enum
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, id.name)) continue;
if (sym.kind != .variable and sym.kind != .param) continue;
const ty = sym.ty orelse break;
break :blk switch (ty) {
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
// Resolve subject name to an enum type
const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return;
// Use enum variant names directly from sema symbols (works even without doc.root)
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, enum_name)) continue;
if (sym.kind != .enum_type) continue;
// Find the enum_decl node in the origin doc (or main doc)
const lookup_doc = if (sym.origin) |origin_path|
self.documents.get(origin_path)
else
null;
const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root;
const root = lookup_root orelse continue;
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
if (node.data == .enum_decl) {
for (node.data.enum_decl.variant_names) |variant| {
try items.append(self.allocator, .{
.label = variant,
.kind = @intFromEnum(lsp.CompletionItemKind.EnumMember),
});
}
}
break :blk null;
},
.field_access => |fa| blk: {
// e.g. e.key — resolve the field's type
if (fa.object.data == .identifier) {
const var_name = fa.object.data.identifier.name;
// Find variable's struct type, then look up the field type
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, var_name)) continue;
const ty = sym.ty orelse break;
const struct_name = switch (ty) {
.struct_type => |n| n,
else => break,
};
// Look up the struct's field type
if (sema.struct_types.get(struct_name)) |info| {
for (info.field_names, 0..) |fname, fi| {
if (std.mem.eql(u8, fname, fa.field) and fi < info.field_types.len) {
break :blk switch (info.field_types[fi]) {
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
}
break;
}
}
/// Scan backward through source text to find the subject of an enclosing match expression.
/// Looks for the pattern: SUBJECT == { ... case .
/// Returns the subject text (e.g. "event", "e.key") or null.
fn findMatchSubjectText(source: []const u8, cursor_offset: u32) ?[]const u8 {
var pos: u32 = cursor_offset;
var brace_depth: u32 = 0;
// Walk backward, tracking brace depth, looking for "== {"
while (pos > 2) {
pos -= 1;
const ch = source[pos];
if (ch == '}') {
brace_depth += 1;
} else if (ch == '{') {
if (brace_depth > 0) {
brace_depth -= 1;
} else {
// Found an unmatched '{' — check if preceded by "=="
var scan = pos;
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
if (scan >= 2 and source[scan - 1] == '=' and source[scan - 2] == '=') {
// Found "== {" — extract the subject before "=="
var end = scan - 2;
while (end > 0 and std.ascii.isWhitespace(source[end - 1])) end -= 1;
if (end == 0) return null;
var start = end;
while (start > 0 and (isIdentChar(source[start - 1]) or source[start - 1] == '.')) {
start -= 1;
}
if (start < end) return source[start..end];
}
// Not a match expr — but keep scanning (might be a nested block)
}
}
}
return null;
}
/// Resolve a capture variable's struct type by scanning backward for `case .VARIANT: (name)`
/// and looking up the variant's payload type in the enum declaration.
fn collectCaptureCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32, var_name: []const u8) !void {
const sema = doc.sema orelse doc.last_good_sema orelse return;
// Scan backward from cursor to find: case .VARIANT: (var_name)
const variant_name = findCaptureVariant(doc.source, cursor_offset, var_name) orelse return;
// Find the enclosing match subject
const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return;
// Resolve subject to an enum type
const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return;
// Find the enum_decl and look up the variant's payload type
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, enum_name)) continue;
if (sym.kind != .enum_type) continue;
const lookup_doc = if (sym.origin) |origin_path|
self.documents.get(origin_path)
else
null;
const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root;
const root = lookup_root orelse continue;
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
if (node.data == .enum_decl) {
const ed = node.data.enum_decl;
// Find the matching variant and its payload type
for (ed.variant_names, 0..) |vn, vi| {
if (!std.mem.eql(u8, vn, variant_name)) continue;
if (vi >= ed.variant_types.len) break;
const vt = ed.variant_types[vi] orelse break;
// The payload type should be a struct — get its name
const payload_type_name = if (vt.data == .type_expr) vt.data.type_expr.name else break;
// Now offer that struct's fields
const payload_sema = if (lookup_doc) |ld| ld.sema orelse sema else sema;
try self.collectMemberCompletions(items, payload_sema, root, payload_type_name);
return;
}
}
}
break;
}
}
/// Scan backward from cursor to find `case .VARIANT: (var_name)` and return VARIANT.
fn findCaptureVariant(source: []const u8, cursor_offset: u32, var_name: []const u8) ?[]const u8 {
// Look backward for pattern: case .VARIANT: (var_name)
// We search for "(var_name)" first, then look for "case .VARIANT:" before it
var pos: u32 = cursor_offset;
while (pos > var_name.len + 10) { // need room for "case .X: (name)"
pos -= 1;
// Look for the closing ) of a capture
if (source[pos] != ')') continue;
// Check if the capture name matches
if (pos < var_name.len + 1) continue;
const name_end = pos;
const name_start = pos - @as(u32, @intCast(var_name.len));
if (!std.mem.eql(u8, source[name_start..name_end], var_name)) continue;
// Check for ( before the name
if (name_start < 1 or source[name_start - 1] != '(') continue;
// Now scan backward from '(' to find ": " then ".VARIANT" then "case "
var scan = name_start - 1;
// Skip whitespace
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
// Expect ':'
if (scan < 1 or source[scan - 1] != ':') continue;
scan -= 1;
// Now extract the variant name: scan backward for ".VARIANT"
// Skip whitespace before ':'
// The variant is an enum literal like .key_down
// Actually the ':' comes right after the variant value (explicit) or variant name
// Pattern: case .VARIANT: (name) OR case .VARIANT :: 0x300: PayloadType;... nah
// In the match arm: case .key_down: (e) {
// So before ':' we have the enum literal .key_down
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
// Extract identifier (variant name)
const vend = scan;
while (scan > 0 and (isIdentChar(source[scan - 1]))) scan -= 1;
if (scan >= vend) continue;
const variant = source[scan..vend];
// Check for '.' before variant name
if (scan < 1 or source[scan - 1] != '.') continue;
return variant;
}
return null;
}
/// Resolve a text expression like "event" or "e.key" to an enum/union type name.
fn resolveExprEnumType(self: *Server, sema: SemaResult, expr: []const u8) ?[]const u8 {
// Simple identifier: look up variable type
if (std.mem.indexOfScalar(u8, expr, '.') == null) {
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, expr)) continue;
if (sym.kind != .variable and sym.kind != .param) continue;
const ty = sym.ty orelse return null;
return switch (ty) {
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
}
return null;
}
// Field access: "var.field" — resolve var's struct type, then look up field
if (std.mem.indexOfScalar(u8, expr, '.')) |dot| {
const var_name = expr[0..dot];
const field_name = expr[dot + 1 ..];
for (sema.symbols) |sym| {
if (!std.mem.eql(u8, sym.name, var_name)) continue;
const ty = sym.ty orelse return null;
const struct_name = switch (ty) {
.struct_type => |n| n,
else => return null,
};
if (sema.struct_types.get(struct_name)) |info| {
for (info.field_names, 0..) |fname, fi| {
if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) {
return switch (info.field_types[fi]) {
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
}
}
}
// Also check imported docs for struct info
if (sym.origin) |origin_path| {
if (self.documents.get(origin_path)) |od| {
if (od.sema) |imp_sema| {
if (imp_sema.struct_types.get(struct_name)) |info| {
for (info.field_names, 0..) |fname, fi| {
if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) {
return switch (info.field_types[fi]) {
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
}
}
}
}
break;
}
}
break :blk null;
},
else => null,
};
return null;
}
}
const name = enum_name orelse return;
try self.collectMemberCompletions(items, sema, root, name);
return null;
}
fn collectDeclCompletions(allocator: std.mem.Allocator, items: *std.ArrayList(lsp.CompletionItem), decls: []const *sx.ast.Node) !void {
@@ -1896,3 +2065,45 @@ pub const Server = struct {
return buf.items;
}
};
test "findMatchSubjectText: simple identifier" {
const source = "if event == {\n case .";
const result = Server.findMatchSubjectText(source, @intCast(source.len));
try std.testing.expectEqualStrings("event", result.?);
}
test "findMatchSubjectText: field access" {
const source = "if e.key == {\n case .";
const result = Server.findMatchSubjectText(source, @intCast(source.len));
try std.testing.expectEqualStrings("e.key", result.?);
}
test "findMatchSubjectText: nested braces" {
const source = "if event == {\n case .quit: { do_something(); }\n case .";
const result = Server.findMatchSubjectText(source, @intCast(source.len));
try std.testing.expectEqualStrings("event", result.?);
}
test "findMatchSubjectText: no match context" {
const source = "while true {\n case .";
const result = Server.findMatchSubjectText(source, @intCast(source.len));
try std.testing.expect(result == null);
}
test "findCaptureVariant: simple capture" {
const source = "case .key_down: (e) {\n e.";
const result = Server.findCaptureVariant(source, @intCast(source.len), "e");
try std.testing.expectEqualStrings("key_down", result.?);
}
test "findCaptureVariant: different name" {
const source = "case .mouse_motion: (evt) {\n evt.";
const result = Server.findCaptureVariant(source, @intCast(source.len), "evt");
try std.testing.expectEqualStrings("mouse_motion", result.?);
}
test "findCaptureVariant: no capture" {
const source = "case .quit: running = false;\n e.";
const result = Server.findCaptureVariant(source, @intCast(source.len), "e");
try std.testing.expect(result == null);
}