1801 lines
72 KiB
Zig
1801 lines
72 KiB
Zig
const std = @import("std");
|
|
const sx = struct {
|
|
pub const ast = @import("../ast.zig");
|
|
pub const parser = @import("../parser.zig");
|
|
pub const lexer = @import("../lexer.zig");
|
|
pub const token = @import("../token.zig");
|
|
pub const types = @import("../types.zig");
|
|
pub const sema = @import("../sema.zig");
|
|
pub const errors = @import("../errors.zig");
|
|
pub const imports = @import("../imports.zig");
|
|
};
|
|
const lsp = @import("types.zig");
|
|
const doc_mod = @import("document.zig");
|
|
const DocumentStore = doc_mod.DocumentStore;
|
|
const Document = doc_mod.Document;
|
|
const Transport = @import("transport.zig").Transport;
|
|
const SemaResult = sx.sema.SemaResult;
|
|
|
|
pub const Server = struct {
|
|
allocator: std.mem.Allocator,
|
|
documents: DocumentStore,
|
|
transport: *Transport,
|
|
io: std.Io,
|
|
shutdown_requested: bool = false,
|
|
|
|
pub fn init(allocator: std.mem.Allocator, transport: *Transport, io: std.Io) Server {
|
|
return .{
|
|
.allocator = allocator,
|
|
.documents = DocumentStore.init(allocator, io),
|
|
.transport = transport,
|
|
.io = io,
|
|
};
|
|
}
|
|
|
|
pub fn handleMessage(self: *Server, raw: []const u8) bool {
|
|
const parsed = std.json.parseFromSlice(std.json.Value, self.allocator, raw, .{}) catch {
|
|
return true;
|
|
};
|
|
const root = parsed.value;
|
|
|
|
const method = jsonStr(jsonGet(root, "method") orelse return true) orelse return true;
|
|
|
|
const id = jsonGet(root, "id");
|
|
const params = jsonGet(root, "params");
|
|
|
|
if (std.mem.eql(u8, method, "initialize")) {
|
|
self.handleInitialize(id) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "initialized")) {
|
|
// Nothing to do
|
|
} else if (std.mem.eql(u8, method, "shutdown")) {
|
|
self.shutdown_requested = true;
|
|
if (id) |req_id| {
|
|
const id_json = lsp.valueToJson(self.allocator, req_id) catch return true;
|
|
const resp = lsp.jsonRpcResponse(self.allocator, id_json, "null") catch return true;
|
|
self.transport.writeMessage(resp) catch {};
|
|
}
|
|
} else if (std.mem.eql(u8, method, "exit")) {
|
|
return false;
|
|
} else if (std.mem.eql(u8, method, "textDocument/didOpen")) {
|
|
if (params) |p| self.handleDidOpen(p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/didChange")) {
|
|
if (params) |p| self.handleDidChange(p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/didClose")) {
|
|
if (params) |p| self.handleDidClose(p);
|
|
} else if (std.mem.eql(u8, method, "textDocument/definition")) {
|
|
if (params) |p| self.handleDefinition(id, p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/hover")) {
|
|
if (params) |p| self.handleHover(id, p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/documentSymbol")) {
|
|
if (params) |p| self.handleDocumentSymbol(id, p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/completion")) {
|
|
if (params) |p| self.handleCompletion(id, p) catch |e| self.logError(method, e);
|
|
} else if (std.mem.eql(u8, method, "textDocument/signatureHelp")) {
|
|
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);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
fn logError(self: *Server, method: []const u8, err: anyerror) void {
|
|
self.logMessage("lsp: {s} failed: {s}", .{ method, @errorName(err) });
|
|
}
|
|
|
|
fn logMessage(self: *Server, comptime fmt: []const u8, args: anytype) void {
|
|
const stderr = std.Io.File.stderr();
|
|
var buf: [512]u8 = undefined;
|
|
const msg = std.fmt.bufPrint(&buf, fmt ++ "\n", args) catch return;
|
|
stderr.writeStreamingAll(self.io, msg) catch {};
|
|
}
|
|
|
|
const RequestContext = struct {
|
|
id_json: []const u8,
|
|
uri: []const u8,
|
|
};
|
|
|
|
fn extractRequest(self: *Server, id: ?std.json.Value, params: std.json.Value) !?RequestContext {
|
|
const req_id = id orelse return null;
|
|
const id_json = try lsp.valueToJson(self.allocator, req_id);
|
|
const td = jsonGet(params, "textDocument") orelse return null;
|
|
const uri = jsonStr(jsonGet(td, "uri") orelse return null) orelse return null;
|
|
return .{ .id_json = id_json, .uri = uri };
|
|
}
|
|
|
|
fn extractPosition(params: std.json.Value) ?struct { line: u32, character: u32 } {
|
|
const position = jsonGet(params, "position") orelse return null;
|
|
const line = std.math.cast(u32, jsonInt(jsonGet(position, "line") orelse return null) orelse return null) orelse return null;
|
|
const character = std.math.cast(u32, jsonInt(jsonGet(position, "character") orelse return null) orelse return null) orelse return null;
|
|
return .{ .line = line, .character = character };
|
|
}
|
|
|
|
fn sendResponse(self: *Server, id_json: []const u8, result_json: []const u8) !void {
|
|
const resp = try lsp.jsonRpcResponse(self.allocator, id_json, result_json);
|
|
try self.transport.writeMessage(resp);
|
|
}
|
|
|
|
fn handleInitialize(self: *Server, id: ?std.json.Value) !void {
|
|
const req_id = id orelse return;
|
|
const id_json = try lsp.valueToJson(self.allocator, req_id);
|
|
const result_json = try lsp.initializeResultJson(self.allocator);
|
|
try self.sendResponse(id_json, result_json);
|
|
}
|
|
|
|
// ---- Document lifecycle ----
|
|
|
|
fn handleDidOpen(self: *Server, params: std.json.Value) !void {
|
|
const td = jsonGet(params, "textDocument") orelse return;
|
|
const uri = jsonStr(jsonGet(td, "uri") orelse return) orelse return;
|
|
const text = jsonStr(jsonGet(td, "text") orelse return) orelse return;
|
|
const version = jsonInt(jsonGet(td, "version") orelse return) orelse return;
|
|
|
|
try self.analyzeAndPublish(uri, text, version);
|
|
}
|
|
|
|
fn handleDidChange(self: *Server, params: std.json.Value) !void {
|
|
const td = jsonGet(params, "textDocument") orelse return;
|
|
const uri = jsonStr(jsonGet(td, "uri") orelse return) orelse return;
|
|
const version = jsonInt(jsonGet(td, "version") orelse return) orelse return;
|
|
|
|
const changes_arr = jsonArr(jsonGet(params, "contentChanges") orelse return) orelse return;
|
|
if (changes_arr.len == 0) return;
|
|
|
|
const last = changes_arr[changes_arr.len - 1];
|
|
const text = jsonStr(jsonGet(last, "text") orelse return) orelse return;
|
|
|
|
try self.analyzeAndPublish(uri, text, version);
|
|
}
|
|
|
|
fn handleDidClose(_: *Server, params: std.json.Value) void {
|
|
_ = params;
|
|
// Documents stay in the store (imports may reference them).
|
|
}
|
|
|
|
// ---- Go to definition ----
|
|
|
|
fn handleDefinition(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
|
|
const ctx = try self.extractRequest(id, params) orelse return;
|
|
const pos = extractPosition(params) orelse return;
|
|
const id_json = ctx.id_json;
|
|
const uri = ctx.uri;
|
|
const file_path = uriToFilePath(uri) orelse "";
|
|
|
|
const doc = self.documents.get(file_path) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
const sema = doc.sema orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
const offset = positionToOffset(doc.source, pos.line, pos.character) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
// 1. Qualified name (e.g. "std.print" or UFCS "list.append")
|
|
if (extractQualifiedName(doc.source, offset)) |qn| {
|
|
// Namespace import member
|
|
if (self.findImportByNs(doc, qn.ns)) |imp| {
|
|
if (self.documents.get(imp.path)) |imp_doc| {
|
|
if (imp_doc.sema) |imp_sema| {
|
|
if (findSymbolByName(imp_sema.symbols, qn.member)) |si| {
|
|
const sym = imp_sema.symbols[si];
|
|
if (try self.sendSymbolLocation(id_json, imp_doc, sym)) return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// UFCS: obj.method → find free function "method"
|
|
if (findSymbolByName(sema.symbols, qn.member)) |si| {
|
|
const sym = sema.symbols[si];
|
|
if (sym.kind == .function) {
|
|
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Reference at offset → jump to definition
|
|
if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ref_idx| {
|
|
const ref = sema.references[ref_idx];
|
|
if (ref.symbol_index < sema.symbols.len) {
|
|
const sym = sema.symbols[ref.symbol_index];
|
|
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
|
}
|
|
}
|
|
|
|
// 3. Symbol definition name at offset
|
|
if (findSymbolNameAtOffset(sema.symbols, doc.source, offset)) |sym_idx| {
|
|
const sym = sema.symbols[sym_idx];
|
|
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
|
}
|
|
|
|
// 4. #import "path" string → open the file
|
|
if (findImportPathAtOffset(doc.source, offset)) |import_path| {
|
|
const base_dir = sx.imports.dirName(file_path);
|
|
const resolved = if (std.mem.eql(u8, base_dir, "."))
|
|
import_path
|
|
else
|
|
try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ base_dir, import_path });
|
|
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{resolved});
|
|
const range = lsp.Range{
|
|
.start = .{ .line = 0, .character = 0 },
|
|
.end = .{ .line = 0, .character = 0 },
|
|
};
|
|
const loc_json = try lsp.locationJson(self.allocator, target_uri, range);
|
|
try self.sendResponse(id_json, loc_json);
|
|
return;
|
|
}
|
|
|
|
// 5. Fallback: identifier in string interpolation
|
|
if (extractIdentAtOffset(doc.source, offset)) |name| {
|
|
const name_start = @as(u32, @intCast(@intFromPtr(name.ptr) - @intFromPtr(doc.source.ptr)));
|
|
const is_qualified = name_start > 0 and doc.source[name_start - 1] == '.';
|
|
if (!is_qualified) {
|
|
if (findSymbolByName(sema.symbols, name)) |si| {
|
|
const sym = sema.symbols[si];
|
|
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
try self.sendResponse(id_json, "null");
|
|
}
|
|
|
|
// ---- Hover ----
|
|
|
|
fn handleHover(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
|
|
const ctx = try self.extractRequest(id, params) orelse return;
|
|
const pos = extractPosition(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, "null");
|
|
};
|
|
const sema = doc.sema orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
const root = doc.root orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
const offset = positionToOffset(doc.source, pos.line, pos.character) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
// Qualified name (e.g. std.print)
|
|
if (extractQualifiedName(doc.source, offset)) |qn| {
|
|
// Namespace member hover
|
|
if (self.findImportByNs(doc, qn.ns)) |imp| {
|
|
if (self.documents.get(imp.path)) |imp_doc| {
|
|
if (imp_doc.root) |imp_root| {
|
|
if (findDeclByName(imp_root, qn.member)) |decl| {
|
|
const hover_text = try formatDeclHover(self.allocator, decl, imp_doc.source);
|
|
const hover_json = try lsp.hoverJson(self.allocator, hover_text);
|
|
return try self.sendResponse(id_json, hover_json);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Struct field hover (e.g. point.x)
|
|
if (try self.formatStructFieldHover(doc, qn.ns, qn.member)) |hover_text| {
|
|
const hover_json = try lsp.hoverJson(self.allocator, hover_text);
|
|
return try self.sendResponse(id_json, hover_json);
|
|
}
|
|
}
|
|
|
|
// Enum variant hover
|
|
if (sx.sema.findNodeAtOffset(root, offset)) |node| {
|
|
if (node.data == .enum_literal) {
|
|
if (try self.formatEnumVariantHover(doc, node.data.enum_literal.name)) |hover_text| {
|
|
const hover_json = try lsp.hoverJson(self.allocator, hover_text);
|
|
return try self.sendResponse(id_json, hover_json);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find symbol via reference or definition name
|
|
var sym_idx: ?usize = null;
|
|
if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ref_idx| {
|
|
const si = sema.references[ref_idx].symbol_index;
|
|
if (si < sema.symbols.len) sym_idx = si;
|
|
} else {
|
|
sym_idx = findSymbolNameAtOffset(sema.symbols, doc.source, offset);
|
|
}
|
|
|
|
// Fallback: identifier in string interpolation
|
|
if (sym_idx == null) {
|
|
if (extractIdentAtOffset(doc.source, offset)) |name| {
|
|
const name_start = @as(u32, @intCast(@intFromPtr(name.ptr) - @intFromPtr(doc.source.ptr)));
|
|
const is_qualified = name_start > 0 and doc.source[name_start - 1] == '.';
|
|
if (!is_qualified) {
|
|
sym_idx = findSymbolByName(sema.symbols, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sym_idx) |si| {
|
|
const sym = sema.symbols[si];
|
|
// Resolve the right source and root for hover formatting
|
|
const hover_doc = self.resolveSymbolDoc(doc, sym);
|
|
const hover_root = hover_doc.root orelse root;
|
|
const hover_text = try formatSymbolHover(self.allocator, sym, hover_root, hover_doc.source);
|
|
const hover_json = try lsp.hoverJson(self.allocator, hover_text);
|
|
return try self.sendResponse(id_json, hover_json);
|
|
}
|
|
|
|
try self.sendResponse(id_json, "null");
|
|
}
|
|
|
|
// ---- Document symbols ----
|
|
|
|
fn handleDocumentSymbol(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 {
|
|
return try self.sendResponse(id_json, "[]");
|
|
};
|
|
|
|
var doc_symbols = std.ArrayList(lsp.DocumentSymbol).empty;
|
|
for (sema.symbols) |sym| {
|
|
if (sym.scope_depth != 0) continue;
|
|
// Only show symbols defined in this file
|
|
if (sym.origin != null) continue;
|
|
|
|
const kind: u32 = switch (sym.kind) {
|
|
.function => @intFromEnum(lsp.SymbolKindLsp.Function),
|
|
.variable => @intFromEnum(lsp.SymbolKindLsp.Variable),
|
|
.constant => @intFromEnum(lsp.SymbolKindLsp.Constant),
|
|
.enum_type => @intFromEnum(lsp.SymbolKindLsp.Enum),
|
|
.struct_type => @intFromEnum(lsp.SymbolKindLsp.Struct),
|
|
.type_alias => @intFromEnum(lsp.SymbolKindLsp.Class),
|
|
.param => @intFromEnum(lsp.SymbolKindLsp.Variable),
|
|
.namespace => @intFromEnum(lsp.SymbolKindLsp.Namespace),
|
|
};
|
|
|
|
const range = spanToRange(doc.source, sym.def_span);
|
|
try doc_symbols.append(self.allocator, .{
|
|
.name = sym.name,
|
|
.kind = kind,
|
|
.range = range,
|
|
.selection_range = range,
|
|
});
|
|
}
|
|
|
|
const symbols_json = try lsp.documentSymbolsJson(self.allocator, doc_symbols.items);
|
|
try self.sendResponse(id_json, symbols_json);
|
|
}
|
|
|
|
// ---- Completion ----
|
|
|
|
fn handleCompletion(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
|
|
const ctx = try self.extractRequest(id, params) orelse return;
|
|
const pos = extractPosition(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, "[]");
|
|
};
|
|
|
|
// Check if cursor is after a dot
|
|
if (positionToOffset(doc.source, pos.line, pos.character)) |off| {
|
|
var scan = off;
|
|
while (scan > 0 and isIdentChar(doc.source[scan - 1])) scan -= 1;
|
|
if (scan > 0 and doc.source[scan - 1] == '.') {
|
|
try self.handleDotCompletion(id_json, doc, scan);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Regular completion: all in-scope symbols + keywords
|
|
const sema = doc.sema orelse {
|
|
return try self.sendResponse(id_json, "[]");
|
|
};
|
|
|
|
var items = std.ArrayList(lsp.CompletionItem).empty;
|
|
|
|
for (sema.symbols) |sym| {
|
|
const kind: u32 = switch (sym.kind) {
|
|
.function => @intFromEnum(lsp.CompletionItemKind.Function),
|
|
.variable => @intFromEnum(lsp.CompletionItemKind.Variable),
|
|
.constant => @intFromEnum(lsp.CompletionItemKind.Constant),
|
|
.enum_type => @intFromEnum(lsp.CompletionItemKind.Enum),
|
|
.struct_type => @intFromEnum(lsp.CompletionItemKind.Struct),
|
|
.type_alias => @intFromEnum(lsp.CompletionItemKind.Class),
|
|
.param => @intFromEnum(lsp.CompletionItemKind.Variable),
|
|
.namespace => @intFromEnum(lsp.CompletionItemKind.Module),
|
|
};
|
|
|
|
const detail = if (sym.ty) |ty| try ty.displayName(self.allocator) else null;
|
|
|
|
try items.append(self.allocator, .{
|
|
.label = sym.name,
|
|
.kind = kind,
|
|
.detail = detail,
|
|
});
|
|
}
|
|
|
|
const keywords = [_][]const u8{
|
|
"if", "else", "then", "return", "defer",
|
|
"case", "break", "enum", "struct", "true",
|
|
"false", "xx", "while", "continue",
|
|
"and", "or", "union",
|
|
};
|
|
|
|
const builtins = [_]struct { label: []const u8, detail: []const u8 }{
|
|
.{ .label = "type_of", .detail = "(val: $T) -> Type" },
|
|
.{ .label = "type_name", .detail = "($T: Type) -> string" },
|
|
.{ .label = "field_count", .detail = "($T: Type) -> s32" },
|
|
.{ .label = "field_name", .detail = "($T: Type, idx: s32) -> string" },
|
|
.{ .label = "field_value", .detail = "(s: $T, idx: s32) -> Any" },
|
|
.{ .label = "size_of", .detail = "($T: Type) -> s32" },
|
|
.{ .label = "cast", .detail = "(Type) expr — prefix type cast" },
|
|
.{ .label = "alloc", .detail = "(size: s32) -> string" },
|
|
.{ .label = "sqrt", .detail = "(x: $T) -> T" },
|
|
};
|
|
for (&keywords) |kw| {
|
|
try items.append(self.allocator, .{
|
|
.label = kw,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Keyword),
|
|
});
|
|
}
|
|
for (&builtins) |b| {
|
|
try items.append(self.allocator, .{
|
|
.label = b.label,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Function),
|
|
.detail = b.detail,
|
|
});
|
|
}
|
|
|
|
const items_json = try lsp.completionItemsJson(self.allocator, items.items);
|
|
try self.sendResponse(id_json, items_json);
|
|
}
|
|
|
|
fn handleDotCompletion(self: *Server, id_json: []const u8, doc: *const Document, cursor_offset: u32) !void {
|
|
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);
|
|
};
|
|
|
|
// 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 {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const items_json = try lsp.completionListJson(self.allocator, items.items);
|
|
try self.sendResponse(id_json, items_json);
|
|
}
|
|
|
|
fn collectDeclCompletions(allocator: std.mem.Allocator, items: *std.ArrayList(lsp.CompletionItem), decls: []const *sx.ast.Node) !void {
|
|
for (decls) |decl| {
|
|
switch (decl.data) {
|
|
.fn_decl => |fd| {
|
|
var detail_buf = std.ArrayList(u8).empty;
|
|
try detail_buf.append(allocator, '(');
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (pi > 0) try detail_buf.appendSlice(allocator, ", ");
|
|
try detail_buf.appendSlice(allocator, param.name);
|
|
try detail_buf.appendSlice(allocator, ": ");
|
|
if (param.type_expr.data == .type_expr) {
|
|
try detail_buf.appendSlice(allocator, param.type_expr.data.type_expr.name);
|
|
} else {
|
|
try detail_buf.appendSlice(allocator, "?");
|
|
}
|
|
}
|
|
try detail_buf.append(allocator, ')');
|
|
if (fd.return_type) |rt| {
|
|
try detail_buf.appendSlice(allocator, " -> ");
|
|
if (rt.data == .type_expr) {
|
|
try detail_buf.appendSlice(allocator, rt.data.type_expr.name);
|
|
}
|
|
}
|
|
try items.append(allocator, .{
|
|
.label = fd.name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Function),
|
|
.detail = detail_buf.items,
|
|
});
|
|
},
|
|
.const_decl => |cd| {
|
|
const kind: u32 = if (cd.value.data == .lambda)
|
|
@intFromEnum(lsp.CompletionItemKind.Function)
|
|
else
|
|
@intFromEnum(lsp.CompletionItemKind.Constant);
|
|
try items.append(allocator, .{
|
|
.label = cd.name,
|
|
.kind = kind,
|
|
});
|
|
},
|
|
.enum_decl => |ed| {
|
|
try items.append(allocator, .{
|
|
.label = ed.name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Enum),
|
|
});
|
|
},
|
|
.struct_decl => |sd| {
|
|
try items.append(allocator, .{
|
|
.label = sd.name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Struct),
|
|
});
|
|
},
|
|
.union_decl => |ud| {
|
|
try items.append(allocator, .{
|
|
.label = ud.name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Struct),
|
|
});
|
|
},
|
|
.var_decl => |vd| {
|
|
try items.append(allocator, .{
|
|
.label = vd.name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Variable),
|
|
});
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collectMemberCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), sema: SemaResult, root: *sx.ast.Node, name: []const u8) !void {
|
|
for (sema.symbols) |sym| {
|
|
if (!std.mem.eql(u8, sym.name, name)) continue;
|
|
|
|
if (sym.kind == .struct_type) {
|
|
// For imported symbols, look up the source doc's AST
|
|
const lookup_root = if (sym.origin) |origin_path|
|
|
if (self.documents.get(origin_path)) |od| od.root orelse root else root
|
|
else
|
|
root;
|
|
if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| {
|
|
if (node.data == .struct_decl) {
|
|
const sd = node.data.struct_decl;
|
|
for (sd.field_names, 0..) |field_name, fi| {
|
|
const detail: ?[]const u8 = if (fi < sd.field_types.len) blk: {
|
|
const ft = sd.field_types[fi];
|
|
break :blk if (ft.data == .type_expr) ft.data.type_expr.name else null;
|
|
} else null;
|
|
|
|
try items.append(self.allocator, .{
|
|
.label = field_name,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.Field),
|
|
.detail = detail,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else if (sym.kind == .enum_type) {
|
|
const lookup_root = if (sym.origin) |origin_path|
|
|
if (self.documents.get(origin_path)) |od| od.root orelse root else root
|
|
else
|
|
root;
|
|
if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| {
|
|
if (node.data == .enum_decl) {
|
|
const ed = node.data.enum_decl;
|
|
for (ed.variant_names) |variant| {
|
|
try items.append(self.allocator, .{
|
|
.label = variant,
|
|
.kind = @intFromEnum(lsp.CompletionItemKind.EnumMember),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---- Signature help ----
|
|
|
|
fn handleSignatureHelp(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
|
|
const req = try self.extractRequest(id, params) orelse return;
|
|
const pos = extractPosition(params) orelse return;
|
|
const id_json = req.id_json;
|
|
const file_path = uriToFilePath(req.uri) orelse "";
|
|
|
|
const doc = self.documents.get(file_path) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
const offset = positionToOffset(doc.source, pos.line, pos.character) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
const call_ctx = findCallContext(doc.source, offset) orelse {
|
|
return try self.sendResponse(id_json, "null");
|
|
};
|
|
|
|
// Built-in function signatures
|
|
const builtin_sigs = [_]struct { name: []const u8, label: []const u8, params: []const []const u8 }{
|
|
.{ .name = "type_of", .label = "type_of(val: $T) -> Type", .params = &.{"val: $T"} },
|
|
.{ .name = "type_name", .label = "type_name($T: Type) -> string", .params = &.{"$T: Type"} },
|
|
.{ .name = "field_count", .label = "field_count($T: Type) -> s32", .params = &.{"$T: Type"} },
|
|
.{ .name = "field_name", .label = "field_name($T: Type, idx: s32) -> string", .params = &.{ "$T: Type", "idx: s32" } },
|
|
.{ .name = "field_value", .label = "field_value(s: $T, idx: s32) -> Any", .params = &.{ "s: $T", "idx: s32" } },
|
|
.{ .name = "size_of", .label = "size_of($T: Type) -> s32", .params = &.{"$T: Type"} },
|
|
.{ .name = "cast", .label = "cast(Type) expr", .params = &.{"Type"} },
|
|
.{ .name = "alloc", .label = "alloc(size: s32) -> string", .params = &.{"size: s32"} },
|
|
.{ .name = "sqrt", .label = "sqrt(x: $T) -> T", .params = &.{"x: $T"} },
|
|
.{ .name = "print", .label = "print(fmt: string, args: ..Any)", .params = &.{ "fmt: string", "args: ..Any" } },
|
|
.{ .name = "write", .label = "write(str: string) -> void", .params = &.{"str: string"} },
|
|
};
|
|
for (&builtin_sigs) |b| {
|
|
const matches = std.mem.eql(u8, call_ctx.name, b.name) or
|
|
(std.mem.startsWith(u8, call_ctx.name, "std.") and std.mem.eql(u8, call_ctx.name[4..], b.name));
|
|
if (matches) {
|
|
const sig_json = try lsp.signatureHelpJson(self.allocator, b.label, b.params, call_ctx.active_param);
|
|
return try self.sendResponse(id_json, sig_json);
|
|
}
|
|
}
|
|
|
|
// Look up function declaration
|
|
const fn_node = self.findFnDeclByName(doc, call_ctx.name);
|
|
|
|
if (fn_node) |fd| {
|
|
var label_buf = std.ArrayList(u8).empty;
|
|
try label_buf.appendSlice(self.allocator, fd.name);
|
|
try label_buf.append(self.allocator, '(');
|
|
|
|
var param_labels = std.ArrayList([]const u8).empty;
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (pi > 0) try label_buf.appendSlice(self.allocator, ", ");
|
|
const param_start = label_buf.items.len;
|
|
try label_buf.appendSlice(self.allocator, param.name);
|
|
try label_buf.appendSlice(self.allocator, ": ");
|
|
if (param.type_expr.data == .type_expr) {
|
|
try label_buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name);
|
|
} else {
|
|
try label_buf.appendSlice(self.allocator, "?");
|
|
}
|
|
const param_label = try self.allocator.dupe(u8, label_buf.items[param_start..]);
|
|
try param_labels.append(self.allocator, param_label);
|
|
}
|
|
try label_buf.append(self.allocator, ')');
|
|
|
|
if (fd.return_type) |rt| {
|
|
try label_buf.appendSlice(self.allocator, " -> ");
|
|
if (rt.data == .type_expr) {
|
|
try label_buf.appendSlice(self.allocator, rt.data.type_expr.name);
|
|
}
|
|
}
|
|
|
|
const sig_json = try lsp.signatureHelpJson(self.allocator, label_buf.items, param_labels.items, call_ctx.active_param);
|
|
return try self.sendResponse(id_json, sig_json);
|
|
}
|
|
|
|
try self.sendResponse(id_json, "null");
|
|
}
|
|
|
|
// ---- Semantic tokens ----
|
|
|
|
fn handleSemanticTokens(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, "{\"data\":[]}");
|
|
};
|
|
const sema = doc.sema orelse {
|
|
return try self.sendResponse(id_json, "{\"data\":[]}");
|
|
};
|
|
|
|
var data = std.ArrayList(u32).empty;
|
|
var prev_line: u32 = 0;
|
|
var prev_char: u32 = 0;
|
|
|
|
var lexer = sx.lexer.Lexer.init(doc.source);
|
|
while (true) {
|
|
const tok = lexer.next();
|
|
if (tok.tag == .eof) break;
|
|
|
|
if (tok.tag == .string_literal or tok.tag == .raw_string_literal) {
|
|
try emitStringParts(&data, self.allocator, doc.source, tok.loc.start, tok.loc.end, &prev_line, &prev_char);
|
|
continue;
|
|
}
|
|
|
|
const token_type = classifyToken(tok, sema, doc.source) orelse continue;
|
|
try emitToken(&data, self.allocator, doc.source, tok.loc.start, tok.loc.end, token_type, &prev_line, &prev_char);
|
|
}
|
|
|
|
const result_json = try lsp.semanticTokensJson(self.allocator, data.items);
|
|
try self.sendResponse(id_json, result_json);
|
|
}
|
|
|
|
fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
|
|
const ST = lsp.SemanticTokenType;
|
|
return switch (tok.tag) {
|
|
.kw_if,
|
|
.kw_else,
|
|
.kw_then,
|
|
.kw_true,
|
|
.kw_false,
|
|
.kw_enum,
|
|
.kw_case,
|
|
.kw_break,
|
|
.kw_continue,
|
|
.kw_while,
|
|
.kw_for,
|
|
.kw_return,
|
|
.kw_defer,
|
|
.kw_struct,
|
|
.kw_union,
|
|
.kw_xx,
|
|
.kw_and,
|
|
.kw_or,
|
|
.kw_null,
|
|
.hash_run,
|
|
.hash_import,
|
|
.hash_insert,
|
|
.hash_builtin,
|
|
.hash_foreign,
|
|
.hash_library,
|
|
=> ST.keyword,
|
|
|
|
.kw_f32, .kw_f64, .kw_Type => ST.type_,
|
|
|
|
.int_literal, .float_literal => ST.number,
|
|
.string_literal, .raw_string_literal => null,
|
|
|
|
.plus,
|
|
.minus,
|
|
.star,
|
|
.slash,
|
|
.equal,
|
|
.equal_equal,
|
|
.bang,
|
|
.bang_equal,
|
|
.less,
|
|
.less_equal,
|
|
.greater,
|
|
.greater_equal,
|
|
.plus_equal,
|
|
.minus_equal,
|
|
.star_equal,
|
|
.slash_equal,
|
|
.percent,
|
|
.percent_equal,
|
|
.ampersand,
|
|
.pipe,
|
|
.arrow,
|
|
.fat_arrow,
|
|
.colon_colon,
|
|
.colon_equal,
|
|
.triple_minus,
|
|
=> ST.operator_,
|
|
|
|
.identifier => classifyIdentifier(tok, sema, source),
|
|
|
|
.colon,
|
|
.semicolon,
|
|
.comma,
|
|
.dot,
|
|
.dot_dot,
|
|
.dollar,
|
|
.l_paren,
|
|
.r_paren,
|
|
.l_brace,
|
|
.r_brace,
|
|
.l_bracket,
|
|
.r_bracket,
|
|
.eof,
|
|
.invalid,
|
|
=> null,
|
|
};
|
|
}
|
|
|
|
fn classifyIdentifier(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
|
|
const ST = lsp.SemanticTokenType;
|
|
const offset = tok.loc.start;
|
|
if (tok.loc.start >= source.len or tok.loc.end > source.len or tok.loc.start >= tok.loc.end) return null;
|
|
const name = source[tok.loc.start..tok.loc.end];
|
|
|
|
if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ref_idx| {
|
|
const si = sema.references[ref_idx].symbol_index;
|
|
if (si >= sema.symbols.len) return null;
|
|
const sym = sema.symbols[si];
|
|
return symbolKindToTokenType(sym.kind);
|
|
}
|
|
|
|
for (sema.symbols) |sym| {
|
|
if (sym.def_span.start == offset and std.mem.eql(u8, sym.name, name)) {
|
|
return symbolKindToTokenType(sym.kind);
|
|
}
|
|
}
|
|
|
|
if (sx.types.Type.fromName(name) != null) {
|
|
return ST.type_;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn symbolKindToTokenType(kind: sx.sema.SymbolKind) u32 {
|
|
const ST = lsp.SemanticTokenType;
|
|
return switch (kind) {
|
|
.function => ST.function,
|
|
.variable => ST.variable,
|
|
.constant => ST.variable,
|
|
.param => ST.parameter,
|
|
.enum_type => ST.enum_,
|
|
.struct_type => ST.struct_,
|
|
.type_alias => ST.type_,
|
|
.namespace => ST.namespace,
|
|
};
|
|
}
|
|
|
|
fn emitToken(
|
|
data: *std.ArrayList(u32),
|
|
allocator: std.mem.Allocator,
|
|
source: [:0]const u8,
|
|
start: u32,
|
|
end: u32,
|
|
token_type: u32,
|
|
prev_line: *u32,
|
|
prev_char: *u32,
|
|
) !void {
|
|
if (start >= source.len or end > source.len or start >= end) return;
|
|
const loc = sx.errors.SourceLoc.compute(source, start);
|
|
if (loc.line == 0 or loc.col == 0) return;
|
|
const line = loc.line - 1;
|
|
const col = loc.col - 1;
|
|
const length = end - start;
|
|
|
|
if (line < prev_line.*) return;
|
|
const delta_line = line - prev_line.*;
|
|
const delta_char = if (delta_line == 0) (if (col >= prev_char.*) col - prev_char.* else return) else col;
|
|
|
|
try data.append(allocator, delta_line);
|
|
try data.append(allocator, delta_char);
|
|
try data.append(allocator, length);
|
|
try data.append(allocator, token_type);
|
|
try data.append(allocator, 0);
|
|
|
|
prev_line.* = line;
|
|
prev_char.* = col;
|
|
}
|
|
|
|
fn emitStringSegment(
|
|
data: *std.ArrayList(u32),
|
|
allocator: std.mem.Allocator,
|
|
source: [:0]const u8,
|
|
seg_start: u32,
|
|
seg_end: u32,
|
|
token_type: u32,
|
|
prev_line: *u32,
|
|
prev_char: *u32,
|
|
) !void {
|
|
var line_start = seg_start;
|
|
var pos = seg_start;
|
|
while (pos < seg_end) : (pos += 1) {
|
|
if (source[pos] == '\n') {
|
|
if (pos + 1 > line_start) {
|
|
try emitToken(data, allocator, source, line_start, pos + 1, token_type, prev_line, prev_char);
|
|
}
|
|
line_start = pos + 1;
|
|
}
|
|
}
|
|
if (line_start < seg_end) {
|
|
try emitToken(data, allocator, source, line_start, seg_end, token_type, prev_line, prev_char);
|
|
}
|
|
}
|
|
|
|
fn emitStringParts(
|
|
data: *std.ArrayList(u32),
|
|
allocator: std.mem.Allocator,
|
|
source: [:0]const u8,
|
|
tok_start: u32,
|
|
tok_end: u32,
|
|
prev_line: *u32,
|
|
prev_char: *u32,
|
|
) !void {
|
|
const ST = lsp.SemanticTokenType;
|
|
var pos = tok_start;
|
|
var seg_start = tok_start;
|
|
var in_interp = false;
|
|
|
|
while (pos < tok_end) : (pos += 1) {
|
|
if (in_interp) {
|
|
if (source[pos] == '}') {
|
|
in_interp = false;
|
|
seg_start = pos + 1;
|
|
}
|
|
} else {
|
|
if (source[pos] == '\\' and pos + 1 < tok_end) {
|
|
pos += 1;
|
|
} else if (source[pos] == '{') {
|
|
if (pos > seg_start) {
|
|
try emitStringSegment(data, allocator, source, seg_start, pos, ST.string_, prev_line, prev_char);
|
|
}
|
|
in_interp = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!in_interp and seg_start < tok_end) {
|
|
try emitStringSegment(data, allocator, source, seg_start, tok_end, ST.string_, prev_line, prev_char);
|
|
}
|
|
}
|
|
|
|
// ---- Core analysis pipeline ----
|
|
|
|
fn analyzeAndPublish(self: *Server, uri: []const u8, text: []const u8, version: i64) !void {
|
|
const file_path = uriToFilePath(uri) orelse "";
|
|
const source = try self.allocator.dupeZ(u8, text);
|
|
|
|
const doc = try self.documents.openOrUpdate(file_path, source, version);
|
|
self.documents.analyzeDocument(doc) catch {};
|
|
|
|
// Publish diagnostics from sema
|
|
if (doc.sema) |sema| {
|
|
try self.sendDiagnostics(uri, semaToLspDiags(self.allocator, doc.source, sema.diagnostics));
|
|
} else {
|
|
try self.sendDiagnostics(uri, &.{});
|
|
}
|
|
}
|
|
|
|
fn semaToLspDiags(allocator: std.mem.Allocator, source: [:0]const u8, diags: []const sx.errors.Diagnostic) []const lsp.Diagnostic {
|
|
var result = std.ArrayList(lsp.Diagnostic).empty;
|
|
for (diags) |d| {
|
|
const range = if (d.span) |span| spanToRange(source, span) else lsp.Range{
|
|
.start = .{ .line = 0, .character = 0 },
|
|
.end = .{ .line = 0, .character = 1 },
|
|
};
|
|
const severity: u32 = switch (d.level) {
|
|
.err => 1,
|
|
.warn => 2,
|
|
.note => 3,
|
|
};
|
|
result.append(allocator, .{
|
|
.range = range,
|
|
.severity = severity,
|
|
.message = d.message,
|
|
}) catch continue;
|
|
}
|
|
return result.items;
|
|
}
|
|
|
|
fn sendDiagnostics(self: *Server, uri: []const u8, diagnostics: []const lsp.Diagnostic) !void {
|
|
const params_json = try lsp.publishDiagnosticsJson(self.allocator, uri, diagnostics);
|
|
const body = try lsp.jsonRpcNotification(self.allocator, "textDocument/publishDiagnostics", params_json);
|
|
try self.transport.writeMessage(body);
|
|
}
|
|
|
|
// ---- Symbol resolution helpers ----
|
|
|
|
/// Send a Location response for a symbol, resolving to the correct file via origin.
|
|
fn sendSymbolLocation(self: *Server, id_json: []const u8, doc: *const Document, sym: sx.sema.Symbol) !bool {
|
|
if (sym.origin) |origin_path| {
|
|
// Symbol is from an imported file
|
|
const origin_doc = self.documents.get(origin_path) orelse return false;
|
|
const range = spanToRange(origin_doc.source, sym.def_span);
|
|
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{origin_path});
|
|
const loc_json = try lsp.locationJson(self.allocator, target_uri, range);
|
|
try self.sendResponse(id_json, loc_json);
|
|
return true;
|
|
} else {
|
|
// Symbol is local
|
|
const range = spanToRange(doc.source, sym.def_span);
|
|
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{doc.path});
|
|
const loc_json = try lsp.locationJson(self.allocator, target_uri, range);
|
|
try self.sendResponse(id_json, loc_json);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// Resolve which document a symbol belongs to (for hover/source lookup).
|
|
fn resolveSymbolDoc(self: *Server, doc: *const Document, sym: sx.sema.Symbol) *const Document {
|
|
if (sym.origin) |origin_path| {
|
|
return self.documents.get(origin_path) orelse doc;
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
/// Find an import by namespace name.
|
|
fn findImportByNs(_: *Server, doc: *const Document, ns_name: []const u8) ?doc_mod.Import {
|
|
for (doc.imports) |imp| {
|
|
if (imp.ns) |ns| {
|
|
if (std.mem.eql(u8, ns, ns_name)) return imp;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Find a fn_decl by name. Supports dotted names like "ns.func".
|
|
fn findFnDeclByName(self: *Server, doc: *const Document, name: []const u8) ?sx.ast.FnDecl {
|
|
// Check for dotted name (e.g. "std.print")
|
|
if (std.mem.indexOfScalar(u8, name, '.')) |dot_idx| {
|
|
const ns_name = name[0..dot_idx];
|
|
const fn_name = name[dot_idx + 1 ..];
|
|
|
|
if (self.findImportByNs(doc, ns_name)) |imp| {
|
|
if (self.documents.get(imp.path)) |imp_doc| {
|
|
if (imp_doc.root) |imp_root| {
|
|
if (imp_root.data == .root) {
|
|
for (imp_root.data.root.decls) |decl| {
|
|
if (decl.data == .fn_decl and std.mem.eql(u8, decl.data.fn_decl.name, fn_name)) {
|
|
return decl.data.fn_decl;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Top-level lookup in current doc
|
|
const root = doc.root orelse return null;
|
|
const func_name = extractLastSegment(name);
|
|
if (root.data == .root) {
|
|
for (root.data.root.decls) |decl| {
|
|
if (decl.data == .fn_decl and std.mem.eql(u8, decl.data.fn_decl.name, func_name)) {
|
|
return decl.data.fn_decl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check imported docs for flat-imported functions
|
|
for (doc.imports) |imp| {
|
|
if (imp.ns != null) continue; // skip namespaced
|
|
if (self.documents.get(imp.path)) |imp_doc| {
|
|
if (imp_doc.root) |imp_root| {
|
|
if (imp_root.data == .root) {
|
|
for (imp_root.data.root.decls) |decl| {
|
|
if (decl.data == .fn_decl and std.mem.eql(u8, decl.data.fn_decl.name, func_name)) {
|
|
return decl.data.fn_decl;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// UFCS completions: free functions whose first param matches the given type.
|
|
fn collectUfcsCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), root: *sx.ast.Node, type_name: []const u8) !void {
|
|
if (root.data != .root) return;
|
|
for (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,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ---- Hover helpers ----
|
|
|
|
fn formatStructFieldHover(self: *Server, doc: *const Document, obj_name: []const u8, field_name: []const u8) !?[]const u8 {
|
|
const sema = doc.sema orelse return null;
|
|
const struct_name = resolveStructTypeName(sema, doc, obj_name) orelse return null;
|
|
|
|
for (sema.symbols) |sym| {
|
|
if (sym.kind != .struct_type or !std.mem.eql(u8, sym.name, struct_name)) continue;
|
|
|
|
const lookup_doc = self.resolveSymbolDoc(doc, sym);
|
|
const lookup_root = lookup_doc.root orelse return null;
|
|
|
|
if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| {
|
|
if (node.data == .struct_decl) {
|
|
const sd = node.data.struct_decl;
|
|
for (sd.field_names, 0..) |fn_, fi| {
|
|
if (!std.mem.eql(u8, fn_, field_name)) continue;
|
|
|
|
var buf = std.ArrayList(u8).empty;
|
|
|
|
// Doc comment above field
|
|
const fn_addr = @intFromPtr(fn_.ptr);
|
|
const src_addr = @intFromPtr(lookup_doc.source.ptr);
|
|
const src_end = src_addr + lookup_doc.source.len;
|
|
if (fn_addr >= src_addr and fn_addr < src_end) {
|
|
const field_offset = @as(u32, @intCast(fn_addr - src_addr));
|
|
if (extractDocComment(lookup_doc.source, field_offset)) |comment| {
|
|
try buf.appendSlice(self.allocator, comment);
|
|
try buf.appendSlice(self.allocator, "\n\n");
|
|
}
|
|
}
|
|
|
|
try buf.appendSlice(self.allocator, "```sx\n");
|
|
try buf.appendSlice(self.allocator, struct_name);
|
|
try buf.append(self.allocator, '.');
|
|
try buf.appendSlice(self.allocator, field_name);
|
|
if (fi < sd.field_types.len) {
|
|
if (sd.field_types[fi].data == .type_expr) {
|
|
try buf.appendSlice(self.allocator, " : ");
|
|
try buf.appendSlice(self.allocator, sd.field_types[fi].data.type_expr.name);
|
|
}
|
|
}
|
|
try buf.appendSlice(self.allocator, "\n```");
|
|
return buf.items;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn formatEnumVariantHover(self: *Server, doc: *const Document, variant_name: []const u8) !?[]const u8 {
|
|
const sema = doc.sema orelse return null;
|
|
|
|
for (sema.symbols) |sym| {
|
|
if (sym.kind != .enum_type) continue;
|
|
|
|
const lookup_doc = self.resolveSymbolDoc(doc, sym);
|
|
const lookup_root = lookup_doc.root orelse continue;
|
|
|
|
if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| {
|
|
if (node.data == .enum_decl) {
|
|
const ed = node.data.enum_decl;
|
|
for (ed.variant_names) |v| {
|
|
if (!std.mem.eql(u8, v, variant_name)) continue;
|
|
|
|
var buf = std.ArrayList(u8).empty;
|
|
|
|
const v_addr = @intFromPtr(v.ptr);
|
|
const src_addr2 = @intFromPtr(lookup_doc.source.ptr);
|
|
const src_end2 = src_addr2 + lookup_doc.source.len;
|
|
if (v_addr >= src_addr2 and v_addr < src_end2) {
|
|
const variant_offset = @as(u32, @intCast(v_addr - src_addr2));
|
|
if (extractDocComment(lookup_doc.source, variant_offset)) |comment| {
|
|
try buf.appendSlice(self.allocator, comment);
|
|
try buf.appendSlice(self.allocator, "\n\n");
|
|
}
|
|
}
|
|
|
|
try buf.appendSlice(self.allocator, "```sx\n");
|
|
try buf.appendSlice(self.allocator, sym.name);
|
|
try buf.append(self.allocator, '.');
|
|
try buf.appendSlice(self.allocator, variant_name);
|
|
try buf.appendSlice(self.allocator, "\n```");
|
|
return buf.items;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn resolveVariableType(sema: SemaResult, var_name: []const u8) ?[]const u8 {
|
|
var i = sema.symbols.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const sym = 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;
|
|
}
|
|
|
|
fn resolveStructTypeName(sema: SemaResult, doc: *const Document, var_name: []const u8) ?[]const u8 {
|
|
var i = sema.symbols.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const sym = sema.symbols[i];
|
|
if (!std.mem.eql(u8, sym.name, var_name)) continue;
|
|
|
|
const ty = sym.ty orelse return null;
|
|
if (ty != .struct_type) return null;
|
|
|
|
if (sym.kind == .param) {
|
|
if (sym.def_span.start < doc.source.len and sym.def_span.end <= doc.source.len) {
|
|
return doc.source[sym.def_span.start..sym.def_span.end];
|
|
}
|
|
}
|
|
|
|
if (sym.kind == .variable) {
|
|
const root = doc.root orelse return null;
|
|
if (sym.def_span.start < doc.source.len) {
|
|
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
|
|
if (node.data == .var_decl) {
|
|
if (node.data.var_decl.type_annotation) |ta| {
|
|
if (ta.data == .type_expr) return ta.data.type_expr.name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ---- JSON helpers ----
|
|
|
|
fn jsonGet(val: std.json.Value, key: []const u8) ?std.json.Value {
|
|
return switch (val) {
|
|
.object => |obj| obj.get(key),
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn jsonStr(val: std.json.Value) ?[]const u8 {
|
|
return switch (val) {
|
|
.string => |s| s,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn jsonInt(val: std.json.Value) ?i64 {
|
|
return switch (val) {
|
|
.integer => |i| i,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn jsonArr(val: std.json.Value) ?[]std.json.Value {
|
|
return switch (val) {
|
|
.array => |a| a.items,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
// ---- Text helpers ----
|
|
|
|
fn findSymbolNameAtOffset(symbols: []const sx.sema.Symbol, source: [:0]const u8, offset: u32) ?usize {
|
|
for (symbols, 0..) |sym, i| {
|
|
if (sym.origin != null) continue; // skip imported symbols
|
|
const name_start = sym.def_span.start;
|
|
const name_end = name_start + @as(u32, @intCast(sym.name.len));
|
|
if (offset >= name_start and offset < name_end and name_end <= source.len) {
|
|
if (std.mem.eql(u8, source[name_start..name_end], sym.name)) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn positionToOffset(source: []const u8, line: u32, character: u32) ?u32 {
|
|
var cur_line: u32 = 0;
|
|
var cur_col: u32 = 0;
|
|
for (source, 0..) |ch, i| {
|
|
if (cur_line == line and cur_col == character) {
|
|
return @intCast(i);
|
|
}
|
|
if (ch == '\n') {
|
|
if (cur_line == line) return @intCast(i);
|
|
cur_line += 1;
|
|
cur_col = 0;
|
|
} else {
|
|
cur_col += 1;
|
|
}
|
|
}
|
|
if (cur_line == line and cur_col == character) {
|
|
return @intCast(source.len);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn extractDotPrefix(source: []const u8, cursor_offset: u32) ?[]const u8 {
|
|
if (cursor_offset < 2) return null;
|
|
const dot_pos = cursor_offset - 1;
|
|
if (source[dot_pos] != '.') return null;
|
|
|
|
var start: u32 = dot_pos;
|
|
while (start > 0) {
|
|
const ch = source[start - 1];
|
|
if (std.ascii.isAlphanumeric(ch) or ch == '_' or ch == '.') {
|
|
start -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (start == dot_pos) return null;
|
|
const prefix = source[start..dot_pos];
|
|
|
|
var trimmed_start: usize = 0;
|
|
while (trimmed_start < prefix.len and prefix[trimmed_start] == '.') {
|
|
trimmed_start += 1;
|
|
}
|
|
if (trimmed_start == prefix.len) return null;
|
|
return prefix[trimmed_start..];
|
|
}
|
|
|
|
fn findCallContext(source: []const u8, cursor_offset: u32) ?struct { name: []const u8, active_param: u32 } {
|
|
if (cursor_offset == 0) return null;
|
|
|
|
var depth: i32 = 0;
|
|
var comma_count: u32 = 0;
|
|
var pos: u32 = cursor_offset;
|
|
|
|
while (pos > 0) {
|
|
pos -= 1;
|
|
const ch = source[pos];
|
|
|
|
if (ch == ')') {
|
|
depth += 1;
|
|
} else if (ch == '(') {
|
|
if (depth == 0) {
|
|
if (pos == 0) return null;
|
|
const name_end: u32 = pos;
|
|
var name_start: u32 = pos;
|
|
while (name_start > 0) {
|
|
const nc = source[name_start - 1];
|
|
if (std.ascii.isAlphanumeric(nc) or nc == '_' or nc == '.') {
|
|
name_start -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (name_start == name_end) return null;
|
|
var trimmed = name_start;
|
|
while (trimmed < name_end and source[trimmed] == '.') {
|
|
trimmed += 1;
|
|
}
|
|
if (trimmed == name_end) return null;
|
|
return .{
|
|
.name = source[trimmed..name_end],
|
|
.active_param = comma_count,
|
|
};
|
|
}
|
|
depth -= 1;
|
|
} else if (ch == ',' and depth == 0) {
|
|
comma_count += 1;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn extractLastSegment(name: []const u8) []const u8 {
|
|
var i = name.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
if (name[i] == '.') {
|
|
return name[i + 1 ..];
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
fn extractIdentAtOffset(source: []const u8, offset: u32) ?[]const u8 {
|
|
if (offset >= source.len) return null;
|
|
var start: u32 = offset;
|
|
while (start > 0 and isIdentChar(source[start - 1])) {
|
|
start -= 1;
|
|
}
|
|
var end: u32 = offset;
|
|
while (end < source.len and isIdentChar(source[end])) {
|
|
end += 1;
|
|
}
|
|
if (start == end) return null;
|
|
return source[start..end];
|
|
}
|
|
|
|
fn isIdentChar(c: u8) bool {
|
|
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_';
|
|
}
|
|
|
|
fn findImportPathAtOffset(source: []const u8, offset: u32) ?[]const u8 {
|
|
if (offset >= source.len) return null;
|
|
|
|
var qstart: u32 = offset;
|
|
while (qstart > 0 and source[qstart] != '"') : (qstart -= 1) {}
|
|
if (source[qstart] != '"') return null;
|
|
|
|
const qend: u32 = if (offset < source.len and source[offset] == '"')
|
|
offset
|
|
else blk: {
|
|
var e = offset;
|
|
while (e < source.len and source[e] != '"' and source[e] != '\n') : (e += 1) {}
|
|
if (e >= source.len or source[e] != '"') return null;
|
|
break :blk e;
|
|
};
|
|
|
|
if (qstart == qend) return null;
|
|
|
|
var scan = qstart;
|
|
while (scan > 0 and (source[scan - 1] == ' ' or source[scan - 1] == '\t')) : (scan -= 1) {}
|
|
const kw = "#import";
|
|
if (scan < kw.len) return null;
|
|
if (!std.mem.eql(u8, source[scan - kw.len .. scan], kw)) return null;
|
|
|
|
return source[qstart + 1 .. qend];
|
|
}
|
|
|
|
fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8 } {
|
|
if (offset >= source.len) return null;
|
|
|
|
var end: u32 = offset;
|
|
while (end < source.len and isIdentChar(source[end])) end += 1;
|
|
var start: u32 = offset;
|
|
while (start > 0 and isIdentChar(source[start - 1])) start -= 1;
|
|
|
|
if (start == end) return null;
|
|
|
|
if (start >= 2 and source[start - 1] == '.') {
|
|
var ns_start: u32 = start - 1;
|
|
while (ns_start > 0 and isIdentChar(source[ns_start - 1])) ns_start -= 1;
|
|
if (ns_start < start - 1) {
|
|
return .{
|
|
.ns = source[ns_start .. start - 1],
|
|
.member = source[start..end],
|
|
};
|
|
}
|
|
}
|
|
|
|
if (end < source.len and source[end] == '.') {
|
|
var member_end: u32 = end + 1;
|
|
while (member_end < source.len and isIdentChar(source[member_end])) member_end += 1;
|
|
if (member_end > end + 1) {
|
|
return .{
|
|
.ns = source[start..end],
|
|
.member = source[end + 1 .. member_end],
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn findSymbolByName(symbols: []const sx.sema.Symbol, name: []const u8) ?usize {
|
|
var i = symbols.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
if (std.mem.eql(u8, symbols[i].name, name)) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn uriToFilePath(uri: []const u8) ?[]const u8 {
|
|
if (std.mem.startsWith(u8, uri, "file://")) {
|
|
return uri[7..];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn spanToRange(source: [:0]const u8, span: sx.ast.Span) lsp.Range {
|
|
const clamped_start = @min(span.start, @as(u32, @intCast(source.len)));
|
|
const clamped_end = @min(span.end, @as(u32, @intCast(source.len)));
|
|
const start = sx.errors.SourceLoc.compute(source, clamped_start);
|
|
const end = sx.errors.SourceLoc.compute(source, clamped_end);
|
|
return .{
|
|
.start = .{ .line = start.line - 1, .character = start.col - 1 },
|
|
.end = .{ .line = end.line - 1, .character = end.col - 1 },
|
|
};
|
|
}
|
|
|
|
fn extractDocComment(source: []const u8, def_start: u32) ?[]const u8 {
|
|
if (def_start == 0 or def_start > source.len) return null;
|
|
|
|
var pos: u32 = def_start;
|
|
while (pos > 0 and source[pos - 1] != '\n') : (pos -= 1) {}
|
|
if (pos == 0) return null;
|
|
|
|
const block_end = pos;
|
|
var block_start = pos;
|
|
|
|
while (block_start > 0) {
|
|
var scan = block_start - 1;
|
|
while (scan > 0 and source[scan - 1] != '\n') : (scan -= 1) {}
|
|
const line = std.mem.trimEnd(u8, source[scan..block_start], "\r\n");
|
|
const trimmed = std.mem.trimStart(u8, line, " \t");
|
|
if (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == '/') {
|
|
block_start = scan;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (block_start >= block_end) return null;
|
|
var end_pos = block_end;
|
|
while (end_pos > block_start and (source[end_pos - 1] == '\n' or source[end_pos - 1] == '\r')) : (end_pos -= 1) {}
|
|
if (end_pos <= block_start) return null;
|
|
return source[block_start..end_pos];
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
fn findDeclByName(root: *sx.ast.Node, name: []const u8) ?*sx.ast.Node {
|
|
if (root.data != .root) return null;
|
|
for (root.data.root.decls) |decl| {
|
|
if (decl.data.declName()) |dn| {
|
|
if (std.mem.eql(u8, dn, name)) return decl;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn formatDeclHover(allocator: std.mem.Allocator, decl: *sx.ast.Node, source: []const u8) ![]const u8 {
|
|
var buf = std.ArrayList(u8).empty;
|
|
|
|
if (extractDocComment(source, decl.span.start)) |comment| {
|
|
try buf.appendSlice(allocator, comment);
|
|
try buf.appendSlice(allocator, "\n\n");
|
|
}
|
|
|
|
try buf.appendSlice(allocator, "```sx\n");
|
|
|
|
switch (decl.data) {
|
|
.fn_decl => |fd| {
|
|
try buf.appendSlice(allocator, fd.name);
|
|
try buf.appendSlice(allocator, " :: (");
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (pi > 0) try buf.appendSlice(allocator, ", ");
|
|
try buf.appendSlice(allocator, param.name);
|
|
try buf.appendSlice(allocator, ": ");
|
|
if (param.type_expr.data == .type_expr) {
|
|
try buf.appendSlice(allocator, param.type_expr.data.type_expr.name);
|
|
} else {
|
|
try buf.appendSlice(allocator, "?");
|
|
}
|
|
}
|
|
try buf.append(allocator, ')');
|
|
if (fd.return_type) |rt| {
|
|
try buf.appendSlice(allocator, " -> ");
|
|
if (rt.data == .type_expr) {
|
|
try buf.appendSlice(allocator, rt.data.type_expr.name);
|
|
}
|
|
}
|
|
},
|
|
.enum_decl => |ed| {
|
|
try buf.appendSlice(allocator, ed.name);
|
|
if (ed.is_flags) {
|
|
try buf.appendSlice(allocator, " :: enum flags { ");
|
|
} else {
|
|
try buf.appendSlice(allocator, " :: enum { ");
|
|
}
|
|
for (ed.variant_names, 0..) |v, i| {
|
|
if (i > 0) try buf.appendSlice(allocator, ", ");
|
|
try buf.append(allocator, '.');
|
|
try buf.appendSlice(allocator, v);
|
|
}
|
|
try buf.appendSlice(allocator, " }");
|
|
},
|
|
.struct_decl => |sd| {
|
|
try buf.appendSlice(allocator, sd.name);
|
|
try buf.appendSlice(allocator, " :: struct { ");
|
|
for (sd.field_names, 0..) |fn_, fi| {
|
|
if (fi > 0) try buf.appendSlice(allocator, ", ");
|
|
try buf.appendSlice(allocator, fn_);
|
|
if (fi < sd.field_types.len) {
|
|
if (sd.field_types[fi].data == .type_expr) {
|
|
try buf.appendSlice(allocator, ": ");
|
|
try buf.appendSlice(allocator, sd.field_types[fi].data.type_expr.name);
|
|
}
|
|
}
|
|
}
|
|
try buf.appendSlice(allocator, " }");
|
|
},
|
|
.union_decl => |ud| {
|
|
try buf.appendSlice(allocator, ud.name);
|
|
try buf.appendSlice(allocator, " :: union { ");
|
|
for (ud.field_names, 0..) |fn_name, i| {
|
|
if (i > 0) try buf.appendSlice(allocator, ", ");
|
|
// Anonymous struct fields: show as "struct { ... }"
|
|
if (std.mem.startsWith(u8, fn_name, "__anon_")) {
|
|
if (i < ud.field_types.len and ud.field_types[i].data == .type_expr) {
|
|
try buf.appendSlice(allocator, ud.field_types[i].data.type_expr.name);
|
|
} else {
|
|
try buf.appendSlice(allocator, "struct { ... }");
|
|
}
|
|
} else {
|
|
try buf.appendSlice(allocator, fn_name);
|
|
if (i < ud.field_types.len) {
|
|
try buf.appendSlice(allocator, ": ");
|
|
if (ud.field_types[i].data == .type_expr) {
|
|
try buf.appendSlice(allocator, ud.field_types[i].data.type_expr.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try buf.appendSlice(allocator, " }");
|
|
},
|
|
.const_decl => |cd| {
|
|
try buf.appendSlice(allocator, cd.name);
|
|
try buf.appendSlice(allocator, " :: ");
|
|
if (cd.type_annotation) |ta| {
|
|
if (ta.data == .type_expr) {
|
|
try buf.appendSlice(allocator, ta.data.type_expr.name);
|
|
}
|
|
}
|
|
},
|
|
.var_decl => |vd| {
|
|
try buf.appendSlice(allocator, vd.name);
|
|
if (vd.type_annotation) |ta| {
|
|
if (ta.data == .type_expr) {
|
|
try buf.appendSlice(allocator, " : ");
|
|
try buf.appendSlice(allocator, ta.data.type_expr.name);
|
|
}
|
|
}
|
|
},
|
|
else => {
|
|
try buf.appendSlice(allocator, "(declaration)");
|
|
},
|
|
}
|
|
|
|
try buf.appendSlice(allocator, "\n```");
|
|
return buf.items;
|
|
}
|
|
|
|
fn formatSymbolHover(allocator: std.mem.Allocator, sym: sx.sema.Symbol, root: *sx.ast.Node, source: [:0]const u8) ![]const u8 {
|
|
// Try offset-based AST lookup
|
|
if (sym.def_span.start < source.len) {
|
|
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
|
|
if (node.data.declName()) |dn| {
|
|
if (std.mem.eql(u8, dn, sym.name)) {
|
|
return try formatDeclHover(allocator, node, source);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: name-based lookup
|
|
if (findDeclByName(root, sym.name)) |decl| {
|
|
return try formatDeclHover(allocator, decl, source);
|
|
}
|
|
|
|
// Last resort: simple format
|
|
var buf = std.ArrayList(u8).empty;
|
|
|
|
if (sym.def_span.start < source.len) {
|
|
if (extractDocComment(source, sym.def_span.start)) |comment| {
|
|
try buf.appendSlice(allocator, comment);
|
|
try buf.appendSlice(allocator, "\n\n");
|
|
}
|
|
}
|
|
|
|
try buf.appendSlice(allocator, "```sx\n");
|
|
|
|
switch (sym.kind) {
|
|
.function => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: (...)");
|
|
if (sym.ty) |ty| {
|
|
try buf.appendSlice(allocator, " -> ");
|
|
const type_name = try ty.displayName(allocator);
|
|
try buf.appendSlice(allocator, type_name);
|
|
}
|
|
},
|
|
.variable => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
if (sym.ty) |ty| {
|
|
try buf.appendSlice(allocator, " : ");
|
|
const type_name = try ty.displayName(allocator);
|
|
try buf.appendSlice(allocator, type_name);
|
|
}
|
|
},
|
|
.constant => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: ");
|
|
if (sym.ty) |ty| {
|
|
const type_name = try ty.displayName(allocator);
|
|
try buf.appendSlice(allocator, type_name);
|
|
} else {
|
|
try buf.appendSlice(allocator, "(constant)");
|
|
}
|
|
},
|
|
.param => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
if (sym.ty) |ty| {
|
|
try buf.appendSlice(allocator, " : ");
|
|
const type_name = try ty.displayName(allocator);
|
|
try buf.appendSlice(allocator, type_name);
|
|
}
|
|
},
|
|
.enum_type => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: enum { ... }");
|
|
},
|
|
.struct_type => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: struct { ... }");
|
|
},
|
|
.type_alias => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: (type)");
|
|
},
|
|
.namespace => {
|
|
try buf.appendSlice(allocator, sym.name);
|
|
try buf.appendSlice(allocator, " :: (namespace)");
|
|
},
|
|
}
|
|
|
|
try buf.appendSlice(allocator, "\n```");
|
|
return buf.items;
|
|
}
|
|
};
|