pipes
This commit is contained in:
@@ -177,12 +177,27 @@ pub const Server = struct {
|
||||
// Namespace import member
|
||||
if (self.findImportByNs(doc, qn.ns)) |imp| {
|
||||
if (self.documents.get(imp.path)) |imp_doc| {
|
||||
// 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.sendSymbolLocation(id_json, imp_doc, sym)) 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.sendSymbolLocation(id_json, dir_doc, sym)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,21 +225,28 @@ pub const Server = struct {
|
||||
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
||||
}
|
||||
|
||||
// 4. #import "path" string → open the file
|
||||
// 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 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;
|
||||
|
||||
// For directory imports, try to read as file first
|
||||
if (std.Io.Dir.readFileAlloc(.cwd(), self.io, resolved, self.allocator, .limited(1))) |_| {
|
||||
// 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
|
||||
@@ -1032,6 +1054,7 @@ pub const Server = struct {
|
||||
.at,
|
||||
.pipe,
|
||||
.pipe_equal,
|
||||
.pipe_arrow,
|
||||
.caret,
|
||||
.caret_equal,
|
||||
.tilde,
|
||||
@@ -1283,6 +1306,17 @@ pub const Server = struct {
|
||||
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")
|
||||
@@ -1292,13 +1326,15 @@ pub const Server = struct {
|
||||
|
||||
if (self.findImportByNs(doc, ns_name)) |imp| {
|
||||
if (self.documents.get(imp.path)) |imp_doc| {
|
||||
if (imp_doc.root) |imp_root| {
|
||||
if (imp_root.data == .root) {
|
||||
for (imp_root.data.root.decls) |decl| {
|
||||
if (decl.data == .fn_decl and std.mem.eql(u8, decl.data.fn_decl.name, fn_name)) {
|
||||
return decl.data.fn_decl;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1590,7 +1626,7 @@ pub const Server = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn positionToOffset(source: []const u8, line: u32, character: u32) ?u32 {
|
||||
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| {
|
||||
@@ -1694,7 +1730,7 @@ pub const Server = struct {
|
||||
return name;
|
||||
}
|
||||
|
||||
fn extractIdentAtOffset(source: []const u8, offset: u32) ?[]const u8 {
|
||||
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])) {
|
||||
@@ -1712,7 +1748,7 @@ pub const Server = struct {
|
||||
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_';
|
||||
}
|
||||
|
||||
fn findImportPathAtOffset(source: []const u8, offset: u32) ?[]const u8 {
|
||||
pub fn findImportPathAtOffset(source: []const u8, offset: u32) ?[]const u8 {
|
||||
if (offset >= source.len) return null;
|
||||
|
||||
var qstart: u32 = offset;
|
||||
@@ -1739,7 +1775,7 @@ pub const Server = struct {
|
||||
return source[qstart + 1 .. qend];
|
||||
}
|
||||
|
||||
fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8 } {
|
||||
pub fn extractQualifiedName(source: []const u8, offset: u32) ?struct { ns: []const u8, member: []const u8 } {
|
||||
if (offset >= source.len) return null;
|
||||
|
||||
var end: u32 = offset;
|
||||
@@ -1774,7 +1810,7 @@ pub const Server = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findSymbolByName(symbols: []const sx.sema.Symbol, name: []const u8) ?usize {
|
||||
pub fn findSymbolByName(symbols: []const sx.sema.Symbol, name: []const u8) ?usize {
|
||||
var i = symbols.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
@@ -2126,3 +2162,299 @@ test "findCaptureVariant: no capture" {
|
||||
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) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user