...
This commit is contained in:
@@ -28,6 +28,8 @@ pub const Document = struct {
|
||||
last_good_sema: ?sx.sema.SemaResult = null,
|
||||
/// Import declarations parsed from this file.
|
||||
imports: []const Import,
|
||||
/// True while this document is being analyzed (circular import guard).
|
||||
is_analyzing: bool = false,
|
||||
|
||||
pub fn topLevelSymbols(self: *const Document) []const sx.sema.Symbol {
|
||||
const sr = self.sema orelse return &.{};
|
||||
@@ -115,6 +117,10 @@ pub const DocumentStore = struct {
|
||||
|
||||
/// Analyze a document: parse, resolve imports, run sema with imported symbols pre-registered.
|
||||
pub fn analyzeDocument(self: *DocumentStore, doc: *Document) !void {
|
||||
if (doc.is_analyzing) return; // circular import guard
|
||||
doc.is_analyzing = true;
|
||||
defer doc.is_analyzing = false;
|
||||
|
||||
// Parse if needed
|
||||
if (doc.root == null) {
|
||||
var p = sx.parser.Parser.init(self.allocator, doc.source);
|
||||
@@ -144,10 +150,6 @@ pub const DocumentStore = struct {
|
||||
// Recursively analyze imported documents and pre-register their symbols
|
||||
var analyzer = sx.sema.Analyzer.init(self.allocator);
|
||||
|
||||
// Track in-progress documents to detect cycles
|
||||
var cycle_guard = std.StringHashMap(void).init(self.allocator);
|
||||
try cycle_guard.put(doc.path, {});
|
||||
|
||||
for (doc.imports) |imp| {
|
||||
// Try as file first; if that fails, try as directory import
|
||||
const imp_doc = self.getOrLoad(imp.path) catch {
|
||||
@@ -155,11 +157,8 @@ pub const DocumentStore = struct {
|
||||
const dir_files = self.listDirectoryFiles(imp.path) orelse continue;
|
||||
for (dir_files) |file_path| {
|
||||
const file_doc = self.getOrLoad(file_path) catch continue;
|
||||
if (cycle_guard.contains(file_path)) continue;
|
||||
if (file_doc.sema == null) {
|
||||
try cycle_guard.put(file_path, {});
|
||||
self.analyzeDocument(file_doc) catch {};
|
||||
_ = cycle_guard.remove(file_path);
|
||||
}
|
||||
const file_sema = file_doc.sema orelse continue;
|
||||
if (imp.ns) |ns_name| {
|
||||
@@ -219,14 +218,9 @@ pub const DocumentStore = struct {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Cycle detection
|
||||
if (cycle_guard.contains(imp.path)) continue;
|
||||
|
||||
// Ensure imported doc is analyzed
|
||||
if (imp_doc.sema == null) {
|
||||
try cycle_guard.put(imp.path, {});
|
||||
self.analyzeDocument(imp_doc) catch {};
|
||||
_ = cycle_guard.remove(imp.path);
|
||||
}
|
||||
|
||||
const imp_sema = imp_doc.sema orelse continue;
|
||||
|
||||
@@ -174,6 +174,7 @@ pub const Server = struct {
|
||||
|
||||
// 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| {
|
||||
@@ -181,7 +182,7 @@ pub const Server = struct {
|
||||
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;
|
||||
if (try self.sendSymbolLocationWithOrigin(id_json, imp_doc, sym, qn_origin)) return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -194,7 +195,7 @@ pub const Server = struct {
|
||||
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;
|
||||
if (try self.sendSymbolLocationWithOrigin(id_json, dir_doc, sym, qn_origin)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +206,7 @@ pub const Server = struct {
|
||||
if (findSymbolByName(sema.symbols, qn.member)) |si| {
|
||||
const sym = sema.symbols[si];
|
||||
if (sym.kind == .function) {
|
||||
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
||||
if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, qn_origin)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,14 +216,14 @@ pub const Server = struct {
|
||||
const ref = sema.references[ref_idx];
|
||||
if (ref.symbol_index < sema.symbols.len) {
|
||||
const sym = sema.symbols[ref.symbol_index];
|
||||
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
||||
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.sendSymbolLocation(id_json, doc, sym)) return;
|
||||
if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, sym.def_span)) return;
|
||||
}
|
||||
|
||||
// 4. #import "path" string → open the file (or directory)
|
||||
@@ -256,7 +257,9 @@ pub const Server = struct {
|
||||
if (!is_qualified) {
|
||||
if (findSymbolByName(sema.symbols, name)) |si| {
|
||||
const sym = sema.symbols[si];
|
||||
if (try self.sendSymbolLocation(id_json, doc, sym)) return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1112,6 +1115,11 @@ pub const Server = struct {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1270,20 +1278,34 @@ pub const Server = struct {
|
||||
|
||||
/// 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| {
|
||||
// Symbol is from an imported file
|
||||
const origin_doc = self.documents.get(origin_path) orelse return false;
|
||||
const range = spanToRange(origin_doc.source, sym.def_span);
|
||||
const target_range = spanToRange(origin_doc.source, sym.def_span);
|
||||
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{origin_path});
|
||||
const loc_json = try lsp.locationJson(self.allocator, target_uri, range);
|
||||
try self.sendResponse(id_json, loc_json);
|
||||
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 {
|
||||
// Symbol is local
|
||||
const range = spanToRange(doc.source, sym.def_span);
|
||||
const target_range = spanToRange(doc.source, sym.def_span);
|
||||
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{doc.path});
|
||||
const loc_json = try lsp.locationJson(self.allocator, target_uri, range);
|
||||
try self.sendResponse(id_json, loc_json);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1775,7 +1797,7 @@ pub const Server = struct {
|
||||
return source[qstart + 1 .. qend];
|
||||
}
|
||||
|
||||
pub 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, full_start: u32, full_end: u32 } {
|
||||
if (offset >= source.len) return null;
|
||||
|
||||
var end: u32 = offset;
|
||||
@@ -1792,6 +1814,8 @@ pub const Server = struct {
|
||||
return .{
|
||||
.ns = source[ns_start .. start - 1],
|
||||
.member = source[start..end],
|
||||
.full_start = ns_start,
|
||||
.full_end = end,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1803,6 +1827,8 @@ pub const Server = struct {
|
||||
return .{
|
||||
.ns = source[start..end],
|
||||
.member = source[end + 1 .. member_end],
|
||||
.full_start = start,
|
||||
.full_end = member_end,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,23 @@ pub fn locationJson(allocator: std.mem.Allocator, uri: []const u8, range: Range)
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a LocationLink JSON response (for go-to-definition with origin range).
|
||||
pub fn locationLinkJson(allocator: std.mem.Allocator, target_uri: []const u8, target_range: Range, origin_range: Range) ![]const u8 {
|
||||
const uri_escaped = try jsonString(allocator, target_uri);
|
||||
return std.fmt.allocPrint(allocator,
|
||||
"[{{\"originSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++
|
||||
"\"targetUri\":{s}," ++
|
||||
"\"targetRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++
|
||||
"\"targetSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}]",
|
||||
.{
|
||||
origin_range.start.line, origin_range.start.character, origin_range.end.line, origin_range.end.character,
|
||||
uri_escaped,
|
||||
target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character,
|
||||
target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a Hover JSON response.
|
||||
pub fn hoverJson(allocator: std.mem.Allocator, contents: []const u8) ![]const u8 {
|
||||
const escaped = try jsonString(allocator, contents);
|
||||
|
||||
Reference in New Issue
Block a user