Skip to content

various wasi related improvements #2328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 46 additions & 22 deletions src/DocumentStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ config: Config,
lock: std.Thread.RwLock = .{},
thread_pool: if (builtin.single_threaded) void else *std.Thread.Pool,
handles: std.StringArrayHashMapUnmanaged(*Handle) = .empty,
build_files: std.StringArrayHashMapUnmanaged(*BuildFile) = .empty,
cimports: std.AutoArrayHashMapUnmanaged(Hash, translate_c.Result) = .empty,
build_files: if (supports_build_system) std.StringArrayHashMapUnmanaged(*BuildFile) else void = if (supports_build_system) .empty else {},
cimports: if (supports_build_system) std.AutoArrayHashMapUnmanaged(Hash, translate_c.Result) else void = if (supports_build_system) .empty else {},
diagnostics_collection: *DiagnosticsCollection,
builds_in_progress: std.atomic.Value(i32) = .init(0),
transport: ?lsp.AnyTransport = null,
Expand All @@ -41,6 +41,8 @@ pub const Hash = [Hasher.mac_length]u8;

pub const max_document_size = std.math.maxInt(u32);

pub const supports_build_system = std.process.can_spawn;

pub fn computeHash(bytes: []const u8) Hash {
var hasher: Hasher = .init(&@splat(0));
hasher.update(bytes);
Expand Down Expand Up @@ -316,6 +318,7 @@ pub const Handle = struct {
/// `DocumentStore.build_files` is guaranteed to contain this Uri.
/// Uri memory managed by its build_file
pub fn getAssociatedBuildFileUri(self: *Handle, document_store: *DocumentStore) error{OutOfMemory}!?Uri {
comptime std.debug.assert(supports_build_system);
switch (try self.getAssociatedBuildFileUri2(document_store)) {
.none,
.unresolved,
Expand All @@ -336,6 +339,8 @@ pub const Handle = struct {
/// The associated build file (build.zig) has been successfully resolved.
resolved: *BuildFile,
} {
comptime std.debug.assert(supports_build_system);

self.impl.lock.lock();
defer self.impl.lock.unlock();

Expand Down Expand Up @@ -634,16 +639,19 @@ pub fn deinit(self: *DocumentStore) void {
}
self.handles.deinit(self.allocator);

for (self.build_files.values()) |build_file| {
build_file.deinit(self.allocator);
self.allocator.destroy(build_file);
}
self.build_files.deinit(self.allocator);
if (supports_build_system) {
for (self.build_files.values()) |build_file| {
build_file.deinit(self.allocator);
self.allocator.destroy(build_file);
}
self.build_files.deinit(self.allocator);

for (self.cimports.values()) |*result| {
result.deinit(self.allocator);
for (self.cimports.values()) |*result| {
result.deinit(self.allocator);
}
self.cimports.deinit(self.allocator);
}
self.cimports.deinit(self.allocator);

self.* = undefined;
}

Expand Down Expand Up @@ -718,6 +726,7 @@ pub fn getOrLoadHandle(self: *DocumentStore, uri: Uri) ?*Handle {
/// **Thread safe** takes a shared lock
/// This function does not protect against data races from modifying the BuildFile
pub fn getBuildFile(self: *DocumentStore, uri: Uri) ?*BuildFile {
comptime std.debug.assert(supports_build_system);
self.lock.lockShared();
defer self.lock.unlockShared();
return self.build_files.get(uri);
Expand All @@ -727,6 +736,8 @@ pub fn getBuildFile(self: *DocumentStore, uri: Uri) ?*BuildFile {
/// **Thread safe** takes an exclusive lock
/// This function does not protect against data races from modifying the BuildFile
fn getOrLoadBuildFile(self: *DocumentStore, uri: Uri) ?*BuildFile {
comptime std.debug.assert(supports_build_system);

if (self.getBuildFile(uri)) |build_file| return build_file;

const new_build_file: *BuildFile = blk: {
Expand Down Expand Up @@ -754,9 +765,7 @@ fn getOrLoadBuildFile(self: *DocumentStore, uri: Uri) ?*BuildFile {

// this code path is only reached when the build file is new

if (std.process.can_spawn) {
self.invalidateBuildFile(new_build_file.uri);
}
self.invalidateBuildFile(new_build_file.uri);

return new_build_file;
}
Expand Down Expand Up @@ -817,8 +826,10 @@ pub fn closeDocument(self: *DocumentStore, uri: Uri) void {
defer self.lock.unlock();

self.garbageCollectionImports() catch {};
self.garbageCollectionCImports() catch {};
self.garbageCollectionBuildFiles() catch {};
if (supports_build_system) {
self.garbageCollectionCImports() catch {};
self.garbageCollectionBuildFiles() catch {};
}
}

/// Takes ownership of `new_text` which has to be allocated with this DocumentStore's allocator.
Expand Down Expand Up @@ -864,7 +875,7 @@ pub fn refreshDocumentFromFileSystem(self: *DocumentStore, uri: Uri) !bool {
/// Invalidates a build files.
/// **Thread safe** takes a shared lock
pub fn invalidateBuildFile(self: *DocumentStore, build_file_uri: Uri) void {
comptime std.debug.assert(std.process.can_spawn);
comptime std.debug.assert(supports_build_system);

if (self.config.zig_exe_path == null) return;
if (self.config.build_runner_path == null) return;
Expand Down Expand Up @@ -1464,7 +1475,9 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]const u8, open: bool

_ = handle.setOpen(open);

if (isBuildFile(handle.uri) and !isInStd(handle.uri)) {
if (!supports_build_system) {
// nothing to do
} else if (isBuildFile(handle.uri) and !isInStd(handle.uri)) {
_ = self.getOrLoadBuildFile(handle.uri);
} else if (!isBuiltinFile(handle.uri) and !isInStd(handle.uri)) blk: {
const potential_build_files = self.collectPotentialBuildFiles(uri) catch {
Expand Down Expand Up @@ -1614,6 +1627,8 @@ fn collectDependenciesInternal(
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

if (!supports_build_system) return;

{
if (lock) store.lock.lockShared();
defer if (lock) store.lock.unlockShared();
Expand Down Expand Up @@ -1656,6 +1671,8 @@ pub fn collectIncludeDirs(
handle: *Handle,
include_dirs: *std.ArrayListUnmanaged([]const u8),
) !bool {
comptime std.debug.assert(supports_build_system);

var arena_allocator: std.heap.ArenaAllocator = .init(allocator);
defer arena_allocator.deinit();

Expand Down Expand Up @@ -1695,6 +1712,8 @@ pub fn collectCMacros(
handle: *Handle,
c_macros: *std.ArrayListUnmanaged([]const u8),
) !bool {
comptime std.debug.assert(supports_build_system);

const collected_all = switch (try handle.getAssociatedBuildFileUri2(store)) {
.none => true,
.unresolved => false,
Expand All @@ -1719,10 +1738,11 @@ pub fn collectCMacros(
/// returned memory is owned by DocumentStore
/// **Thread safe** takes an exclusive lock
pub fn resolveCImport(self: *DocumentStore, handle: *Handle, node: Ast.Node.Index) error{OutOfMemory}!?Uri {
comptime std.debug.assert(supports_build_system);

const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

if (!std.process.can_spawn) return null;
if (self.config.zig_exe_path == null) return null;
if (self.config.zig_lib_dir == null) return null;
if (self.config.global_cache_dir == null) return null;
Expand Down Expand Up @@ -1891,17 +1911,21 @@ pub fn uriFromImportStr(self: *DocumentStore, allocator: std.mem.Allocator, hand

return try URI.fromPath(allocator, std_path);
} else if (std.mem.eql(u8, import_str, "builtin")) {
if (try handle.getAssociatedBuildFileUri(self)) |build_file_uri| {
const build_file = self.getBuildFile(build_file_uri).?;
if (build_file.builtin_uri) |builtin_uri| {
return try allocator.dupe(u8, builtin_uri);
if (supports_build_system) {
if (try handle.getAssociatedBuildFileUri(self)) |build_file_uri| {
const build_file = self.getBuildFile(build_file_uri).?;
if (build_file.builtin_uri) |builtin_uri| {
return try allocator.dupe(u8, builtin_uri);
}
}
}
if (self.config.builtin_path) |builtin_path| {
return try URI.fromPath(allocator, builtin_path);
}
return null;
} else if (!std.mem.endsWith(u8, import_str, ".zig")) {
if (!supports_build_system) return null;

if (try handle.getAssociatedBuildFileUri(self)) |build_file_uri| blk: {
const build_file = self.getBuildFile(build_file_uri).?;
const build_config = build_file.tryLockConfig() orelse break :blk;
Expand Down
20 changes: 8 additions & 12 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ fn initializeHandler(server: *Server, arena: std.mem.Allocator, request: types.I
}
}

// TODO Instead of checking `is_test`, possible config paths should be provided by the main function.
if (!zig_builtin.is_test) {
var maybe_config_result = if (server.config_path) |config_path|
configuration.loadFromFile(server.allocator, config_path)
Expand Down Expand Up @@ -877,9 +878,12 @@ fn removeWorkspace(server: *Server, uri: types.URI) void {
fn didChangeWatchedFilesHandler(server: *Server, arena: std.mem.Allocator, notification: types.DidChangeWatchedFilesParams) Error!void {
var updated_files: usize = 0;
for (notification.changes) |change| {
const file_path = Uri.parse(arena, change.uri) catch |err| {
log.err("failed to parse URI '{s}': {}", .{ change.uri, err });
continue;
const file_path = Uri.parse(arena, change.uri) catch |err| switch (err) {
error.UnsupportedScheme => continue,
else => {
log.err("failed to parse URI '{s}': {}", .{ change.uri, err });
continue;
},
};
const file_extension = std.fs.path.extension(file_path);
if (!std.mem.eql(u8, file_extension, ".zig") and !std.mem.eql(u8, file_extension, ".zon")) continue;
Expand Down Expand Up @@ -1079,7 +1083,7 @@ pub fn updateConfiguration(
}
}

if (new_zig_exe_path or new_zig_lib_path) {
if (DocumentStore.supports_build_system and (new_zig_exe_path or new_zig_lib_path)) {
for (server.document_store.cimports.values()) |*result| {
result.deinit(server.document_store.allocator);
}
Expand Down Expand Up @@ -1148,14 +1152,6 @@ pub fn updateConfiguration(
}
}

if (server.config.prefer_ast_check_as_child_process) {
if (!std.process.can_spawn) {
log.info("'prefer_ast_check_as_child_process' is ignored because the '{s}' operating system can't spawn child processes", .{@tagName(zig_builtin.target.os.tag)});
} else if (server.status == .initialized and server.config.zig_exe_path == null) {
log.warn("'prefer_ast_check_as_child_process' is ignored because Zig could not be found", .{});
}
}

if (server.config.enable_build_on_save orelse false) {
if (!BuildOnSaveSupport.isSupportedComptime()) {
// This message is not very helpful but it relatively uncommon to happen anyway.
Expand Down
1 change: 1 addition & 0 deletions src/analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,7 @@ fn resolveTypeOfNodeUncached(analyser: *Analyser, node_handle: NodeWithHandle) e
}

if (std.mem.eql(u8, call_name, "@cImport")) {
if (!DocumentStore.supports_build_system) return null;
const cimport_uri = (try analyser.store.resolveCImport(handle, node)) orelse return null;

const new_handle = analyser.store.getOrLoadHandle(cimport_uri) orelse return null;
Expand Down
6 changes: 4 additions & 2 deletions src/configuration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ const Config = @import("Config.zig");

const logger = std.log.scoped(.config);

pub fn getLocalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 {
fn getLocalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 {
const folder_path = try known_folders.getPath(allocator, .local_configuration) orelse return null;
defer allocator.free(folder_path);
return try std.fs.path.join(allocator, &.{ folder_path, "zls.json" });
}

pub fn getGlobalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 {
fn getGlobalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 {
const folder_path = try known_folders.getPath(allocator, .global_configuration) orelse return null;
defer allocator.free(folder_path);
return try std.fs.path.join(allocator, &.{ folder_path, "zls.json" });
}

pub fn load(allocator: std.mem.Allocator) error{OutOfMemory}!LoadConfigResult {
if (builtin.target.os.tag == .wasi) return .not_found;

const local_config_path = getLocalConfigPath(allocator) catch |err| blk: {
logger.warn("failed to resolve local configuration path: {}", .{err});
break :blk null;
Expand Down
55 changes: 30 additions & 25 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ fn completeFileSystemStringLiteral(builder: *Builder, pos_context: Analyser.Posi
if (std.fs.path.isAbsolute(completing) and pos_context != .import_string_literal) {
try search_paths.append(builder.arena, completing);
} else if (pos_context == .cinclude_string_literal) {
if (!DocumentStore.supports_build_system) return;
_ = store.collectIncludeDirs(builder.arena, builder.orig_handle, &search_paths) catch |err| {
log.err("failed to resolve include paths: {}", .{err});
return;
Expand Down Expand Up @@ -804,31 +805,35 @@ fn completeFileSystemStringLiteral(builder: *Builder, pos_context: Analyser.Posi
}

if (completing.len == 0 and pos_context == .import_string_literal) {
if (try builder.orig_handle.getAssociatedBuildFileUri(store)) |uri| blk: {
const build_file = store.getBuildFile(uri).?;
const build_config = build_file.tryLockConfig() orelse break :blk;
defer build_file.unlockConfig();

try completions.ensureUnusedCapacity(builder.arena, build_config.packages.len);
for (build_config.packages) |pkg| {
completions.putAssumeCapacity(.{
.label = pkg.name,
.kind = .Module,
.detail = pkg.path,
}, {});
}
} else if (DocumentStore.isBuildFile(builder.orig_handle.uri)) blk: {
const build_file = store.getBuildFile(builder.orig_handle.uri) orelse break :blk;
const build_config = build_file.tryLockConfig() orelse break :blk;
defer build_file.unlockConfig();

try completions.ensureUnusedCapacity(builder.arena, build_config.deps_build_roots.len);
for (build_config.deps_build_roots) |dbr| {
completions.putAssumeCapacity(.{
.label = dbr.name,
.kind = .Module,
.detail = dbr.path,
}, {});
no_modules: {
if (!DocumentStore.supports_build_system) break :no_modules;

if (try builder.orig_handle.getAssociatedBuildFileUri(store)) |uri| {
const build_file = store.getBuildFile(uri).?;
const build_config = build_file.tryLockConfig() orelse break :no_modules;
defer build_file.unlockConfig();

try completions.ensureUnusedCapacity(builder.arena, build_config.packages.len);
for (build_config.packages) |pkg| {
completions.putAssumeCapacity(.{
.label = pkg.name,
.kind = .Module,
.detail = pkg.path,
}, {});
}
} else if (DocumentStore.isBuildFile(builder.orig_handle.uri)) {
const build_file = store.getBuildFile(builder.orig_handle.uri) orelse break :no_modules;
const build_config = build_file.tryLockConfig() orelse break :no_modules;
defer build_file.unlockConfig();

try completions.ensureUnusedCapacity(builder.arena, build_config.deps_build_roots.len);
for (build_config.deps_build_roots) |dbr| {
completions.putAssumeCapacity(.{
.label = dbr.name,
.kind = .Module,
.detail = dbr.path,
}, {});
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/features/goto.zig
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ fn gotoDefinitionBuiltin(
const name_loc = offsets.tokenIndexToLoc(handle.tree.source, loc.start);
const name = offsets.locToSlice(handle.tree.source, name_loc);
if (std.mem.eql(u8, name, "@cImport")) {
if (!DocumentStore.supports_build_system) return null;

const tree = handle.tree;
const index = for (handle.cimports.items(.node), 0..) |cimport_node, index| {
const main_token = tree.nodeMainToken(cimport_node);
Expand Down Expand Up @@ -205,6 +207,8 @@ fn gotoDefinitionString(
.cinclude_string_literal => try URI.fromPath(
arena,
blk: {
if (!DocumentStore.supports_build_system) return null;

if (std.fs.path.isAbsolute(import_str)) break :blk import_str;
var include_dirs: std.ArrayListUnmanaged([]const u8) = .empty;
_ = document_store.collectIncludeDirs(arena, handle, &include_dirs) catch |err| {
Expand Down
3 changes: 2 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ fn logFn(
}

fn defaultLogFilePath(allocator: std.mem.Allocator) std.mem.Allocator.Error!?[]const u8 {
if (zig_builtin.target.os.tag == .wasi) return null;
const cache_path = known_folders.getPath(allocator, .cache) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseError => return null,
Expand Down Expand Up @@ -282,7 +283,7 @@ fn parseArgs(allocator: std.mem.Allocator) ParseArgsError!ParseArgsResult {
}
}

if (std.io.getStdIn().isTty()) {
if (zig_builtin.target.os.tag != .wasi and std.io.getStdIn().isTty()) {
log.warn("ZLS is not a CLI tool, it communicates over the Language Server Protocol.", .{});
log.warn("Did you mean to run 'zls --help'?", .{});
log.warn("", .{});
Expand Down
4 changes: 2 additions & 2 deletions src/uri.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ test fromPath {

/// Parses a Uri and returns the unescaped path
/// Caller owns the returned memory
pub fn parse(allocator: std.mem.Allocator, str: []const u8) (std.Uri.ParseError || error{OutOfMemory})![]u8 {
pub fn parse(allocator: std.mem.Allocator, str: []const u8) (std.Uri.ParseError || error{ UnsupportedScheme, OutOfMemory })![]u8 {
var uri = try std.Uri.parse(str);
if (!std.mem.eql(u8, uri.scheme, "file")) return error.InvalidFormat;
if (!std.mem.eql(u8, uri.scheme, "file")) return error.UnsupportedScheme;
if (builtin.os.tag == .windows and uri.path.percent_encoded.len != 0 and uri.path.percent_encoded[0] == '/') {
uri.path.percent_encoded = uri.path.percent_encoded[1..];
}
Expand Down