layout
This commit is contained in:
@@ -24,6 +24,8 @@ pub const Document = struct {
|
||||
root: ?*sx.ast.Node,
|
||||
/// Sema results for this file (references are relative to this source).
|
||||
sema: ?sx.sema.SemaResult,
|
||||
/// Last successful sema (preserved across parse failures for completions).
|
||||
last_good_sema: ?sx.sema.SemaResult = null,
|
||||
/// Import declarations parsed from this file.
|
||||
imports: []const Import,
|
||||
|
||||
@@ -199,6 +201,9 @@ pub const DocumentStore = struct {
|
||||
|
||||
// Run sema on this file's own AST
|
||||
doc.sema = analyzer.analyze(root) catch null;
|
||||
if (doc.sema != null) {
|
||||
doc.last_good_sema = doc.sema;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const DocumentStore, path: []const u8) ?*Document {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user