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"); pub const c_import = @import("../c_import.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, root_path: []const u8 = "", stdlib_paths: []const []const u8 = &.{}, pub fn init(allocator: std.mem.Allocator, transport: *Transport, io: std.Io, stdlib_paths: []const []const u8) Server { return .{ .allocator = allocator, .documents = DocumentStore.init(allocator, io, stdlib_paths), .transport = transport, .io = io, .stdlib_paths = stdlib_paths, }; } 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, params) 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); } else if (std.mem.eql(u8, method, "textDocument/inlayHint")) { if (params) |p| self.handleInlayHint(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, params: ?std.json.Value) !void { // chdir to workspace root so relative paths in #import c work chdir: { const p = params orelse break :chdir; const root_uri_val = jsonGet(p, "rootUri") orelse break :chdir; const root_uri = jsonStr(root_uri_val) orelse break :chdir; const prefix = "file://"; if (!std.mem.startsWith(u8, root_uri, prefix)) break :chdir; const root_path = root_uri[prefix.len..]; self.root_path = self.allocator.dupe(u8, root_path) catch break :chdir; self.documents.root_path = self.root_path; const path_z = self.allocator.dupeZ(u8, root_path) catch break :chdir; _ = std.c.chdir(path_z.ptr); } 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 doc.last_good_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| { const qn_origin = sx.ast.Span{ .start = qn.full_start, .end = qn.full_end }; // Namespace import member if (self.findImportByNs(doc, qn.ns)) |imp| { if (self.documents.get(imp.path)) |imp_doc| { // C import source location: jump to the C header if (imp_doc.c_source_locations.get(qn.member)) |cloc| { if (try self.sendCSourceLocation(id_json, cloc, qn_origin, doc.source)) return; } // Single-file import if (imp_doc.sema) |imp_sema| { if (findSymbolByName(imp_sema.symbols, qn.member)) |si| { const sym = imp_sema.symbols[si]; if (try self.sendSymbolLocationWithOrigin(id_json, imp_doc, sym, qn_origin)) return; } } } else { // Directory import: search all documents whose path starts with the import directory const dir_prefix = try std.fmt.allocPrint(self.allocator, "{s}/", .{imp.path}); var it = self.documents.by_path.iterator(); while (it.next()) |entry| { if (!std.mem.startsWith(u8, entry.key_ptr.*, dir_prefix)) continue; const dir_doc = entry.value_ptr.*; if (dir_doc.sema) |dir_sema| { if (findSymbolByName(dir_sema.symbols, qn.member)) |si| { const sym = dir_sema.symbols[si]; if (try self.sendSymbolLocationWithOrigin(id_json, dir_doc, sym, qn_origin)) 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.sendSymbolLocationWithOrigin(id_json, doc, sym, qn_origin)) return; } } // Struct method/field: obj.method or Type.method or obj.field if (try self.resolveStructMemberDef(id_json, sema, doc, qn.ns, qn.member, qn_origin)) return; } // 1b. Dot-shorthand: .method(args) — identifier preceded by dot with no qualifier if (extractIdentAtOffset(doc.source, offset)) |name| { const name_start = @as(u32, @intCast(@intFromPtr(name.ptr) - @intFromPtr(doc.source.ptr))); if (name_start > 0 and doc.source[name_start - 1] == '.' and (name_start < 2 or !isIdentChar(doc.source[name_start - 2]))) { const name_end = name_start + @as(u32, @intCast(name.len)); const origin = sx.ast.Span{ .start = name_start, .end = name_end }; // Search all type declarations for a matching method/variant/field for (sema.symbols) |sym| { if (sym.kind != .struct_type and sym.kind != .enum_type) continue; const lookup = self.findTypeDeclNode(sema, doc, sym.name) orelse continue; if (try self.sendMemberLocation(id_json, lookup, name, doc, origin)) 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.sendSymbolLocationWithOrigin(id_json, doc, sym, ref.span)) 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.sendSymbolLocationWithOrigin(id_json, doc, sym, sym.def_span)) return; } // 4. #import "path" string → open the file (or directory) if (findImportPathAtOffset(doc.source, offset)) |import_path| { const base_dir = sx.imports.dirName(file_path); const rp: ?[]const u8 = if (self.root_path.len > 0) self.root_path else null; const resolved = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, import_path, rp, self.stdlib_paths); // For directory imports, try to read as file first if (std.Io.Dir.readFileAlloc(.cwd(), self.io, resolved, self.allocator, .limited(10 * 1024 * 1024))) |_| { // It's a file — navigate to it 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; } else |_| { // Might be a directory — no single file to navigate to } } // 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]; const name_end = name_start + @as(u32, @intCast(name.len)); const origin = sx.ast.Span{ .start = name_start, .end = name_end }; if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, origin)) 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), .protocol_type => @intFromEnum(lsp.SymbolKindLsp.Interface), .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 // Fall back to last successful analysis when current parse/analysis fails // (common while user is mid-typing) const sema = doc.sema orelse doc.last_good_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), .protocol_type => @intFromEnum(lsp.CompletionItemKind.Interface), .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) -> s64" }, .{ .label = "align_of", .detail = "($T: Type) -> s64" }, .{ .label = "cast", .detail = "(Type) expr — prefix type cast" }, .{ .label = "malloc", .detail = "(size: s64) -> *void" }, .{ .label = "free", .detail = "(ptr: *void) -> void" }, .{ .label = "memcpy", .detail = "(dst: *void, src: *void, size: s64) -> *void" }, .{ .label = "memset", .detail = "(dst: *void, val: s64, size: s64) -> void" }, .{ .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| { if (doc.sema orelse doc.last_good_sema) |sema| { // 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 if (doc.root) |root| { // Try as type name directly (e.g. Vec2., Color.) try self.collectMemberCompletions(&items, sema, doc, 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, doc, type_name); try self.collectUfcsCompletions(&items, root, type_name); } } } } // Fallback: try resolving as a match arm capture variable (works without current parse) if (items.items.len == 0) { try self.collectCaptureCompletions(&items, doc, cursor_offset, prefix); } } else { // Bare dot (no prefix) — check if we're inside a match expression // and offer the subject's enum variants (e.g. case .quit) try self.collectMatchEnumCompletions(&items, doc, cursor_offset); } const items_json = try lsp.completionListJson(self.allocator, items.items); try self.sendResponse(id_json, items_json); } fn collectMatchEnumCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32) !void { // Text-based approach: scan backward from cursor to find enclosing "SUBJECT == {" // This works even when the file has parse errors (user is mid-typing) const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return; // Use sema (current or last successful) to resolve the subject's type const sema = doc.sema orelse doc.last_good_sema orelse return; // Resolve subject name to an enum type const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return; // Use enum variant names directly from sema symbols (works even without doc.root) for (sema.symbols) |sym| { if (!std.mem.eql(u8, sym.name, enum_name)) continue; if (sym.kind != .enum_type) continue; // Find the enum_decl node in the origin doc (or main doc) const lookup_doc = if (sym.origin) |origin_path| self.documents.get(origin_path) else null; const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root; const root = lookup_root orelse continue; if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| { if (node.data == .enum_decl) { for (node.data.enum_decl.variant_names) |variant| { try items.append(self.allocator, .{ .label = variant, .kind = @intFromEnum(lsp.CompletionItemKind.EnumMember), }); } } } break; } } /// Scan backward through source text to find the subject of an enclosing match expression. /// Looks for the pattern: SUBJECT == { ... case . /// Returns the subject text (e.g. "event", "e.key") or null. fn findMatchSubjectText(source: []const u8, cursor_offset: u32) ?[]const u8 { var pos: u32 = cursor_offset; var brace_depth: u32 = 0; // Walk backward, tracking brace depth, looking for "== {" while (pos > 2) { pos -= 1; const ch = source[pos]; if (ch == '}') { brace_depth += 1; } else if (ch == '{') { if (brace_depth > 0) { brace_depth -= 1; } else { // Found an unmatched '{' — check if preceded by "==" var scan = pos; while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; if (scan >= 2 and source[scan - 1] == '=' and source[scan - 2] == '=') { // Found "== {" — extract the subject before "==" var end = scan - 2; while (end > 0 and std.ascii.isWhitespace(source[end - 1])) end -= 1; if (end == 0) return null; var start = end; while (start > 0 and (isIdentChar(source[start - 1]) or source[start - 1] == '.')) { start -= 1; } if (start < end) return source[start..end]; } // Not a match expr — but keep scanning (might be a nested block) } } } return null; } /// Resolve a capture variable's struct type by scanning backward for `case .VARIANT: (name)` /// and looking up the variant's payload type in the enum declaration. fn collectCaptureCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32, var_name: []const u8) !void { const sema = doc.sema orelse doc.last_good_sema orelse return; // Scan backward from cursor to find: case .VARIANT: (var_name) const variant_name = findCaptureVariant(doc.source, cursor_offset, var_name) orelse return; // Find the enclosing match subject const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return; // Resolve subject to an enum type const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return; // Find the enum_decl and look up the variant's payload type for (sema.symbols) |sym| { if (!std.mem.eql(u8, sym.name, enum_name)) continue; if (sym.kind != .enum_type) continue; const lookup_doc = if (sym.origin) |origin_path| self.documents.get(origin_path) else null; const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root; const root = lookup_root orelse continue; if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| { if (node.data == .enum_decl) { const ed = node.data.enum_decl; // Find the matching variant and its payload type for (ed.variant_names, 0..) |vn, vi| { if (!std.mem.eql(u8, vn, variant_name)) continue; if (vi >= ed.variant_types.len) break; const vt = ed.variant_types[vi] orelse break; // The payload type should be a struct — get its name const payload_type_name = if (vt.data == .type_expr) vt.data.type_expr.name else break; // Now offer that struct's fields const payload_sema = if (lookup_doc) |ld| ld.sema orelse sema else sema; const payload_doc = if (lookup_doc) |ld| ld else doc; try self.collectMemberCompletions(items, payload_sema, payload_doc, payload_type_name); return; } } } break; } } /// Scan backward from cursor to find `case .VARIANT: (var_name)` and return VARIANT. fn findCaptureVariant(source: []const u8, cursor_offset: u32, var_name: []const u8) ?[]const u8 { // Look backward for pattern: case .VARIANT: (var_name) // We search for "(var_name)" first, then look for "case .VARIANT:" before it var pos: u32 = cursor_offset; while (pos > var_name.len + 10) { // need room for "case .X: (name)" pos -= 1; // Look for the closing ) of a capture if (source[pos] != ')') continue; // Check if the capture name matches if (pos < var_name.len + 1) continue; const name_end = pos; const name_start = pos - @as(u32, @intCast(var_name.len)); if (!std.mem.eql(u8, source[name_start..name_end], var_name)) continue; // Check for ( before the name if (name_start < 1 or source[name_start - 1] != '(') continue; // Now scan backward from '(' to find ": " then ".VARIANT" then "case " var scan = name_start - 1; // Skip whitespace while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; // Expect ':' if (scan < 1 or source[scan - 1] != ':') continue; scan -= 1; // Now extract the variant name: scan backward for ".VARIANT" // Skip whitespace before ':' // The variant is an enum literal like .key_down // Actually the ':' comes right after the variant value (explicit) or variant name // Pattern: case .VARIANT: (name) OR case .VARIANT :: 0x300: PayloadType;... nah // In the match arm: case .key_down: (e) { // So before ':' we have the enum literal .key_down while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; // Extract identifier (variant name) const vend = scan; while (scan > 0 and (isIdentChar(source[scan - 1]))) scan -= 1; if (scan >= vend) continue; const variant = source[scan..vend]; // Check for '.' before variant name if (scan < 1 or source[scan - 1] != '.') continue; return variant; } return null; } /// Resolve a text expression like "event" or "e.key" to an enum/union type name. fn resolveExprEnumType(self: *Server, sema: SemaResult, expr: []const u8) ?[]const u8 { // Simple identifier: look up variable type if (std.mem.indexOfScalar(u8, expr, '.') == null) { for (sema.symbols) |sym| { if (!std.mem.eql(u8, sym.name, expr)) continue; if (sym.kind != .variable and sym.kind != .param) continue; const ty = sym.ty orelse return null; return switch (ty) { .enum_type => |n| n, .union_type => |n| n, else => null, }; } return null; } // Field access: "var.field" — resolve var's struct type, then look up field if (std.mem.indexOfScalar(u8, expr, '.')) |dot| { const var_name = expr[0..dot]; const field_name = expr[dot + 1 ..]; for (sema.symbols) |sym| { if (!std.mem.eql(u8, sym.name, var_name)) continue; const ty = sym.ty orelse return null; const struct_name = switch (ty) { .struct_type => |n| n, else => return null, }; if (sema.struct_types.get(struct_name)) |info| { for (info.field_names, 0..) |fname, fi| { if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) { return switch (info.field_types[fi]) { .enum_type => |n| n, .union_type => |n| n, else => null, }; } } } // Also check imported docs for struct info if (sym.origin) |origin_path| { if (self.documents.get(origin_path)) |od| { if (od.sema) |imp_sema| { if (imp_sema.struct_types.get(struct_name)) |info| { for (info.field_names, 0..) |fname, fi| { if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) { return switch (info.field_types[fi]) { .enum_type => |n| n, .union_type => |n| n, else => null, }; } } } } } } return null; } } return null; } 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); if (param.type_expr.data != .inferred_type) { 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), }); }, .protocol_decl => |pd| { try items.append(allocator, .{ .label = pd.name, .kind = @intFromEnum(lsp.CompletionItemKind.Interface), }); }, .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, doc: *const Document, name: []const u8) !void { const lookup = self.findTypeDeclNode(sema, doc, name) orelse return; const node = lookup.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, }); } for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { try items.append(self.allocator, .{ .label = method_node.data.fn_decl.name, .kind = @intFromEnum(lsp.CompletionItemKind.Method), }); } } } else 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), }); } } else if (node.data == .protocol_decl) { const pd = node.data.protocol_decl; for (pd.methods) |method| { // Build detail string: (params) -> ret var detail_buf = std.ArrayList(u8).empty; try detail_buf.append(self.allocator, '('); for (method.param_names, 0..) |pname, pi| { if (pi > 0) try detail_buf.appendSlice(self.allocator, ", "); try detail_buf.appendSlice(self.allocator, pname); if (pi < method.params.len) { try detail_buf.appendSlice(self.allocator, ": "); if (method.params[pi].data == .type_expr) { try detail_buf.appendSlice(self.allocator, method.params[pi].data.type_expr.name); } } } try detail_buf.append(self.allocator, ')'); if (method.return_type) |rt| { try detail_buf.appendSlice(self.allocator, " -> "); if (rt.data == .type_expr) { try detail_buf.appendSlice(self.allocator, rt.data.type_expr.name); } } try items.append(self.allocator, .{ .label = method.name, .kind = @intFromEnum(lsp.CompletionItemKind.Method), .detail = detail_buf.items, }); } } } // ---- 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) -> s64", .params = &.{"$T: Type"} }, .{ .name = "align_of", .label = "align_of($T: Type) -> s64", .params = &.{"$T: Type"} }, .{ .name = "cast", .label = "cast(Type) expr", .params = &.{"Type"} }, .{ .name = "malloc", .label = "malloc(size: s64) -> *void", .params = &.{"size: s64"} }, .{ .name = "free", .label = "free(ptr: *void) -> void", .params = &.{"ptr: *void"} }, .{ .name = "memcpy", .label = "memcpy(dst: *void, src: *void, size: s64) -> *void", .params = &.{ "dst: *void", "src: *void", "size: s64" } }, .{ .name = "memset", .label = "memset(dst: *void, val: s64, size: s64) -> void", .params = &.{ "dst: *void", "val: s64", "size: s64" } }, .{ .name = "sqrt", .label = "sqrt(x: $T) -> T", .params = &.{"x: $T"} }, .{ .name = "print", .label = "print(fmt: string, args: ..Any)", .params = &.{ "fmt: string", "args: ..Any" } }, .{ .name = "out", .label = "out(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); if (param.type_expr.data != .inferred_type) { 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); } // ---- Inlay hints ---- fn handleInlayHint(self: *Server, id: ?std.json.Value, params: std.json.Value) !void { const ctx = try self.extractRequest(id, params) orelse return; const id_json = ctx.id_json; const file_path = uriToFilePath(ctx.uri) orelse ""; const doc = self.documents.get(file_path) orelse { return try self.sendResponse(id_json, "[]"); }; const sema = doc.sema orelse doc.last_good_sema orelse { return try self.sendResponse(id_json, "[]"); }; const root = doc.root orelse { return try self.sendResponse(id_json, "[]"); }; var hints = std.ArrayList(lsp.InlayHint).empty; collectInlayHints(self.allocator, root, sema.symbols, sema.fn_signatures, doc.source, &hints); self.collectCallHints(doc, root, &hints); const result_json = try lsp.inlayHintsJson(self.allocator, hints.items); try self.sendResponse(id_json, result_json); } fn collectInlayHints( allocator: std.mem.Allocator, node: *const sx.ast.Node, symbols: []const sx.sema.Symbol, fn_signatures: std.StringHashMap(sx.sema.FnSignature), source: [:0]const u8, hints: *std.ArrayList(lsp.InlayHint), ) void { switch (node.data) { .root => |r| { for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, fn_signatures, source, hints); }, .block => |b| { for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, fn_signatures, source, hints); }, .fn_decl => |fd| { collectInlayHints(allocator, fd.body, symbols, fn_signatures, source, hints); // Return type hint for arrow functions without explicit return type if (fd.return_type == null and fd.is_arrow) { if (fn_signatures.get(fd.name)) |sig| { if (sig.return_type != .void_type) { addReturnTypeHint(allocator, node.span, source, sig.return_type, hints); } } } }, .lambda => |lm| { collectInlayHints(allocator, lm.body, symbols, fn_signatures, source, hints); }, .if_expr => |ie| { if (ie.binding_name) |bname| { addBindingHint(allocator, bname, node.span, symbols, source, hints); } collectInlayHints(allocator, ie.then_branch, symbols, fn_signatures, source, hints); if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, fn_signatures, source, hints); }, .while_expr => |we| { if (we.binding_name) |bname| { addBindingHint(allocator, bname, node.span, symbols, source, hints); } collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints); }, .for_expr => |fe| { collectInlayHints(allocator, fe.body, symbols, fn_signatures, source, hints); }, .var_decl => |vd| { // Only show hint when type is inferred (:= syntax) if (vd.type_annotation != null) return; if (vd.value == null) return; addHintForDecl(allocator, vd.name, node.span, symbols, source, hints, true); }, .const_decl => |cd| { // Skip if explicit type annotation if (cd.type_annotation != null) return; // Handle lambda with return type hint if (cd.value.data == .lambda) { const lam = cd.value.data.lambda; collectInlayHints(allocator, lam.body, symbols, fn_signatures, source, hints); if (lam.return_type == null) { if (fn_signatures.get(cd.name)) |sig| { if (sig.return_type != .void_type) { addReturnTypeHint(allocator, cd.value.span, source, sig.return_type, hints); } } } return; } // Skip functions, types, structs, enums, unions, comptime, foreign, library switch (cd.value.data) { .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl, .comptime_expr, .foreign_expr, .library_decl, => return, else => {}, } addHintForDecl(allocator, cd.name, node.span, symbols, source, hints, false); }, else => {}, } } fn addHintForDecl( allocator: std.mem.Allocator, name: []const u8, span: sx.ast.Span, symbols: []const sx.sema.Symbol, source: [:0]const u8, hints: *std.ArrayList(lsp.InlayHint), is_colon_equal: bool, ) void { // Find symbol by matching span start const sym = findSymbolAtSpan(symbols, span.start, name) orelse return; const ty = sym.ty orelse return; // Skip void types — not useful to display if (ty == .void_type) return; const type_name = ty.displayName(allocator) catch return; if (is_colon_equal) { // For `:=` declarations: place hint between `:` and `=` // Scan from after the name to find `:=` var pos = span.start + @as(u32, @intCast(name.len)); while (pos + 1 < source.len) : (pos += 1) { if (source[pos] == ':' and source[pos + 1] == '=') { // Place hint at the `=` position (between `:` and `=`) const eq_offset = pos + 1; const loc = sx.errors.SourceLoc.compute(source, eq_offset); if (loc.line == 0 or loc.col == 0) return; hints.append(allocator, .{ .line = loc.line - 1, .character = loc.col - 1, .label = type_name, .padding_left = true, .padding_right = true, }) catch {}; return; } } } else { // For `::` declarations: place hint between first `:` and second `:` var pos = span.start + @as(u32, @intCast(name.len)); while (pos + 1 < source.len) : (pos += 1) { if (source[pos] == ':' and source[pos + 1] == ':') { const second_colon = pos + 1; const loc = sx.errors.SourceLoc.compute(source, second_colon); if (loc.line == 0 or loc.col == 0) return; hints.append(allocator, .{ .line = loc.line - 1, .character = loc.col - 1, .label = type_name, .padding_left = true, .padding_right = true, }) catch {}; return; } } } } fn addBindingHint( allocator: std.mem.Allocator, name: []const u8, span: sx.ast.Span, symbols: []const sx.sema.Symbol, source: [:0]const u8, hints: *std.ArrayList(lsp.InlayHint), ) void { // Look up symbol by name + span (sema stores binding with if/while node span) const sym = findSymbolAtSpan(symbols, span.start, name) orelse return; const ty = sym.ty orelse return; if (ty == .void_type) return; const type_name = ty.displayName(allocator) catch return; // Scan from span start to find the `:=` used in the binding var pos = span.start; while (pos + 1 < source.len) : (pos += 1) { if (source[pos] == ':' and source[pos + 1] == '=') { const eq_offset = pos + 1; const loc = sx.errors.SourceLoc.compute(source, eq_offset); if (loc.line == 0 or loc.col == 0) return; hints.append(allocator, .{ .line = loc.line - 1, .character = loc.col - 1, .label = type_name, .padding_left = true, .padding_right = true, }) catch {}; return; } } } fn addReturnTypeHint( allocator: std.mem.Allocator, span: sx.ast.Span, source: [:0]const u8, return_type: sx.types.Type, hints: *std.ArrayList(lsp.InlayHint), ) void { // Find '(' from span start var pos: u32 = span.start; while (pos < source.len and source[pos] != '(') : (pos += 1) {} if (pos >= source.len) return; // Match nested parens to find closing ')' var depth: u32 = 0; while (pos < source.len) : (pos += 1) { if (source[pos] == '(') { depth += 1; } else if (source[pos] == ')') { depth -= 1; if (depth == 0) break; } } if (pos >= source.len or depth != 0) return; // Place hint right after ')' const loc = sx.errors.SourceLoc.compute(source, pos + 1); if (loc.line == 0 or loc.col == 0) return; const type_name = return_type.displayName(allocator) catch return; const label = std.fmt.allocPrint(allocator, "-> {s}", .{type_name}) catch return; hints.append(allocator, .{ .line = loc.line - 1, .character = loc.col - 1, .label = label, .kind = 1, .padding_left = true, .padding_right = true, }) catch {}; } fn findSymbolAtSpan(symbols: []const sx.sema.Symbol, span_start: u32, name: []const u8) ?sx.sema.Symbol { for (symbols) |sym| { if (sym.def_span.start == span_start and std.mem.eql(u8, sym.name, name)) { return sym; } } return null; } // ---- Parameter name hints at call sites ---- fn collectCallHints(self: *Server, doc: *const Document, node: *const sx.ast.Node, hints: *std.ArrayList(lsp.InlayHint)) void { switch (node.data) { .root => |r| { for (r.decls) |decl| self.collectCallHints(doc, decl, hints); }, .block => |b| { for (b.stmts) |stmt| self.collectCallHints(doc, stmt, hints); }, .fn_decl => |fd| { self.collectCallHints(doc, fd.body, hints); }, .lambda => |lm| { self.collectCallHints(doc, lm.body, hints); }, .if_expr => |ie| { self.collectCallHints(doc, ie.condition, hints); self.collectCallHints(doc, ie.then_branch, hints); if (ie.else_branch) |eb| self.collectCallHints(doc, eb, hints); }, .while_expr => |we| { self.collectCallHints(doc, we.condition, hints); self.collectCallHints(doc, we.body, hints); }, .for_expr => |fe| { self.collectCallHints(doc, fe.iterable, hints); self.collectCallHints(doc, fe.body, hints); }, .var_decl => |vd| { if (vd.value) |val| self.collectCallHints(doc, val, hints); }, .const_decl => |cd| { self.collectCallHints(doc, cd.value, hints); }, .return_stmt => |rs| { if (rs.value) |val| self.collectCallHints(doc, val, hints); }, .assignment => |a| { self.collectCallHints(doc, a.value, hints); }, .binary_op => |bop| { self.collectCallHints(doc, bop.lhs, hints); self.collectCallHints(doc, bop.rhs, hints); }, .unary_op => |uop| { self.collectCallHints(doc, uop.operand, hints); }, .call => |c| { // Recurse into arguments (they may contain nested calls) for (c.args) |arg| self.collectCallHints(doc, arg, hints); // Emit parameter name hints for this call self.emitCallParamHints(doc, c, hints); }, .push_stmt => |ps| { self.collectCallHints(doc, ps.context_expr, hints); self.collectCallHints(doc, ps.body, hints); }, .defer_stmt => |ds| { self.collectCallHints(doc, ds.expr, hints); }, else => {}, } } fn emitCallParamHints(self: *Server, doc: *const Document, call: sx.ast.Call, hints: *std.ArrayList(lsp.InlayHint)) void { if (call.args.len == 0) return; // Resolve callee name and find function declaration var param_offset: usize = 0; const fd = self.resolveCallTarget(doc, call, ¶m_offset) orelse return; // Emit hints for each argument for (call.args, 0..) |arg, i| { const param_idx = i + param_offset; if (param_idx >= fd.params.len) break; const param = fd.params[param_idx]; // Skip variadic params if (param.is_variadic) break; // Skip if arg is an identifier matching the param name if (arg.data == .identifier) { if (std.mem.eql(u8, arg.data.identifier.name, param.name)) continue; } // Skip _ params if (std.mem.eql(u8, param.name, "_")) continue; const loc = sx.errors.SourceLoc.compute(doc.source, arg.span.start); if (loc.line == 0 or loc.col == 0) continue; const label = std.fmt.allocPrint(self.allocator, "{s}:", .{param.name}) catch continue; hints.append(self.allocator, .{ .line = loc.line - 1, .character = loc.col - 1, .label = label, .padding_left = false, }) catch {}; } } fn resolveCallTarget(self: *Server, doc: *const Document, call: sx.ast.Call, param_offset: *usize) ?sx.ast.FnDecl { param_offset.* = 0; if (call.callee.data == .identifier) { const name = call.callee.data.identifier.name; return self.findFnDeclByName(doc, name); } if (call.callee.data == .field_access) { const fa = call.callee.data.field_access; // Try namespaced: "ns.func" if (fa.object.data == .identifier) { const ns_name = fa.object.data.identifier.name; const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }) catch return null; if (self.findFnDeclByName(doc, qualified)) |fd| { return fd; } } // Try UFCS: bare function name, skip first param (receiver) if (self.findFnDeclByName(doc, fa.field)) |fd| { if (fd.params.len == call.args.len + 1) { param_offset.* = 1; } return fd; } } return null; } fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 { const ST = lsp.SemanticTokenType; return switch (tok.tag) { .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, .kw_push, .kw_ufcs, .kw_in, .kw_closure, .kw_protocol, .kw_impl, .kw_inline, .kw_callconv, .hash_run, .hash_import, .hash_insert, .hash_builtin, .hash_compiler, .hash_foreign, .hash_library, .hash_framework, .hash_using, .hash_include, .hash_source, .hash_define, .hash_flags, .hash_inline, .hash_objc_call, .hash_jni_call, .hash_jni_static_call, .hash_jni_class, .hash_jni_interface, .hash_objc_class, .hash_objc_protocol, .hash_swift_class, .hash_swift_struct, .hash_swift_protocol, .hash_extends, .hash_implements, .hash_jni_method_descriptor, .hash_jni_env, .hash_jni_main, .hash_selector, .hash_property, => ST.keyword, .kw_f32, .kw_f64, .kw_Type, .kw_Self => 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, .ampersand_equal, .at, .pipe, .pipe_equal, .pipe_arrow, .caret, .caret_equal, .question, .question_question, .question_dot, .tilde, .less_less, .less_less_equal, .greater_greater, .greater_greater_equal, .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_; } // Uppercase identifiers are conventionally types if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') { 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_, .protocol_type => ST.interface, .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, .help => 4, }; 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 { return self.sendSymbolLocationWithOrigin(id_json, doc, sym, null); } fn sendSymbolLocationWithOrigin(self: *Server, id_json: []const u8, doc: *const Document, sym: sx.sema.Symbol, origin_span: ?sx.ast.Span) !bool { if (sym.origin) |origin_path| { const origin_doc = self.documents.get(origin_path) orelse return false; const target_range = spanToRange(origin_doc.source, sym.def_span); const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{origin_path}); if (origin_span) |os| { const src_range = spanToRange(doc.source, os); const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); try self.sendResponse(id_json, loc_json); } else { const loc_json = try lsp.locationJson(self.allocator, target_uri, target_range); try self.sendResponse(id_json, loc_json); } return true; } else { const target_range = spanToRange(doc.source, sym.def_span); const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{doc.path}); if (origin_span) |os| { const src_range = spanToRange(doc.source, os); const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); try self.sendResponse(id_json, loc_json); } else { const loc_json = try lsp.locationJson(self.allocator, target_uri, target_range); try self.sendResponse(id_json, loc_json); } return true; } } /// Send a go-to-definition response pointing to a C header source location. fn sendCSourceLocation(self: *Server, id_json: []const u8, cloc: sx.c_import.CSourceLocation, origin_span: ?sx.ast.Span, origin_source: [:0]const u8) !bool { // Resolve to absolute path if relative const abs_path = if (cloc.file.len > 0 and cloc.file[0] != '/') try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ self.root_path, cloc.file }) else cloc.file; const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{abs_path}); const line: u32 = if (cloc.line > 0) cloc.line - 1 else 0; // LSP lines are 0-based const target_range = lsp.Range{ .start = .{ .line = line, .character = 0 }, .end = .{ .line = line, .character = 0 }, }; if (origin_span) |os| { const src_range = spanToRange(origin_source, os); const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); try self.sendResponse(id_json, loc_json); } else { const loc_json = try lsp.locationJson(self.allocator, target_uri, target_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; } const TypeDeclLookup = struct { node: *sx.ast.Node, doc: *const Document }; /// Given a type name, find its AST declaration node and the document it lives in. /// Works for struct_decl, enum_decl, union_decl, and protocol_decl across files. fn findTypeDeclNode(self: *Server, sema: SemaResult, doc: *const Document, type_name: []const u8) ?TypeDeclLookup { for (sema.symbols) |sym| { if (!std.mem.eql(u8, sym.name, type_name)) continue; if (sym.kind != .struct_type and sym.kind != .enum_type and sym.kind != .protocol_type) 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| { return .{ .node = node, .doc = lookup_doc }; } return null; } return null; } /// Send a go-to-definition response for a struct method, field, or enum variant. fn sendMemberLocation(self: *Server, id_json: []const u8, lookup: TypeDeclLookup, member_name: []const u8, origin_doc: *const Document, origin: sx.ast.Span) !bool { if (lookup.node.data == .struct_decl) { const sd = lookup.node.data.struct_decl; // Check methods for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { if (std.mem.eql(u8, method_node.data.fn_decl.name, member_name)) { return self.sendSpanLocation(id_json, lookup.doc, method_node.span, origin_doc, origin); } } } // Check fields — use field type span as approximate location for (sd.field_names, 0..) |fname, fi| { if (std.mem.eql(u8, fname, member_name)) { if (fi < sd.field_types.len) { return self.sendSpanLocation(id_json, lookup.doc, sd.field_types[fi].span, origin_doc, origin); } } } } else if (lookup.node.data == .enum_decl) { const ed = lookup.node.data.enum_decl; for (ed.variant_names) |v| { if (std.mem.eql(u8, v, member_name)) { return self.sendSpanLocation(id_json, lookup.doc, lookup.node.span, origin_doc, origin); } } } else if (lookup.node.data == .union_decl) { const ud = lookup.node.data.union_decl; for (ud.field_names) |fname| { if (std.mem.eql(u8, fname, member_name)) { return self.sendSpanLocation(id_json, lookup.doc, lookup.node.span, origin_doc, origin); } } } return false; } /// Resolve qualified name as struct/union method or field, send definition location. fn resolveStructMemberDef(self: *Server, id_json: []const u8, sema: SemaResult, doc: *const Document, ns: []const u8, member: []const u8, origin: sx.ast.Span) !bool { // Try ns as a type name directly if (self.findTypeDeclNode(sema, doc, ns)) |lookup| { if (try self.sendMemberLocation(id_json, lookup, member, doc, origin)) return true; } // Try ns as a variable name → resolve to struct type if (resolveStructTypeName(sema, doc, ns)) |type_name| { if (self.findTypeDeclNode(sema, doc, type_name)) |lookup| { if (try self.sendMemberLocation(id_json, lookup, member, doc, origin)) return true; } } return false; } /// Send a go-to-definition response pointing to a span in a document. fn sendSpanLocation(self: *Server, id_json: []const u8, target_doc: *const Document, target_span: sx.ast.Span, origin_doc: *const Document, origin_span: sx.ast.Span) !bool { const target_range = spanToRange(target_doc.source, target_span); const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{target_doc.path}); const src_range = spanToRange(origin_doc.source, origin_span); const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); try self.sendResponse(id_json, loc_json); return true; } /// Find an import by namespace name (falls back to last good imports). fn findImportByNs(_: *Server, doc: *const Document, ns_name: []const u8) ?doc_mod.Import { const imports_lists = [_][]const doc_mod.Import{ doc.imports, doc.last_good_imports }; for (&imports_lists) |imports| { for (imports) |imp| { if (imp.ns) |ns| { if (std.mem.eql(u8, ns, ns_name)) return imp; } } } return null; } pub fn searchFnDeclInDoc(target_doc: *const Document, fn_name: []const u8) ?sx.ast.FnDecl { const root = target_doc.root orelse return null; if (root.data != .root) return null; for (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; } } 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 (searchFnDeclInDoc(imp_doc, fn_name)) |fd| return fd; } else { // Directory import: search all documents in the directory const dir_prefix = std.fmt.allocPrint(self.allocator, "{s}/", .{imp.path}) catch null; if (dir_prefix) |prefix| { var it = self.documents.by_path.iterator(); while (it.next()) |entry| { if (!std.mem.startsWith(u8, entry.key_ptr.*, prefix)) continue; if (searchFnDeclInDoc(entry.value_ptr.*, fn_name)) |fd| return fd; } } } } } // 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); if (param.type_expr.data == .inferred_type) { // Inferred type — show name only } else if (param.type_expr.data == .type_expr) { try buf.appendSlice(self.allocator, ": "); 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; const lookup = self.findTypeDeclNode(sema, doc, struct_name) orelse return null; if (lookup.node.data != .struct_decl) return null; const sd = lookup.node.data.struct_decl; const lookup_doc = lookup.doc; 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; } return null; } fn formatEnumVariantHover(self: *Server, doc: *const Document, variant_name: []const u8) !?[]const u8 { const sema = doc.sema orelse return null; // Search all enum types for the variant for (sema.symbols) |sym| { if (sym.kind != .enum_type) continue; const lookup = self.findTypeDeclNode(sema, doc, sym.name) orelse continue; if (lookup.node.data != .enum_decl) continue; const ed = lookup.node.data.enum_decl; const lookup_doc = lookup.doc; 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; } pub 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; } pub 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 == '_'; } pub 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]; } pub fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8, full_start: u32, full_end: u32 } { 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], .full_start = ns_start, .full_end = 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], .full_start = start, .full_end = member_end, }; } } return null; } pub 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); if (param.type_expr.data != .inferred_type) { 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 "); } if (ed.backing_type) |bt| { if (bt.data == .type_expr) { try buf.appendSlice(allocator, bt.data.type_expr.name); try buf.appendSlice(allocator, " "); } else if (bt.data == .struct_decl) { const sd = bt.data.struct_decl; try buf.appendSlice(allocator, "struct { "); for (sd.field_names, 0..) |fn_, fi| { if (fi > 0) try buf.appendSlice(allocator, "; "); try buf.appendSlice(allocator, fn_); try buf.appendSlice(allocator, ": "); if (fi < sd.field_types.len) { if (sd.field_types[fi].data == .type_expr) { try buf.appendSlice(allocator, sd.field_types[fi].data.type_expr.name); } else if (sd.field_types[fi].data == .array_type_expr) { const ate = sd.field_types[fi].data.array_type_expr; try buf.append(allocator, '['); if (ate.length.data == .int_literal) { const val = ate.length.data.int_literal.value; var num_buf: [20]u8 = undefined; const num_str = std.fmt.bufPrint(&num_buf, "{d}", .{val}) catch "?"; try buf.appendSlice(allocator, num_str); } try buf.append(allocator, ']'); if (ate.element_type.data == .type_expr) { try buf.appendSlice(allocator, ate.element_type.data.type_expr.name); } } } } try buf.appendSlice(allocator, " } "); } } try buf.appendSlice(allocator, "{ "); 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, " }"); }, .protocol_decl => |pd| { try buf.appendSlice(allocator, pd.name); try buf.appendSlice(allocator, " :: protocol"); if (pd.is_inline) try buf.appendSlice(allocator, " #inline"); try buf.appendSlice(allocator, " { "); for (pd.methods, 0..) |method, mi| { if (mi > 0) try buf.appendSlice(allocator, " "); try buf.appendSlice(allocator, method.name); try buf.appendSlice(allocator, " :: ("); for (method.param_names, 0..) |pname, pi| { if (pi > 0) try buf.appendSlice(allocator, ", "); try buf.appendSlice(allocator, pname); if (pi < method.params.len) { try buf.appendSlice(allocator, ": "); if (method.params[pi].data == .type_expr) { try buf.appendSlice(allocator, method.params[pi].data.type_expr.name); } else { try buf.appendSlice(allocator, "?"); } } } try buf.append(allocator, ')'); if (method.return_type) |rt| { try buf.appendSlice(allocator, " -> "); if (rt.data == .type_expr) { try buf.appendSlice(allocator, rt.data.type_expr.name); } } try buf.appendSlice(allocator, ";"); } try buf.appendSlice(allocator, " }"); }, .impl_block => |ib| { try buf.appendSlice(allocator, "impl "); try buf.appendSlice(allocator, ib.protocol_name); try buf.appendSlice(allocator, " for "); try buf.appendSlice(allocator, ib.target_type); }, .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 { ... }"); }, .protocol_type => { try buf.appendSlice(allocator, sym.name); try buf.appendSlice(allocator, " :: protocol { ... }"); }, .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; } }; test "findMatchSubjectText: simple identifier" { const source = "if event == {\n case ."; const result = Server.findMatchSubjectText(source, @intCast(source.len)); try std.testing.expectEqualStrings("event", result.?); } test "findMatchSubjectText: field access" { const source = "if e.key == {\n case ."; const result = Server.findMatchSubjectText(source, @intCast(source.len)); try std.testing.expectEqualStrings("e.key", result.?); } test "findMatchSubjectText: nested braces" { const source = "if event == {\n case .quit: { do_something(); }\n case ."; const result = Server.findMatchSubjectText(source, @intCast(source.len)); try std.testing.expectEqualStrings("event", result.?); } test "findMatchSubjectText: no match context" { const source = "while true {\n case ."; const result = Server.findMatchSubjectText(source, @intCast(source.len)); try std.testing.expect(result == null); } test "findCaptureVariant: simple capture" { const source = "case .key_down: (e) {\n e."; const result = Server.findCaptureVariant(source, @intCast(source.len), "e"); try std.testing.expectEqualStrings("key_down", result.?); } test "findCaptureVariant: different name" { const source = "case .mouse_motion: (evt) {\n evt."; const result = Server.findCaptureVariant(source, @intCast(source.len), "evt"); try std.testing.expectEqualStrings("mouse_motion", result.?); } test "findCaptureVariant: no capture" { const source = "case .quit: running = false;\n e."; const result = Server.findCaptureVariant(source, @intCast(source.len), "e"); try std.testing.expect(result == null); } // ---- Helper function tests ---- test "extractQualifiedName: namespace.member at member offset" { const source = "pkg.mul(3, 4)"; // offset inside "mul" (index 4) const result = Server.extractQualifiedName(source, 4); try std.testing.expect(result != null); try std.testing.expectEqualStrings("pkg", result.?.ns); try std.testing.expectEqualStrings("mul", result.?.member); } test "extractQualifiedName: namespace.member at namespace offset" { const source = "pkg.mul(3, 4)"; // offset inside "pkg" (index 1) const result = Server.extractQualifiedName(source, 1); try std.testing.expect(result != null); try std.testing.expectEqualStrings("pkg", result.?.ns); try std.testing.expectEqualStrings("mul", result.?.member); } test "extractQualifiedName: bare identifier returns null" { const source = "hello(42)"; const result = Server.extractQualifiedName(source, 2); try std.testing.expect(result == null); } test "extractQualifiedName: chained a.b.c at c offset" { const source = "a.b.c(1)"; // offset inside "c" (index 4) const result = Server.extractQualifiedName(source, 4); try std.testing.expect(result != null); try std.testing.expectEqualStrings("b", result.?.ns); try std.testing.expectEqualStrings("c", result.?.member); } test "positionToOffset: line 0 char 0" { const source = "hello\nworld\n"; const result = Server.positionToOffset(source, 0, 0); try std.testing.expectEqual(@as(u32, 0), result.?); } test "positionToOffset: line 1 char 0" { const source = "hello\nworld\n"; const result = Server.positionToOffset(source, 1, 0); try std.testing.expectEqual(@as(u32, 6), result.?); } test "positionToOffset: line 0 char 3" { const source = "hello\nworld\n"; const result = Server.positionToOffset(source, 0, 3); try std.testing.expectEqual(@as(u32, 3), result.?); } test "extractIdentAtOffset: middle of identifier" { const source = "foo bar baz"; const result = Server.extractIdentAtOffset(source, 5); try std.testing.expect(result != null); try std.testing.expectEqualStrings("bar", result.?); } test "extractIdentAtOffset: at space returns null" { const source = "foo bar"; const result = Server.extractIdentAtOffset(source, 3); try std.testing.expect(result == null); } test "findSymbolByName: finds existing symbol" { const symbols = [_]sx.sema.Symbol{ .{ .name = "add", .kind = .function, .ty = null, .def_span = .{ .start = 0, .end = 3 }, .scope_depth = 0 }, .{ .name = "mul", .kind = .function, .ty = null, .def_span = .{ .start = 10, .end = 13 }, .scope_depth = 0 }, }; const result = Server.findSymbolByName(&symbols, "mul"); try std.testing.expectEqual(@as(usize, 1), result.?); } test "findSymbolByName: returns null for missing" { const symbols = [_]sx.sema.Symbol{ .{ .name = "add", .kind = .function, .ty = null, .def_span = .{ .start = 0, .end = 3 }, .scope_depth = 0 }, }; const result = Server.findSymbolByName(&symbols, "sub"); try std.testing.expect(result == null); } test "findImportPathAtOffset: inside import string" { const source = "#import \"modules/std.sx\";"; // offset inside the string (index 10) const result = Server.findImportPathAtOffset(source, 10); try std.testing.expect(result != null); try std.testing.expectEqualStrings("modules/std.sx", result.?); } test "findImportPathAtOffset: outside import returns null" { const source = "x := 42;"; const result = Server.findImportPathAtOffset(source, 2); try std.testing.expect(result == null); } // ---- Document analysis pipeline tests ---- test "analyzeDocument: parse and sema basic function" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const src: [:0]const u8 = "add :: (a: s32, b: s32) -> s32 { a + b; }"; const doc = try store.openOrUpdate("/test/main.sx", src, 1); try store.analyzeDocument(doc); const sema = doc.sema orelse return error.SkipZigTest; // Should have "add" as a function symbol try std.testing.expect(Server.findSymbolByName(sema.symbols, "add") != null); const idx = Server.findSymbolByName(sema.symbols, "add").?; try std.testing.expectEqual(sx.sema.SymbolKind.function, sema.symbols[idx].kind); } test "analyzeDocument: flat import pre-registers symbols" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); // Pre-load the imported module const lib_src: [:0]const u8 = "mul :: (a: s32, b: s32) -> s32 { a * b; }"; const lib_doc = try store.openOrUpdate("/test/lib.sx", lib_src, 1); try store.analyzeDocument(lib_doc); // Load main file with flat import const main_src: [:0]const u8 = \\#import "lib.sx"; \\main :: () -> s32 { mul(3, 4); } ; const main_doc = try store.openOrUpdate("/test/main.sx", main_src, 1); try store.analyzeDocument(main_doc); const sema = main_doc.sema orelse return error.SkipZigTest; // "mul" should be pre-registered from flat import try std.testing.expect(Server.findSymbolByName(sema.symbols, "mul") != null); // "main" should be defined locally try std.testing.expect(Server.findSymbolByName(sema.symbols, "main") != null); } test "analyzeDocument: namespaced import registers namespace symbol" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); // Pre-load the imported module const lib_src: [:0]const u8 = "add :: (a: s32, b: s32) -> s32 { a + b; }"; const lib_doc = try store.openOrUpdate("/test/lib.sx", lib_src, 1); try store.analyzeDocument(lib_doc); // Load main file with namespaced import const main_src: [:0]const u8 = \\pkg :: #import "lib.sx"; \\main :: () -> s32 { pkg.add(3, 4); } ; const main_doc = try store.openOrUpdate("/test/main.sx", main_src, 1); try store.analyzeDocument(main_doc); const sema = main_doc.sema orelse return error.SkipZigTest; // "pkg" should be a namespace symbol try std.testing.expect(Server.findSymbolByName(sema.symbols, "pkg") != null); const idx = Server.findSymbolByName(sema.symbols, "pkg").?; try std.testing.expectEqual(sx.sema.SymbolKind.namespace, sema.symbols[idx].kind); } test "analyzeDocument: namespaced import fn_signatures have prefix" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const lib_src: [:0]const u8 = "add :: (a: s32, b: s32) -> s32 { a + b; }"; const lib_doc = try store.openOrUpdate("/test/lib.sx", lib_src, 1); try store.analyzeDocument(lib_doc); const main_src: [:0]const u8 = \\pkg :: #import "lib.sx"; \\main :: () -> s32 { pkg.add(1, 2); } ; const main_doc = try store.openOrUpdate("/test/main.sx", main_src, 1); try store.analyzeDocument(main_doc); const sema = main_doc.sema orelse return error.SkipZigTest; // fn_signatures should contain "pkg.add" try std.testing.expect(sema.fn_signatures.contains("pkg.add")); } test "analyzeDocument: pipe operator desugars to call" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const lib_src: [:0]const u8 = "add :: (a: s32, b: s32) -> s32 { a + b; }"; const lib_doc = try store.openOrUpdate("/test/lib.sx", lib_src, 1); try store.analyzeDocument(lib_doc); // Pipe operator should parse and analyze without errors const main_src: [:0]const u8 = \\pkg :: #import "lib.sx"; \\main :: () -> s32 { 3 |> pkg.add(4); } ; const main_doc = try store.openOrUpdate("/test/main.sx", main_src, 1); try store.analyzeDocument(main_doc); const sema = main_doc.sema orelse return error.SkipZigTest; // Should parse successfully with no sema errors for "add" try std.testing.expect(Server.findSymbolByName(sema.symbols, "main") != null); } test "analyzeDocument: for-loop capture variables are registered" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const src: [:0]const u8 = \\main :: () { \\ arr : [3]s32 = ---; \\ for arr: (it, ix) { \\ x := it + ix; \\ } \\} ; const doc = try store.openOrUpdate("/test/main.sx", src, 1); try store.analyzeDocument(doc); const sema = doc.sema orelse return error.SkipZigTest; // Capture variables should be registered — "it" and "ix" should not produce unresolved errors try std.testing.expect(Server.findSymbolByName(sema.symbols, "it") != null); try std.testing.expect(Server.findSymbolByName(sema.symbols, "ix") != null); // "x" should also be registered try std.testing.expect(Server.findSymbolByName(sema.symbols, "x") != null); } test "analyzeDocument: for-loop with underscore capture" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const src: [:0]const u8 = \\main :: () { \\ arr : [3]s32 = ---; \\ for arr: (_, ix) { \\ x := ix; \\ } \\} ; const doc = try store.openOrUpdate("/test/main.sx", src, 1); try store.analyzeDocument(doc); const sema = doc.sema orelse return error.SkipZigTest; // "_" should NOT be registered as a symbol try std.testing.expect(Server.findSymbolByName(sema.symbols, "_") == null); // "ix" should be registered try std.testing.expect(Server.findSymbolByName(sema.symbols, "ix") != null); } test "analyzeDocument: for-loop value-only capture" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var store = doc_mod.DocumentStore.init(alloc, undefined_io()); const src: [:0]const u8 = \\main :: () { \\ arr : [3]s32 = ---; \\ for arr: (val) { \\ x := val; \\ } \\} ; const doc = try store.openOrUpdate("/test/main.sx", src, 1); try store.analyzeDocument(doc); const sema = doc.sema orelse return error.SkipZigTest; // "val" should be registered try std.testing.expect(Server.findSymbolByName(sema.symbols, "val") != null); } /// Helper: create a dummy std.Io that won't be called (used when all docs are pre-loaded). fn undefined_io() std.Io { return .{ .userdata = null, .vtable = @ptrFromInt(@intFromPtr(@as(?*const std.Io.VTable, null)) | 1) }; }