optionals
This commit is contained in:
@@ -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, ¶m_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,
|
||||
|
||||
@@ -108,7 +108,8 @@ pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
|
||||
"\"semanticTokensProvider\":{{\"legend\":{{" ++
|
||||
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
|
||||
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
|
||||
"}},\"full\":true}}}}}}",
|
||||
"}},\"full\":true}}," ++
|
||||
"\"inlayHintProvider\":true}}}}",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
@@ -358,3 +359,29 @@ pub fn publishDiagnosticsJson(allocator: std.mem.Allocator, uri: []const u8, dia
|
||||
try buf.appendSlice(allocator, "]}");
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub const InlayHint = struct {
|
||||
line: u32,
|
||||
character: u32,
|
||||
label: []const u8,
|
||||
kind: u32 = 1, // 1 = Type
|
||||
padding_left: bool = true,
|
||||
padding_right: bool = false,
|
||||
};
|
||||
|
||||
/// Build inlay hints JSON array response.
|
||||
pub fn inlayHintsJson(allocator: std.mem.Allocator, hints: []const InlayHint) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
try buf.append(allocator, '[');
|
||||
for (hints, 0..) |hint, idx| {
|
||||
if (idx > 0) try buf.append(allocator, ',');
|
||||
const label_escaped = try jsonString(allocator, hint.label);
|
||||
const json = try std.fmt.allocPrint(allocator,
|
||||
"{{\"position\":{{\"line\":{d},\"character\":{d}}},\"label\":{s},\"kind\":{d},\"paddingLeft\":{s},\"paddingRight\":{s}}}",
|
||||
.{ hint.line, hint.character, label_escaped, hint.kind, if (hint.padding_left) "true" else "false", if (hint.padding_right) "true" else "false" },
|
||||
);
|
||||
try buf.appendSlice(allocator, json);
|
||||
}
|
||||
try buf.append(allocator, ']');
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user