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; } };