optionals

This commit is contained in:
agra
2026-02-22 22:16:30 +02:00
parent d3e574eae5
commit 1cc67f9b5a
17 changed files with 1952 additions and 32 deletions

View File

@@ -76,6 +76,8 @@ pub const Server = struct {
if (params) |p| self.handleSignatureHelp(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/semanticTokens/full")) {
if (params) |p| self.handleSemanticTokens(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/inlayHint")) {
if (params) |p| self.handleInlayHint(id, p) catch |e| self.logError(method, e);
}
return true;
@@ -1015,6 +1017,325 @@ pub const Server = struct {
try self.sendResponse(id_json, result_json);
}
// ---- Inlay hints ----
fn handleInlayHint(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
const ctx = try self.extractRequest(id, 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, "[]");
};
const sema = doc.sema orelse doc.last_good_sema orelse {
return try self.sendResponse(id_json, "[]");
};
const root = doc.root orelse {
return try self.sendResponse(id_json, "[]");
};
var hints = std.ArrayList(lsp.InlayHint).empty;
collectInlayHints(self.allocator, root, sema.symbols, doc.source, &hints);
self.collectCallHints(doc, root, &hints);
const result_json = try lsp.inlayHintsJson(self.allocator, hints.items);
try self.sendResponse(id_json, result_json);
}
fn collectInlayHints(
allocator: std.mem.Allocator,
node: *const sx.ast.Node,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, source, hints);
},
.block => |b| {
for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, source, hints);
},
.fn_decl => |fd| {
collectInlayHints(allocator, fd.body, symbols, source, hints);
},
.lambda => |lm| {
collectInlayHints(allocator, lm.body, symbols, source, hints);
},
.if_expr => |ie| {
if (ie.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, ie.then_branch, symbols, source, hints);
if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, source, hints);
},
.while_expr => |we| {
if (we.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, we.body, symbols, source, hints);
},
.for_expr => |fe| {
collectInlayHints(allocator, fe.body, symbols, source, hints);
},
.var_decl => |vd| {
// Only show hint when type is inferred (:= syntax)
if (vd.type_annotation != null) return;
if (vd.value == null) return;
addHintForDecl(allocator, vd.name, node.span, symbols, source, hints, true);
},
.const_decl => |cd| {
// Skip if explicit type annotation
if (cd.type_annotation != null) return;
// Skip functions, types, structs, enums, unions, comptime, foreign, library
switch (cd.value.data) {
.lambda, .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl,
.comptime_expr, .foreign_expr, .library_decl,
=> return,
else => {},
}
addHintForDecl(allocator, cd.name, node.span, symbols, source, hints, false);
},
else => {},
}
}
fn addHintForDecl(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
is_colon_equal: bool,
) void {
// Find symbol by matching span start
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
// Skip void types — not useful to display
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
if (is_colon_equal) {
// For `:=` declarations: place hint between `:` and `=`
// Scan from after the name to find `:=`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
// Place hint at the `=` position (between `:` and `=`)
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
} else {
// For `::` declarations: place hint between first `:` and second `:`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == ':') {
const second_colon = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, second_colon);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
}
fn addBindingHint(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
// Look up symbol by name + span (sema stores binding with if/while node span)
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
// Scan from span start to find the `:=` used in the binding
var pos = span.start;
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
fn findSymbolAtSpan(symbols: []const sx.sema.Symbol, span_start: u32, name: []const u8) ?sx.sema.Symbol {
for (symbols) |sym| {
if (sym.def_span.start == span_start and std.mem.eql(u8, sym.name, name)) {
return sym;
}
}
return null;
}
// ---- Parameter name hints at call sites ----
fn collectCallHints(self: *Server, doc: *const Document, node: *const sx.ast.Node, hints: *std.ArrayList(lsp.InlayHint)) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| self.collectCallHints(doc, decl, hints);
},
.block => |b| {
for (b.stmts) |stmt| self.collectCallHints(doc, stmt, hints);
},
.fn_decl => |fd| {
self.collectCallHints(doc, fd.body, hints);
},
.lambda => |lm| {
self.collectCallHints(doc, lm.body, hints);
},
.if_expr => |ie| {
self.collectCallHints(doc, ie.condition, hints);
self.collectCallHints(doc, ie.then_branch, hints);
if (ie.else_branch) |eb| self.collectCallHints(doc, eb, hints);
},
.while_expr => |we| {
self.collectCallHints(doc, we.condition, hints);
self.collectCallHints(doc, we.body, hints);
},
.for_expr => |fe| {
self.collectCallHints(doc, fe.iterable, hints);
self.collectCallHints(doc, fe.body, hints);
},
.var_decl => |vd| {
if (vd.value) |val| self.collectCallHints(doc, val, hints);
},
.const_decl => |cd| {
self.collectCallHints(doc, cd.value, hints);
},
.return_stmt => |rs| {
if (rs.value) |val| self.collectCallHints(doc, val, hints);
},
.assignment => |a| {
self.collectCallHints(doc, a.value, hints);
},
.binary_op => |bop| {
self.collectCallHints(doc, bop.lhs, hints);
self.collectCallHints(doc, bop.rhs, hints);
},
.unary_op => |uop| {
self.collectCallHints(doc, uop.operand, hints);
},
.call => |c| {
// Recurse into arguments (they may contain nested calls)
for (c.args) |arg| self.collectCallHints(doc, arg, hints);
// Emit parameter name hints for this call
self.emitCallParamHints(doc, c, hints);
},
.push_stmt => |ps| {
self.collectCallHints(doc, ps.context_expr, hints);
self.collectCallHints(doc, ps.body, hints);
},
.defer_stmt => |ds| {
self.collectCallHints(doc, ds.expr, hints);
},
else => {},
}
}
fn emitCallParamHints(self: *Server, doc: *const Document, call: sx.ast.Call, hints: *std.ArrayList(lsp.InlayHint)) void {
if (call.args.len == 0) return;
// Resolve callee name and find function declaration
var param_offset: usize = 0;
const fd = self.resolveCallTarget(doc, call, &param_offset) orelse return;
// Emit hints for each argument
for (call.args, 0..) |arg, i| {
const param_idx = i + param_offset;
if (param_idx >= fd.params.len) break;
const param = fd.params[param_idx];
// Skip variadic params
if (param.is_variadic) break;
// Skip if arg is an identifier matching the param name
if (arg.data == .identifier) {
if (std.mem.eql(u8, arg.data.identifier.name, param.name)) continue;
}
// Skip _ params
if (std.mem.eql(u8, param.name, "_")) continue;
const loc = sx.errors.SourceLoc.compute(doc.source, arg.span.start);
if (loc.line == 0 or loc.col == 0) continue;
const label = std.fmt.allocPrint(self.allocator, "{s}:", .{param.name}) catch continue;
hints.append(self.allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = label,
.padding_left = false,
}) catch {};
}
}
fn resolveCallTarget(self: *Server, doc: *const Document, call: sx.ast.Call, param_offset: *usize) ?sx.ast.FnDecl {
param_offset.* = 0;
if (call.callee.data == .identifier) {
const name = call.callee.data.identifier.name;
return self.findFnDeclByName(doc, name);
}
if (call.callee.data == .field_access) {
const fa = call.callee.data.field_access;
// Try namespaced: "ns.func"
if (fa.object.data == .identifier) {
const ns_name = fa.object.data.identifier.name;
const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }) catch return null;
if (self.findFnDeclByName(doc, qualified)) |fd| {
return fd;
}
}
// Try UFCS: bare function name, skip first param (receiver)
if (self.findFnDeclByName(doc, fa.field)) |fd| {
if (fd.params.len == call.args.len + 1) {
param_offset.* = 1;
}
return fd;
}
}
return null;
}
fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
const ST = lsp.SemanticTokenType;
return switch (tok.tag) {
@@ -1084,6 +1405,9 @@ pub const Server = struct {
.pipe_arrow,
.caret,
.caret_equal,
.question,
.question_question,
.question_dot,
.tilde,
.less_less,
.less_less_equal,