This commit is contained in:
agra
2026-02-11 02:22:00 +02:00
parent 89fc6427c4
commit 7bb4fe0c5f
7 changed files with 251 additions and 14 deletions

View File

@@ -163,6 +163,20 @@ pub const Server = struct {
return;
};
// Check if cursor is on a qualified name (e.g. "std.print" or UFCS "list.append")
if (extractQualifiedName(analysis.source, offset)) |qn| {
// Try namespace import first (e.g. std.print)
if (try self.resolveImportedLocation(id_json, analysis, qn.ns, qn.member)) |_| return;
// Try UFCS: obj.method → find free function "method"
if (findSymbolByName(analysis.sema.symbols, qn.member)) |si| {
const sym = analysis.sema.symbols[si];
if (sym.kind == .function) {
if (try self.sendSymbolLocation(id_json, uri, analysis, sym)) return;
}
}
}
// Check if cursor is on a reference → jump to definition
if (sx.sema.findReferenceAtOffset(analysis.sema.references, offset)) |ref_idx| {
const ref = analysis.sema.references[ref_idx];
@@ -178,11 +192,6 @@ pub const Server = struct {
if (try self.sendSymbolLocation(id_json, uri, analysis, sym)) return;
}
// Check if cursor is on a qualified name (e.g. "std.print")
if (extractQualifiedName(analysis.source, offset)) |qn| {
if (try self.resolveImportedLocation(id_json, analysis, qn.ns, qn.member)) |_| return;
}
// Check if cursor is on an #import "path" string → open the file
if (findImportPathAtOffset(analysis.source, offset)) |import_path| {
const file_path = uriToFilePath(uri) orelse "";
@@ -364,11 +373,14 @@ pub const Server = struct {
const line = std.math.cast(u32, jsonInt(jsonGet(position, "line") orelse return) orelse return) orelse return;
const character = std.math.cast(u32, jsonInt(jsonGet(position, "character") orelse return) orelse return) orelse return;
// Check if cursor is right after a dot — if so, do dot-completion
// Check if cursor is after a dot (possibly with partial identifier typed)
if (self.documents.get(uri)) |doc| {
if (positionToOffset(doc.text, line, character)) |off| {
if (off > 0 and doc.text[off - 1] == '.') {
try self.handleDotCompletion(id_json, uri, doc.text, off);
// Scan backwards past any identifier characters to find a dot
var scan = off;
while (scan > 0 and isIdentChar(doc.text[scan - 1])) scan -= 1;
if (scan > 0 and doc.text[scan - 1] == '.') {
try self.handleDotCompletion(id_json, uri, doc.text, scan);
return;
}
}
@@ -448,13 +460,21 @@ pub const Server = struct {
if (self.sema_cache.get(uri)) |analysis| {
// Check if prefix is a namespace — offer its inner declarations
if (!try self.collectNamespaceCompletions(&items, analysis, prefix)) {
// Otherwise look up prefix as a struct/enum type name in sema symbols
// Try as type name directly (e.g. Vec2., Color.)
try self.collectMemberCompletions(&items, analysis, prefix);
// Try as variable name — resolve to type and offer fields + UFCS methods
if (items.items.len == 0) {
if (resolveVariableType(analysis, prefix)) |type_name| {
try self.collectMemberCompletions(&items, analysis, type_name);
try self.collectUfcsCompletions(&items, analysis, type_name);
}
}
}
}
}
const items_json = try lsp.completionItemsJson(self.allocator, items.items);
const items_json = try lsp.completionListJson(self.allocator, items.items);
const resp = try lsp.jsonRpcResponse(self.allocator, id_json, items_json);
try self.transport.writeMessage(resp);
}
@@ -1603,6 +1623,109 @@ pub const Server = struct {
return null;
}
/// Resolve a variable/param name to its struct type name via sema type info.
fn resolveVariableType(analysis: DocumentAnalysis, var_name: []const u8) ?[]const u8 {
var i = analysis.sema.symbols.len;
while (i > 0) {
i -= 1;
const sym = analysis.sema.symbols[i];
if (!std.mem.eql(u8, sym.name, var_name)) continue;
if (sym.kind != .variable and sym.kind != .param) continue;
const ty = sym.ty orelse return null;
return switch (ty) {
.struct_type => |name| name,
else => null,
};
}
return null;
}
/// Extract the base type name from a parameter's type expression.
/// Unwraps pointer wrappers and parameterized types to get the core name.
/// e.g. *List($T) → "List", Vec2 → "Vec2", *Vec2 → "Vec2"
fn extractBaseTypeName(type_node: *sx.ast.Node) ?[]const u8 {
return switch (type_node.data) {
.type_expr => |te| te.name,
.pointer_type_expr => |pte| extractBaseTypeName(pte.pointee_type),
.parameterized_type_expr => |pte| pte.name,
else => null,
};
}
/// Collect UFCS-compatible functions: free functions whose first parameter
/// type matches the given struct type name (directly or through a pointer).
fn collectUfcsCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), analysis: DocumentAnalysis, type_name: []const u8) !void {
if (analysis.root.data != .root) return;
for (analysis.root.data.root.decls) |decl| {
try self.collectUfcsFromDecl(items, decl, type_name);
}
}
fn collectUfcsFromDecl(self: *Server, items: *std.ArrayList(lsp.CompletionItem), decl: *sx.ast.Node, type_name: []const u8) !void {
switch (decl.data) {
.fn_decl => |fd| {
if (fd.params.len > 0) {
const base = extractBaseTypeName(fd.params[0].type_expr) orelse return;
if (!std.mem.eql(u8, base, type_name)) return;
const detail = try self.formatUfcsDetail(fd.params[1..], fd.return_type);
try items.append(self.allocator, .{
.label = fd.name,
.kind = @intFromEnum(lsp.CompletionItemKind.Method),
.detail = detail,
});
}
},
.const_decl => |cd| {
if (cd.value.data == .lambda) {
const lam = cd.value.data.lambda;
if (lam.params.len > 0) {
const base = extractBaseTypeName(lam.params[0].type_expr) orelse return;
if (!std.mem.eql(u8, base, type_name)) return;
const detail = try self.formatUfcsDetail(lam.params[1..], lam.return_type);
try items.append(self.allocator, .{
.label = cd.name,
.kind = @intFromEnum(lsp.CompletionItemKind.Method),
.detail = detail,
});
}
}
},
.namespace_decl => |ns| {
for (ns.decls) |inner| {
try self.collectUfcsFromDecl(items, inner, type_name);
}
},
else => {},
}
}
/// Format detail string for a UFCS method (params excluding the first/self param).
fn formatUfcsDetail(self: *Server, params: []const sx.ast.Param, return_type: ?*sx.ast.Node) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(self.allocator, '(');
for (params, 0..) |param, pi| {
if (pi > 0) try buf.appendSlice(self.allocator, ", ");
try buf.appendSlice(self.allocator, param.name);
try buf.appendSlice(self.allocator, ": ");
if (param.type_expr.data == .type_expr) {
try buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name);
} else {
try buf.appendSlice(self.allocator, "?");
}
}
try buf.append(self.allocator, ')');
if (return_type) |rt| {
try buf.appendSlice(self.allocator, " -> ");
if (rt.data == .type_expr) {
try buf.appendSlice(self.allocator, rt.data.type_expr.name);
}
}
return buf.items;
}
/// Try to resolve a variable/param name to its struct type name.
fn resolveStructTypeName(analysis: DocumentAnalysis, var_name: []const u8) ?[]const u8 {
var i = analysis.sema.symbols.len;