zls/src/DocumentStore.zig

1077 lines
38 KiB
Zig
Raw Normal View History

const std = @import("std");
const builtin = @import("builtin");
const types = @import("lsp.zig");
const URI = @import("uri.zig");
const analysis = @import("analysis.zig");
const offsets = @import("offsets.zig");
2022-09-13 21:12:32 +01:00
const log = std.log.scoped(.store);
const Ast = std.zig.Ast;
const BuildAssociatedConfig = @import("BuildAssociatedConfig.zig");
const BuildConfig = @import("special/build_runner.zig").BuildConfig;
const tracy = @import("tracy.zig");
const Config = @import("Config.zig");
const ZigVersionWrapper = @import("ZigVersionWrapper.zig");
2022-08-18 23:00:46 +01:00
const translate_c = @import("translate_c.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
const DocumentStore = @This();
2022-10-05 12:23:38 +01:00
pub const Uri = []const u8;
2022-08-18 23:00:46 +01:00
2022-10-05 12:23:38 +01:00
pub const Hasher = std.crypto.auth.siphash.SipHash128(1, 3);
pub const Hash = [Hasher.mac_length]u8;
pub fn computeHash(bytes: []const u8) Hash {
var hasher: Hasher = Hasher.init(&[_]u8{0} ** Hasher.key_length);
hasher.update(bytes);
var hash: Hash = undefined;
hasher.final(&hash);
return hash;
}
2022-08-18 23:00:46 +01:00
2020-05-25 15:24:44 +01:00
const BuildFile = struct {
2022-10-10 19:01:54 +01:00
uri: Uri,
/// contains information extracted from running build.zig with a custom build runner
/// e.g. include paths & packages
2022-10-05 12:23:38 +01:00
config: BuildConfig,
2022-10-10 19:01:54 +01:00
/// this build file may have an explicitly specified path to builtin.zig
2022-10-05 12:23:38 +01:00
builtin_uri: ?Uri = null,
build_associated_config: ?BuildAssociatedConfig = null,
2022-10-05 12:23:38 +01:00
pub fn deinit(self: *BuildFile, allocator: std.mem.Allocator) void {
allocator.free(self.uri);
std.json.parseFree(BuildConfig, self.config, .{ .allocator = allocator });
if (self.builtin_uri) |builtin_uri| allocator.free(builtin_uri);
2022-10-05 12:23:38 +01:00
if (self.build_associated_config) |cfg| {
std.json.parseFree(BuildAssociatedConfig, cfg, .{ .allocator = allocator });
2022-08-22 15:54:56 +01:00
}
}
};
pub const Handle = struct {
2022-10-05 12:23:38 +01:00
/// `true` if the document has been directly opened by the client i.e. with `textDocument/didOpen`
/// `false` indicates the document only exists because it is a dependency of another document
/// or has been closed with `textDocument/didClose` and is awaiting cleanup through `garbageCollection`
open: bool,
uri: Uri,
text: [:0]const u8,
tree: Ast,
/// Not null if a ComptimeInterpreter is actually used
interpreter: ?*ComptimeInterpreter = null,
document_scope: analysis.DocumentScope,
2021-03-30 13:41:59 +01:00
/// Contains one entry for every import in the document
2022-10-05 12:23:38 +01:00
import_uris: std.ArrayListUnmanaged(Uri) = .{},
2022-08-18 23:00:46 +01:00
/// Contains one entry for every cimport in the document
2022-10-05 12:23:38 +01:00
cimports: std.MultiArrayList(CImportHandle) = .{},
2022-10-10 19:01:54 +01:00
/// `DocumentStore.build_files` is guaranteed to contain this uri
/// uri memory managed by its build_file
2022-10-05 12:23:38 +01:00
associated_build_file: ?Uri = null,
pub fn deinit(self: *Handle, allocator: std.mem.Allocator) void {
2023-01-24 21:07:19 +00:00
if (self.interpreter) |interpreter| {
interpreter.deinit();
allocator.destroy(interpreter);
}
2022-10-05 12:23:38 +01:00
self.document_scope.deinit(allocator);
self.tree.deinit(allocator);
allocator.free(self.text);
allocator.free(self.uri);
2022-10-05 12:23:38 +01:00
for (self.import_uris.items) |import_uri| {
allocator.free(import_uri);
}
self.import_uris.deinit(allocator);
for (self.cimports.items(.source)) |source| {
allocator.free(source);
}
2022-10-05 12:23:38 +01:00
self.cimports.deinit(allocator);
}
};
2021-12-02 05:16:15 +00:00
allocator: std.mem.Allocator,
2022-10-05 12:23:38 +01:00
config: *const Config,
runtime_zig_version: *const ?ZigVersionWrapper,
handles: std.StringArrayHashMapUnmanaged(*Handle) = .{},
2022-10-05 12:23:38 +01:00
build_files: std.StringArrayHashMapUnmanaged(BuildFile) = .{},
cimports: std.AutoArrayHashMapUnmanaged(Hash, translate_c.Result) = .{},
pub fn deinit(self: *DocumentStore) void {
for (self.handles.values()) |handle| {
2022-10-05 12:23:38 +01:00
handle.deinit(self.allocator);
self.allocator.destroy(handle);
2022-10-05 12:23:38 +01:00
}
self.handles.deinit(self.allocator);
for (self.build_files.values()) |*build_file| {
build_file.deinit(self.allocator);
}
self.build_files.deinit(self.allocator);
for (self.cimports.values()) |*result| {
result.deinit(self.allocator);
}
self.cimports.deinit(self.allocator);
}
/// returns a handle to the given document
2022-10-17 19:33:37 +01:00
pub fn getHandle(self: *DocumentStore, uri: Uri) ?*const Handle {
return self.handles.get(uri);
}
/// returns a handle to the given document
/// will load the document from disk if it hasn't been already
pub fn getOrLoadHandle(self: *DocumentStore, uri: Uri) ?*const Handle {
return self.getOrLoadHandleInternal(uri) catch null;
}
fn getOrLoadHandleInternal(self: *DocumentStore, uri: Uri) !?*const Handle {
if (self.handles.get(uri)) |handle| return handle;
var handle = try self.allocator.create(Handle);
errdefer self.allocator.destroy(handle);
2022-12-27 05:52:15 +00:00
handle.* = (try self.createDocumentFromURI(uri, false)) orelse return error.Unknown; // error name doesn't matter
errdefer handle.deinit(self.allocator);
const gop = try self.handles.getOrPutValue(self.allocator, handle.uri, handle);
2022-12-27 05:52:15 +00:00
if (gop.found_existing) return error.Unknown;
2022-10-05 12:23:38 +01:00
return gop.value_ptr.*;
2022-10-05 12:23:38 +01:00
}
pub fn openDocument(self: *DocumentStore, uri: Uri, text: []const u8) error{OutOfMemory}!Handle {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (self.handles.get(uri)) |handle| {
2022-10-05 12:23:38 +01:00
if (handle.open) {
log.warn("Document already open: {s}", .{uri});
} else {
handle.open = true;
}
return handle.*;
}
var handle = try self.allocator.create(Handle);
errdefer self.allocator.destroy(handle);
const duped_text = try self.allocator.dupeZ(u8, text);
2022-12-27 05:52:15 +00:00
handle.* = try self.createDocument(uri, duped_text, true);
2022-10-05 12:23:38 +01:00
errdefer handle.deinit(self.allocator);
2022-12-27 05:52:15 +00:00
try self.handles.putNoClobber(self.allocator, handle.uri, handle);
2022-10-05 12:23:38 +01:00
return handle.*;
}
2022-10-05 12:23:38 +01:00
pub fn closeDocument(self: *DocumentStore, uri: Uri) void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
const handle = self.handles.get(uri) orelse {
2022-10-05 12:23:38 +01:00
log.warn("Document not found: {s}", .{uri});
return;
};
2022-10-10 19:01:54 +01:00
// instead of destroying the handle here we just mark it not open
// and let it be destroy by the garbage collection code
2022-10-05 12:23:38 +01:00
if (handle.open) {
handle.open = false;
} else {
log.warn("Document already closed: {s}", .{uri});
}
self.garbageCollectionImports() catch {};
self.garbageCollectionCImports() catch {};
self.garbageCollectionBuildFiles() catch {};
}
2022-10-17 19:33:37 +01:00
/// takes ownership of `new_text` which has to be allocated with `self.allocator`
pub fn refreshDocument(self: *DocumentStore, uri: Uri, new_text: [:0]const u8) !void {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-10-17 19:33:37 +01:00
const handle = self.handles.get(uri) orelse unreachable;
// TODO: Handle interpreter cross reference
if (handle.interpreter) |int| {
int.deinit();
handle.interpreter = null;
}
2022-10-17 19:33:37 +01:00
self.allocator.free(handle.text);
handle.text = new_text;
var new_tree = try Ast.parse(self.allocator, handle.text, .zig);
2022-10-05 12:23:38 +01:00
handle.tree.deinit(self.allocator);
handle.tree = new_tree;
var new_document_scope = try analysis.makeDocumentScope(self.allocator, handle.tree);
handle.document_scope.deinit(self.allocator);
handle.document_scope = new_document_scope;
var new_import_uris = try self.collectImportUris(handle.*);
for (handle.import_uris.items) |import_uri| {
self.allocator.free(import_uri);
}
const old_import_count = handle.import_uris.items.len;
const new_import_count = new_import_uris.items.len;
2022-10-05 12:23:38 +01:00
handle.import_uris.deinit(self.allocator);
handle.import_uris = new_import_uris;
var new_cimports = try self.collectCIncludes(handle.*);
const old_cimport_count = handle.cimports.len;
const new_cimport_count = new_cimports.len;
for (handle.cimports.items(.source)) |source| {
self.allocator.free(source);
}
2022-10-05 12:23:38 +01:00
handle.cimports.deinit(self.allocator);
handle.cimports = new_cimports;
if (old_import_count != new_import_count or
old_cimport_count != new_cimport_count)
{
self.garbageCollectionImports() catch {};
self.garbageCollectionCImports() catch {};
}
2022-10-05 12:23:38 +01:00
}
pub fn applySave(self: *DocumentStore, handle: *const Handle) !void {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (std.process.can_spawn and isBuildFile(handle.uri)) {
2022-10-05 12:23:38 +01:00
const build_file = self.build_files.getPtr(handle.uri).?;
const build_config = loadBuildConfiguration(
self.allocator,
build_file.*,
self.config.*,
self.runtime_zig_version.*.?, // if we have the path to zig we should have the zig version
) catch |err| {
2022-10-05 12:23:38 +01:00
log.err("Failed to load build configuration for {s} (error: {})", .{ build_file.uri, err });
return;
};
2022-10-05 12:23:38 +01:00
std.json.parseFree(BuildConfig, build_file.config, .{ .allocator = self.allocator });
build_file.config = build_config;
}
}
2022-10-05 12:23:38 +01:00
/// The `DocumentStore` represents a graph structure where every
/// handle/document is a node and every `@import` and `@cImport` represent
2022-10-05 12:23:38 +01:00
/// a directed edge.
/// We can remove every document which cannot be reached from
/// another document that is `open` (see `Handle.open`)
fn garbageCollectionImports(self: *DocumentStore) error{OutOfMemory}!void {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-12-27 05:52:15 +00:00
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
var reachable = try std.DynamicBitSetUnmanaged.initEmpty(arena.allocator(), self.handles.count());
2022-10-05 12:23:38 +01:00
var queue = std.ArrayListUnmanaged(Uri){};
for (self.handles.values(), 0..) |handle, handle_index| {
2022-10-05 12:23:38 +01:00
if (!handle.open) continue;
reachable.set(handle_index);
2022-10-05 12:23:38 +01:00
2022-12-27 05:52:15 +00:00
try self.collectDependencies(arena.allocator(), handle.*, &queue);
2022-10-05 12:23:38 +01:00
}
2022-10-05 12:23:38 +01:00
while (queue.popOrNull()) |uri| {
const handle_index = self.handles.getIndex(uri) orelse continue;
if (reachable.isSet(handle_index)) continue;
reachable.set(handle_index);
2022-10-05 12:23:38 +01:00
const handle = self.handles.values()[handle_index];
2022-10-05 12:23:38 +01:00
2022-12-27 05:52:15 +00:00
try self.collectDependencies(arena.allocator(), handle.*, &queue);
}
var it = reachable.iterator(.{
.kind = .unset,
.direction = .reverse,
});
while (it.next()) |handle_index| {
const handle = self.handles.values()[handle_index];
log.debug("Closing document {s}", .{handle.uri});
self.handles.swapRemoveAt(handle_index);
handle.deinit(self.allocator);
self.allocator.destroy(handle);
2022-10-05 12:23:38 +01:00
}
}
/// see `garbageCollectionImports`
fn garbageCollectionCImports(self: *DocumentStore) error{OutOfMemory}!void {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (self.cimports.count() == 0) return;
var reachable = try std.DynamicBitSetUnmanaged.initEmpty(self.allocator, self.cimports.count());
defer reachable.deinit(self.allocator);
2022-10-05 12:23:38 +01:00
for (self.handles.values()) |handle| {
for (handle.cimports.items(.hash)) |hash| {
const index = self.cimports.getIndex(hash).?;
reachable.set(index);
2022-10-05 12:23:38 +01:00
}
}
2020-05-25 17:33:08 +01:00
var it = reachable.iterator(.{
.kind = .unset,
.direction = .reverse,
});
while (it.next()) |cimport_index| {
var result = self.cimports.values()[cimport_index];
const message = switch (result) {
.failure => "",
.success => |uri| uri,
};
log.debug("Destroying cimport {s}", .{message});
self.cimports.swapRemoveAt(cimport_index);
result.deinit(self.allocator);
}
}
/// see `garbageCollectionImports`
fn garbageCollectionBuildFiles(self: *DocumentStore) error{OutOfMemory}!void {
2022-10-17 19:23:51 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (self.build_files.count() == 0) return;
var reachable = try std.DynamicBitSetUnmanaged.initEmpty(self.allocator, self.build_files.count());
defer reachable.deinit(self.allocator);
2022-10-05 12:23:38 +01:00
for (self.handles.values()) |handle| {
const build_file_uri = handle.associated_build_file orelse continue;
const build_file_index = self.build_files.getIndex(build_file_uri).?;
reachable.set(build_file_index);
2022-10-05 12:23:38 +01:00
}
var it = reachable.iterator(.{
.kind = .unset,
.direction = .reverse,
});
while (it.next()) |build_file_index| {
var build_file = self.build_files.values()[build_file_index];
log.debug("Destroying build file {s}", .{build_file.uri});
self.build_files.swapRemoveAt(build_file_index);
build_file.deinit(self.allocator);
2022-10-05 12:23:38 +01:00
}
}
2022-12-29 23:21:26 +00:00
pub fn isBuildFile(uri: Uri) bool {
return std.mem.endsWith(u8, uri, "/build.zig");
}
pub fn isBuiltinFile(uri: Uri) bool {
return std.mem.endsWith(u8, uri, "/builtin.zig");
}
pub fn isInStd(uri: Uri) bool {
// TODO: Better logic for detecting std or subdirectories?
return std.mem.indexOf(u8, uri, "/std/") != null;
}
2022-10-05 12:23:38 +01:00
/// looks for a `zls.build.json` file in the build file directory
/// has to be freed with `std.json.parseFree`
fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: BuildFile) !BuildAssociatedConfig {
2022-06-06 04:50:17 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-10-05 12:23:38 +01:00
const build_file_path = try URI.parse(allocator, build_file.uri);
defer allocator.free(build_file_path);
const config_file_path = try std.fs.path.resolve(allocator, &.{ build_file_path, "../zls.build.json" });
defer allocator.free(config_file_path);
var config_file = try std.fs.cwd().openFile(config_file_path, .{});
defer config_file.close();
const file_buf = try config_file.readToEndAlloc(allocator, std.math.maxInt(usize));
defer allocator.free(file_buf);
var token_stream = std.json.TokenStream.init(file_buf);
return try std.json.parse(BuildAssociatedConfig, &token_stream, .{ .allocator = allocator });
}
/// runs the build.zig and extracts include directories and packages
/// has to be freed with `std.json.parseFree`
fn loadBuildConfiguration(
allocator: std.mem.Allocator,
build_file: BuildFile,
config: Config,
runtime_zig_version: ZigVersionWrapper,
2022-10-05 12:23:38 +01:00
) !BuildConfig {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2020-05-25 17:33:08 +01:00
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const arena_allocator = arena.allocator();
2022-10-05 12:23:38 +01:00
const build_file_path = try URI.parse(arena_allocator, build_file.uri);
const directory_path = try std.fs.path.resolve(arena_allocator, &.{ build_file_path, "../" });
// TODO extract this option from `BuildAssociatedConfig.BuildOption`
2022-11-29 22:50:09 +00:00
const zig_cache_root: []const u8 = try std.fs.path.join(arena_allocator, &.{ directory_path, "zig-cache" });
2020-05-25 17:33:08 +01:00
// introduction of modified module cli arguments https://github.com/ziglang/zig/pull/14664
const module_version = comptime std.SemanticVersion.parse("0.11.0-dev.1718+2737dce84") catch unreachable;
const use_new_module_cli = runtime_zig_version.version.order(module_version) != .lt;
const standard_args = if (use_new_module_cli) blk: {
const build_module = try std.fmt.allocPrint(arena_allocator, "@build@::{s}", .{build_file_path});
break :blk [_][]const u8{
config.zig_exe_path.?,
"run",
config.build_runner_path.?,
"--cache-dir",
config.global_cache_path.?,
"--mod",
build_module,
"--deps",
"@build@",
"--",
config.zig_exe_path.?,
directory_path,
zig_cache_root,
config.build_runner_global_cache_path.?,
};
} else [_][]const u8{
2022-10-05 12:23:38 +01:00
config.zig_exe_path.?,
2022-09-28 13:33:48 +01:00
"run",
2022-10-05 12:23:38 +01:00
config.build_runner_path.?,
2022-09-28 13:33:48 +01:00
"--cache-dir",
2022-10-05 12:23:38 +01:00
config.global_cache_path.?,
2022-09-28 13:33:48 +01:00
"--pkg-begin",
"@build@",
build_file_path,
"--pkg-end",
"--",
2022-10-05 12:23:38 +01:00
config.zig_exe_path.?,
2022-09-28 13:33:48 +01:00
directory_path,
2022-10-05 12:23:38 +01:00
zig_cache_root,
config.build_runner_global_cache_path.?,
2022-09-28 13:33:48 +01:00
};
2022-10-05 12:23:38 +01:00
const arg_length = standard_args.len + if (build_file.build_associated_config) |cfg| if (cfg.build_options) |options| options.len else 0 else 0;
var args = try std.ArrayListUnmanaged([]const u8).initCapacity(arena_allocator, arg_length);
args.appendSliceAssumeCapacity(standard_args[0..]);
if (build_file.build_associated_config) |cfg| {
if (cfg.build_options) |options| {
for (options) |opt| {
args.appendAssumeCapacity(try opt.formatParam(arena_allocator));
}
}
}
2020-05-25 17:33:08 +01:00
const zig_run_result = try std.ChildProcess.exec(.{
2022-10-05 12:23:38 +01:00
.allocator = arena_allocator,
.argv = args.items,
.cwd = try std.fs.path.resolve(arena_allocator, &.{ config.zig_exe_path.?, "../" }),
2020-05-25 17:33:08 +01:00
});
defer {
2022-10-05 12:23:38 +01:00
arena_allocator.free(zig_run_result.stdout);
arena_allocator.free(zig_run_result.stderr);
}
2020-05-25 17:33:08 +01:00
errdefer blk: {
2022-10-05 12:23:38 +01:00
const joined = std.mem.join(arena_allocator, " ", args.items) catch break :blk;
log.err(
"Failed to execute build runner to collect build configuration, command:\n{s}\nError: {s}",
.{ joined, zig_run_result.stderr },
);
}
2020-05-25 17:33:08 +01:00
switch (zig_run_result.term) {
2022-10-05 12:23:38 +01:00
.Exited => |exit_code| if (exit_code != 0) return error.RunFailed,
else => return error.RunFailed,
}
2022-08-22 15:54:56 +01:00
2022-10-05 12:23:38 +01:00
const parse_options = std.json.ParseOptions{ .allocator = allocator };
var token_stream = std.json.TokenStream.init(zig_run_result.stdout);
var build_config = std.json.parse(BuildConfig, &token_stream, parse_options) catch return error.RunFailed;
2022-12-27 05:52:15 +00:00
errdefer std.json.parseFree(BuildConfig, build_config, parse_options);
2022-08-22 15:54:56 +01:00
2022-10-05 12:23:38 +01:00
for (build_config.packages) |*pkg| {
const pkg_abs_path = try std.fs.path.resolve(allocator, &[_][]const u8{ directory_path, pkg.path });
allocator.free(pkg.path);
pkg.path = pkg_abs_path;
2020-05-25 17:33:08 +01:00
}
2022-10-05 12:23:38 +01:00
return build_config;
2020-05-25 17:33:08 +01:00
}
2020-05-25 15:24:44 +01:00
// walks the build.zig files above "uri"
const BuildDotZigIterator = struct {
allocator: std.mem.Allocator,
uri_path: []const u8,
dir_path: []const u8,
i: usize,
fn init(allocator: std.mem.Allocator, uri_path: []const u8) !BuildDotZigIterator {
const dir_path = std.fs.path.dirname(uri_path) orelse uri_path;
return BuildDotZigIterator{
.allocator = allocator,
.uri_path = uri_path,
.dir_path = dir_path,
.i = std.fs.path.diskDesignator(uri_path).len + 1,
};
}
// the iterator allocates this memory so you gotta free it
fn next(self: *BuildDotZigIterator) !?[]const u8 {
while (true) {
if (self.i > self.dir_path.len)
return null;
const potential_build_path = try std.fs.path.join(self.allocator, &.{
self.dir_path[0..self.i], "build.zig",
});
self.i += 1;
while (self.i < self.dir_path.len and self.dir_path[self.i] != std.fs.path.sep) : (self.i += 1) {}
if (std.fs.accessAbsolute(potential_build_path, .{})) {
// found a build.zig file
return potential_build_path;
} else |_| {
// nope it failed for whatever reason, free it and move the
// machinery forward
self.allocator.free(potential_build_path);
}
}
}
};
2022-10-05 12:23:38 +01:00
/// takes ownership of `uri`
fn createBuildFile(self: *const DocumentStore, uri: Uri) error{OutOfMemory}!BuildFile {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-10-05 12:23:38 +01:00
var build_file = BuildFile{
.uri = uri,
.config = .{
.packages = &.{},
.include_dirs = &.{},
},
};
2022-10-05 12:23:38 +01:00
errdefer build_file.deinit(self.allocator);
2022-10-05 12:23:38 +01:00
if (loadBuildAssociatedConfiguration(self.allocator, build_file)) |config| {
build_file.build_associated_config = config;
if (config.relative_builtin_path) |relative_builtin_path| blk: {
const build_file_path = URI.parse(self.allocator, build_file.uri) catch break :blk;
2022-10-10 18:43:50 +01:00
const absolute_builtin_path = std.fs.path.resolve(self.allocator, &.{ build_file_path, "../", relative_builtin_path }) catch break :blk;
2022-10-05 12:23:38 +01:00
defer self.allocator.free(absolute_builtin_path);
build_file.builtin_uri = try URI.fromPath(self.allocator, absolute_builtin_path);
}
} else |err| {
if (err != error.FileNotFound) {
log.debug("Failed to load config associated with build file {s} (error: {})", .{ build_file.uri, err });
}
}
// TODO: Do this in a separate thread?
// It can take quite long.
if (loadBuildConfiguration(
self.allocator,
build_file,
self.config.*,
self.runtime_zig_version.*.?, // if we have the path to zig we should have the zig version
)) |build_config| {
2022-10-05 12:23:38 +01:00
build_file.config = build_config;
} else |err| {
log.err("Failed to load build configuration for {s} (error: {})", .{ build_file.uri, err });
}
return build_file;
}
fn uriAssociatedWithBuild(
self: *DocumentStore,
2022-10-05 12:23:38 +01:00
build_file: BuildFile,
uri: Uri,
) error{OutOfMemory}!bool {
2022-10-17 19:23:51 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-12-29 23:21:26 +00:00
var checked_uris = std.StringHashMapUnmanaged(void){};
defer checked_uris.deinit(self.allocator);
for (build_file.config.packages) |package| {
2022-10-05 12:23:38 +01:00
const package_uri = try URI.fromPath(self.allocator, package.path);
defer self.allocator.free(package_uri);
if (std.mem.eql(u8, uri, package_uri)) {
return true;
}
2022-12-29 23:21:26 +00:00
if (try self.uriInImports(&checked_uris, build_file, package_uri, uri))
return true;
}
return false;
}
fn uriInImports(
self: *DocumentStore,
2022-12-29 23:21:26 +00:00
checked_uris: *std.StringHashMapUnmanaged(void),
build_file: BuildFile,
2022-10-05 12:23:38 +01:00
source_uri: Uri,
uri: Uri,
) error{OutOfMemory}!bool {
if (checked_uris.contains(source_uri))
return false;
2022-12-29 23:21:26 +00:00
if (isInStd(source_uri)) return false;
// consider it checked even if a failure happens
2022-12-29 23:21:26 +00:00
try checked_uris.put(self.allocator, source_uri, {});
const handle = self.getOrLoadHandle(source_uri) orelse return false;
2022-12-29 23:21:26 +00:00
if (handle.associated_build_file) |associated_build_file_uri| {
return std.mem.eql(u8, associated_build_file_uri, build_file.uri);
}
2022-10-05 12:23:38 +01:00
for (handle.import_uris.items) |import_uri| {
if (std.mem.eql(u8, uri, import_uri))
return true;
2022-12-29 23:21:26 +00:00
if (try self.uriInImports(checked_uris, build_file, import_uri, uri))
return true;
}
return false;
}
2022-12-27 05:52:15 +00:00
/// takes ownership of the text passed in.
2022-10-05 12:23:38 +01:00
fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) error{OutOfMemory}!Handle {
2022-06-06 04:50:17 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-10-05 12:23:38 +01:00
var handle: Handle = blk: {
errdefer self.allocator.free(text);
2022-12-27 05:52:15 +00:00
var duped_uri = try self.allocator.dupe(u8, uri);
errdefer self.allocator.free(duped_uri);
var tree = try Ast.parse(self.allocator, text, .zig);
2022-10-05 12:23:38 +01:00
errdefer tree.deinit(self.allocator);
var nodes = tree.nodes.toMultiArrayList();
try nodes.setCapacity(self.allocator, nodes.len);
tree.nodes = nodes.slice();
var tokens = tree.tokens.toMultiArrayList();
try tokens.setCapacity(self.allocator, tokens.len);
tree.tokens = tokens.slice();
2022-10-05 12:23:38 +01:00
var document_scope = try analysis.makeDocumentScope(self.allocator, tree);
errdefer document_scope.deinit(self.allocator);
try document_scope.scopes.setCapacity(self.allocator, document_scope.scopes.len);
2022-10-05 12:23:38 +01:00
break :blk Handle{
.open = open,
2022-12-27 05:52:15 +00:00
.uri = duped_uri,
2022-10-05 12:23:38 +01:00
.text = text,
.tree = tree,
.document_scope = document_scope,
};
};
errdefer handle.deinit(self.allocator);
2020-05-18 09:37:15 +01:00
2022-09-13 21:12:32 +01:00
defer {
2022-10-05 12:23:38 +01:00
if (handle.associated_build_file) |build_file_uri| {
log.debug("Opened document `{s}` with build file `{s}`", .{ handle.uri, build_file_uri });
2022-12-29 23:21:26 +00:00
} else if (isBuildFile(handle.uri)) {
2022-10-05 12:23:38 +01:00
log.debug("Opened document `{s}` (build file)", .{handle.uri});
2022-09-13 21:12:32 +01:00
} else {
2022-10-05 12:23:38 +01:00
log.debug("Opened document `{s}`", .{handle.uri});
2022-09-13 21:12:32 +01:00
}
}
2022-10-05 12:23:38 +01:00
handle.import_uris = try self.collectImportUris(handle);
handle.cimports = try self.collectCIncludes(handle);
2020-05-24 17:00:21 +01:00
if (!std.process.can_spawn or self.config.zig_exe_path == null) return handle;
if (isBuildFile(handle.uri) and !isInStd(handle.uri)) {
const gop = try self.build_files.getOrPut(self.allocator, uri);
errdefer |err| {
self.build_files.swapRemoveAt(gop.index);
2022-10-05 12:23:38 +01:00
log.debug("Failed to load build file {s}: (error: {})", .{ uri, err });
}
if (!gop.found_existing) {
const duped_uri = try self.allocator.dupe(u8, uri);
gop.value_ptr.* = try self.createBuildFile(duped_uri);
gop.key_ptr.* = gop.value_ptr.uri;
}
} else if (!isBuiltinFile(handle.uri) and !isInStd(handle.uri)) blk: {
2022-12-29 23:21:26 +00:00
// log.debug("Going to walk down the tree towards: {s}", .{uri});
// walk down the tree towards the uri. When we hit build.zig files
// determine if the uri we're interested in is involved with the build.
// This ensures that _relevant_ build.zig files higher in the
// filesystem have precedence.
2022-10-05 12:23:38 +01:00
const path = URI.parse(self.allocator, uri) catch break :blk;
defer self.allocator.free(path);
2022-10-05 12:23:38 +01:00
var build_it = try BuildDotZigIterator.init(self.allocator, path);
while (try build_it.next()) |build_path| {
defer self.allocator.free(build_path);
2022-12-29 23:21:26 +00:00
// log.debug("found build path: {s}", .{build_path});
2021-10-16 04:05:34 +01:00
2022-12-27 05:52:15 +00:00
const build_file_uri = try URI.fromPath(self.allocator, build_path);
const gop = self.build_files.getOrPut(self.allocator, build_file_uri) catch |err| {
self.allocator.free(build_file_uri);
return err;
};
2021-10-16 04:05:34 +01:00
2022-10-05 12:23:38 +01:00
if (!gop.found_existing) {
2022-12-27 05:52:15 +00:00
errdefer self.build_files.swapRemoveAt(gop.index);
2022-10-05 12:23:38 +01:00
gop.value_ptr.* = try self.createBuildFile(build_file_uri);
2022-12-27 05:52:15 +00:00
} else {
self.allocator.free(build_file_uri);
}
2022-10-05 12:23:38 +01:00
if (try self.uriAssociatedWithBuild(gop.value_ptr.*, uri)) {
2022-12-27 05:52:15 +00:00
handle.associated_build_file = gop.key_ptr.*;
2020-05-25 15:24:44 +01:00
break;
2022-12-29 23:21:26 +00:00
} else if (handle.associated_build_file == null) {
2023-01-11 20:18:37 +00:00
handle.associated_build_file = gop.key_ptr.*;
}
2020-05-25 15:24:44 +01:00
}
}
2020-05-24 17:00:21 +01:00
return handle;
}
2022-10-05 12:23:38 +01:00
fn createDocumentFromURI(self: *DocumentStore, uri: Uri, open: bool) error{OutOfMemory}!?Handle {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2021-03-30 13:41:59 +01:00
const file_path = URI.parse(self.allocator, uri) catch return null;
2022-10-05 12:23:38 +01:00
defer self.allocator.free(file_path);
var file = std.fs.openFileAbsolute(file_path, .{}) catch return null;
2022-10-05 12:23:38 +01:00
defer file.close();
2022-08-18 23:00:46 +01:00
const file_contents = file.readToEndAllocOptions(self.allocator, std.math.maxInt(usize), null, @alignOf(u8), 0) catch return null;
2022-10-05 12:23:38 +01:00
return try self.createDocument(uri, file_contents, open);
}
2022-12-27 05:52:15 +00:00
/// Caller owns returned memory.
2022-10-05 12:23:38 +01:00
fn collectImportUris(self: *const DocumentStore, handle: Handle) error{OutOfMemory}!std.ArrayListUnmanaged(Uri) {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-08-23 11:44:26 +01:00
var imports = try analysis.collectImports(self.allocator, handle.tree);
var i: usize = 0;
2022-12-27 05:52:15 +00:00
errdefer {
// only free the uris
for (imports.items[0..i]) |uri| self.allocator.free(uri);
imports.deinit(self.allocator);
}
// Convert to URIs
2022-08-18 23:00:46 +01:00
while (i < imports.items.len) {
2022-10-05 12:23:38 +01:00
const maybe_uri = try self.uriFromImportStr(self.allocator, handle, imports.items[i]);
if (maybe_uri) |uri| {
// The raw import strings are owned by the document and do not need to be freed here.
2022-08-18 23:00:46 +01:00
imports.items[i] = uri;
i += 1;
} else {
2022-08-18 23:00:46 +01:00
_ = imports.swapRemove(i);
}
}
2022-10-05 12:23:38 +01:00
return imports;
2022-08-18 23:00:46 +01:00
}
2022-10-05 12:23:38 +01:00
pub const CImportHandle = struct {
/// the `@cImport` node
2022-08-18 23:00:46 +01:00
node: Ast.Node.Index,
/// hash of c source file
2022-10-05 12:23:38 +01:00
hash: Hash,
2022-08-18 23:00:46 +01:00
/// c source file
source: []const u8,
};
/// Collects all `@cImport` nodes and converts them into c source code
/// Caller owns returned memory.
2022-10-05 12:23:38 +01:00
fn collectCIncludes(self: *const DocumentStore, handle: Handle) error{OutOfMemory}!std.MultiArrayList(CImportHandle) {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-08-18 23:00:46 +01:00
var cimport_nodes = try analysis.collectCImportNodes(self.allocator, handle.tree);
defer self.allocator.free(cimport_nodes);
2022-10-05 12:23:38 +01:00
var sources = std.MultiArrayList(CImportHandle){};
try sources.ensureTotalCapacity(self.allocator, cimport_nodes.len);
2022-08-18 23:00:46 +01:00
errdefer {
2022-10-05 12:23:38 +01:00
for (sources.items(.source)) |source| {
self.allocator.free(source);
}
2022-08-18 23:00:46 +01:00
sources.deinit(self.allocator);
}
for (cimport_nodes) |node| {
const c_source = translate_c.convertCInclude(self.allocator, handle.tree, node) catch |err| switch (err) {
error.Unsupported => continue,
error.OutOfMemory => return error.OutOfMemory,
};
sources.appendAssumeCapacity(.{
.node = node,
2022-10-05 12:23:38 +01:00
.hash = computeHash(c_source),
2022-08-18 23:00:46 +01:00
.source = c_source,
});
}
2022-08-18 23:00:46 +01:00
2022-10-05 12:23:38 +01:00
return sources;
2022-08-18 23:00:46 +01:00
}
2022-10-10 19:01:54 +01:00
/// collects every file uri the given handle depends on
/// includes imports, cimports & packages
2022-10-05 12:23:38 +01:00
pub fn collectDependencies(
store: *const DocumentStore,
allocator: std.mem.Allocator,
2022-10-05 12:23:38 +01:00
handle: Handle,
dependencies: *std.ArrayListUnmanaged(Uri),
) error{OutOfMemory}!void {
2022-10-17 19:23:51 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
try dependencies.ensureUnusedCapacity(allocator, handle.import_uris.items.len + handle.cimports.len);
2022-10-05 12:23:38 +01:00
for (handle.import_uris.items) |uri| {
dependencies.appendAssumeCapacity(try allocator.dupe(u8, uri));
}
for (handle.cimports.items(.hash)) |hash| {
const result = store.cimports.get(hash) orelse continue;
switch (result) {
2022-10-05 12:23:38 +01:00
.success => |uri| dependencies.appendAssumeCapacity(try allocator.dupe(u8, uri)),
.failure => continue,
2021-03-30 13:41:59 +01:00
}
}
2022-10-05 12:23:38 +01:00
if (handle.associated_build_file) |build_file_uri| {
if (store.build_files.get(build_file_uri)) |build_file| {
const packages = build_file.config.packages;
try dependencies.ensureUnusedCapacity(allocator, packages.len);
for (packages) |pkg| {
dependencies.appendAssumeCapacity(try URI.fromPath(allocator, pkg.path));
2022-08-18 23:00:46 +01:00
}
}
}
}
/// TODO resolve relative paths
pub fn collectIncludeDirs(
store: *const DocumentStore,
allocator: std.mem.Allocator,
handle: Handle,
include_dirs: *std.ArrayListUnmanaged([]const u8),
) !void {
const target_info = try std.zig.system.NativeTargetInfo.detect(.{});
var native_paths = try std.zig.system.NativePaths.detect(allocator, target_info);
defer native_paths.deinit();
const build_file_includes_paths: []const []const u8 = if (handle.associated_build_file) |build_file_uri|
store.build_files.get(build_file_uri).?.config.include_dirs
else
&.{};
try include_dirs.ensureTotalCapacity(allocator, native_paths.include_dirs.items.len + build_file_includes_paths.len);
const native_include_dirs = try native_paths.include_dirs.toOwnedSlice();
defer allocator.free(native_include_dirs);
include_dirs.appendSliceAssumeCapacity(native_include_dirs);
for (build_file_includes_paths) |include_path| {
include_dirs.appendAssumeCapacity(try allocator.dupe(u8, include_path));
}
}
2022-10-10 19:01:54 +01:00
/// returns the document behind `@cImport()` where `node` is the `cImport` node
2022-10-28 05:38:36 +01:00
/// if a cImport can't be translated e.g. requires computing a
/// comptime value `resolveCImport` will return null
/// returned memory is owned by DocumentStore
pub fn resolveCImport(self: *DocumentStore, handle: Handle, node: Ast.Node.Index) error{OutOfMemory}!?Uri {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (!std.process.can_spawn) return null;
const index = std.mem.indexOfScalar(Ast.Node.Index, handle.cimports.items(.node), node) orelse return null;
2022-10-05 12:23:38 +01:00
const hash: Hash = handle.cimports.items(.hash)[index];
2022-10-28 05:38:36 +01:00
// TODO regenerate cimports if config changes or the header files gets modified
const result = self.cimports.get(hash) orelse blk: {
const source: []const u8 = handle.cimports.items(.source)[index];
var include_dirs: std.ArrayListUnmanaged([]const u8) = .{};
defer {
for (include_dirs.items) |path| {
self.allocator.free(path);
}
include_dirs.deinit(self.allocator);
}
self.collectIncludeDirs(self.allocator, handle, &include_dirs) catch |err| {
log.err("failed to resolve include paths: {}", .{err});
return null;
};
2022-10-28 05:38:36 +01:00
var result = (try translate_c.translate(
self.allocator,
self.config.*,
include_dirs.items,
2022-10-28 05:38:36 +01:00
source,
)) orelse return null;
self.cimports.putNoClobber(self.allocator, hash, result) catch result.deinit(self.allocator);
switch (result) {
.success => |uri| log.debug("Translated cImport into {s}", .{uri}),
.failure => {},
}
break :blk result;
};
2022-10-05 12:23:38 +01:00
switch (result) {
.success => |uri| return uri,
.failure => return null,
}
}
2022-10-05 12:23:38 +01:00
/// takes the string inside a @import() node (without the quotation marks)
/// and returns it's uri
/// caller owns the returned memory
pub fn uriFromImportStr(self: *const DocumentStore, allocator: std.mem.Allocator, handle: Handle, import_str: []const u8) error{OutOfMemory}!?Uri {
if (std.mem.eql(u8, import_str, "std")) {
const zig_lib_path = self.config.zig_lib_path orelse return null;
2022-10-05 12:23:38 +01:00
const std_path = std.fs.path.resolve(allocator, &[_][]const u8{ zig_lib_path, "./std/std.zig" }) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return null,
};
defer allocator.free(std_path);
return try URI.fromPath(allocator, std_path);
} else if (std.mem.eql(u8, import_str, "builtin")) {
2022-10-10 18:22:54 +01:00
if (handle.associated_build_file) |build_file_uri| {
const build_file = self.build_files.get(build_file_uri).?;
if (build_file.builtin_uri) |builtin_uri| {
return try allocator.dupe(u8, builtin_uri);
}
}
if (self.config.builtin_path) |_| {
return try URI.fromPath(allocator, self.config.builtin_path.?);
}
return null;
} else if (!std.mem.endsWith(u8, import_str, ".zig")) {
2022-10-05 12:23:38 +01:00
if (handle.associated_build_file) |build_file_uri| {
const build_file = self.build_files.get(build_file_uri).?;
for (build_file.config.packages) |pkg| {
2020-05-25 17:33:08 +01:00
if (std.mem.eql(u8, import_str, pkg.name)) {
2022-10-05 12:23:38 +01:00
return try URI.fromPath(allocator, pkg.path);
2020-05-25 17:33:08 +01:00
}
}
}
return null;
} else {
var seperator_index = handle.uri.len;
while (seperator_index > 0) : (seperator_index -= 1) {
if (std.fs.path.isSep(handle.uri[seperator_index - 1])) break;
2022-10-05 12:23:38 +01:00
}
const base = handle.uri[0 .. seperator_index - 1];
2020-05-14 02:54:05 +01:00
return URI.pathRelative(allocator, base, import_str) catch |err| switch (err) {
2022-10-05 12:23:38 +01:00
error.OutOfMemory => return error.OutOfMemory,
error.UriBadScheme => return null,
};
}
}
2023-01-03 15:21:58 +00:00
fn tagStoreCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handle: Handle, comptime name: []const u8) error{OutOfMemory}![]types.CompletionItem {
2022-10-05 12:23:38 +01:00
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
2022-10-17 19:20:34 +01:00
var dependencies = std.ArrayListUnmanaged(Uri){};
try dependencies.append(arena, handle.uri);
try self.collectDependencies(arena, handle, &dependencies);
2020-07-08 02:05:44 +01:00
// TODO Better solution for deciding what tags to include
var result_set = analysis.CompletionSet{};
2022-10-05 12:23:38 +01:00
2022-10-17 19:25:51 +01:00
for (dependencies.items) |uri| {
2022-10-17 19:20:34 +01:00
// not every dependency is loaded which results in incomplete completion
const hdl = self.handles.get(uri) orelse continue;
const curr_set = @field(hdl.document_scope, name);
for (curr_set.entries.items(.key)) |completion| {
2022-10-05 12:23:38 +01:00
try result_set.put(arena, completion, {});
}
}
2022-08-18 23:00:46 +01:00
return result_set.entries.items(.key);
2020-07-08 02:05:44 +01:00
}
2023-01-03 15:21:58 +00:00
pub fn errorCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handle: Handle) error{OutOfMemory}![]types.CompletionItem {
2022-10-05 12:23:38 +01:00
return try self.tagStoreCompletionItems(arena, handle, "error_completions");
2020-07-08 02:05:44 +01:00
}
2023-01-03 15:21:58 +00:00
pub fn enumCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handle: Handle) error{OutOfMemory}![]types.CompletionItem {
2022-10-05 12:23:38 +01:00
return try self.tagStoreCompletionItems(arena, handle, "enum_completions");
}
2023-02-08 20:01:15 +00:00
pub fn ensureInterpreterExists(self: *DocumentStore, uri: Uri) !*ComptimeInterpreter {
var handle = self.handles.get(uri).?;
2023-02-08 20:01:15 +00:00
if (handle.interpreter != null) return handle.interpreter.?;
{
var interpreter = try self.allocator.create(ComptimeInterpreter);
errdefer self.allocator.destroy(interpreter);
var ip = try ComptimeInterpreter.InternPool.init(self.allocator);
errdefer ip.deinit(self.allocator);
interpreter.* = ComptimeInterpreter{
.allocator = self.allocator,
2023-02-08 20:01:15 +00:00
.ip = ip,
.document_store = self,
2022-11-11 01:51:02 +00:00
.uri = uri,
};
2023-02-08 20:01:15 +00:00
handle.interpreter = interpreter;
}
2023-02-08 20:01:15 +00:00
_ = try handle.interpreter.?.interpret(0, .none, .{});
return handle.interpreter.?;
}