From bfc784734c52a79579c064a054b5e0d25a1f6ce6 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 24 Feb 2026 20:05:24 +0200 Subject: [PATCH] lsp import --- src/imports.zig | 43 ++++++++++++++++++++++--------------- src/lsp/document.zig | 13 ++++++----- src/lsp/server.zig | 9 ++++---- tests/expected/50-smoke.txt | 2 ++ 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/imports.zig b/src/imports.zig index 6dcf03c..3165f5b 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -17,6 +17,31 @@ pub fn dirName(path: []const u8) []const u8 { return if (found) path[0..last_sep] else "."; } +/// Resolve an import path: try relative to base_dir first, fall back to cwd-relative. +/// If root_path is provided, CWD-relative fallback paths are made absolute. +/// Shared between compiler (resolveImports) and LSP (analyzeDocument). +pub fn resolveImportPath(allocator: std.mem.Allocator, io: std.Io, base_dir: []const u8, raw_path: []const u8, root_path: ?[]const u8) ![]const u8 { + if (!std.mem.eql(u8, base_dir, ".")) { + const rel_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ base_dir, raw_path }); + // Check if it exists as file relative to base_dir + if (std.Io.Dir.readFileAlloc(.cwd(), io, rel_path, allocator, .limited(10 * 1024 * 1024))) |_| { + return rel_path; + } else |_| {} + // Check if it exists as directory relative to base_dir + if (std.Io.Dir.openDir(.cwd(), io, rel_path, .{})) |dir| { + dir.close(io); + return rel_path; + } else |_| {} + } + // Fall back to raw path (cwd-relative); absolutify if root_path is known + if (root_path) |rp| { + if (rp.len > 0 and raw_path.len > 0 and raw_path[0] != '/') { + return std.fmt.allocPrint(allocator, "{s}/{s}", .{ rp, raw_path }); + } + } + return raw_path; +} + /// A resolved module: the fully-resolved declarations of a single .sx file, /// with its own scope tracking which names are defined. pub const ResolvedModule = struct { @@ -142,23 +167,7 @@ pub fn resolveImports( } const imp = decl.data.import_decl; - // Resolve path: try relative to file dir first, then fall back to cwd-relative - const resolved_path = resolvePath: { - if (!std.mem.eql(u8, base_dir, ".")) { - const rel_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ base_dir, imp.path }); - // Check if it exists as file or directory relative to base_dir - if (std.Io.Dir.readFileAlloc(.cwd(), io, rel_path, allocator, .limited(10 * 1024 * 1024))) |_| { - break :resolvePath rel_path; - } else |_| {} - // Try as directory - if (std.Io.Dir.openDir(.cwd(), io, rel_path, .{})) |dir| { - dir.close(io); - break :resolvePath rel_path; - } else |_| {} - } - // Fall back to raw path (cwd-relative) - break :resolvePath imp.path; - }; + const resolved_path = try resolveImportPath(allocator, io, base_dir, imp.path, null); // Circular import check — only along the current chain if (chain.contains(resolved_path)) continue; diff --git a/src/lsp/document.zig b/src/lsp/document.zig index dacbc7a..d40b1d5 100644 --- a/src/lsp/document.zig +++ b/src/lsp/document.zig @@ -45,6 +45,8 @@ pub const Document = struct { pub const DocumentStore = struct { allocator: std.mem.Allocator, io: std.Io, + /// Workspace root path (from initialize). Used to absolutify CWD-relative import paths. + root_path: []const u8 = "", /// All loaded documents keyed by resolved file path. by_path: std.StringHashMap(*Document), @@ -56,6 +58,10 @@ pub const DocumentStore = struct { }; } + fn rootPathOpt(self: *const DocumentStore) ?[]const u8 { + return if (self.root_path.len > 0) self.root_path else null; + } + /// Get or create a document for the given file path. Reads from disk if not yet loaded. pub fn getOrLoad(self: *DocumentStore, path: []const u8) !*Document { if (self.by_path.get(path)) |doc| return doc; @@ -171,17 +177,14 @@ pub const DocumentStore = struct { const root = doc.root orelse return; - // Extract imports from AST + // Extract imports from AST — uses shared resolution logic from imports.zig var import_list = std.ArrayList(Import).empty; const base_dir = sx.imports.dirName(doc.path); if (root.data == .root) { for (root.data.root.decls) |decl| { if (decl.data != .import_decl) continue; const imp = decl.data.import_decl; - const resolved_path = if (std.mem.eql(u8, base_dir, ".")) - imp.path - else - try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ base_dir, imp.path }); + const resolved_path = try sx.imports.resolveImportPath(self.allocator, self.io, base_dir, imp.path, self.rootPathOpt()); try import_list.append(self.allocator, .{ .ns = imp.name, .path = resolved_path, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 9c08f9b..5ca5dcb 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -129,6 +129,7 @@ pub const Server = struct { 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); } @@ -249,13 +250,11 @@ pub const Server = struct { // 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 resolved = if (std.mem.eql(u8, base_dir, ".")) - import_path - else - try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ base_dir, import_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); // For directory imports, try to read as file first - if (std.Io.Dir.readFileAlloc(.cwd(), self.io, resolved, self.allocator, .limited(1))) |_| { + 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{ diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 29dfc26..671dec9 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -561,4 +561,6 @@ P6.5: 20 P7.1: 30 P7.2: 10 300 P2.6: 5 10 +P2.7: 15 +P3.3: 102 === DONE ===