diff --git a/README.md b/README.md index 0f719224d..a9042a8bf 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,6 @@ The following options are currently available. | `dangerous_comptime_experiments_do_not_enable` | `bool` | `false` | Whether to use the comptime interpreter | | `skip_std_references` | `bool` | `false` | When true, skips searching for references in std. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is | | `prefer_ast_check_as_child_process` | `bool` | `true` | Favor using `zig ast-check` instead of ZLS's fork | -| `record_session` | `bool` | `false` | When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay` | -| `record_session_path` | `?[]const u8` | `null` | Output file path when `record_session` is set. The recommended file extension *.zlsreplay | -| `replay_session_path` | `?[]const u8` | `null` | Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path. | | `builtin_path` | `?[]const u8` | `null` | Path to 'builtin;' useful for debugging, automatically set if let null | | `zig_lib_path` | `?[]const u8` | `null` | Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports | | `zig_exe_path` | `?[]const u8` | `null` | Zig executable path, e.g. `/path/to/zig/zig`, used to run the custom build runner. If `null`, zig is looked up in `PATH`. Will be used to infer the zig standard library path if none is provided | diff --git a/build.zig b/build.zig index e0046ad99..6f7ecf8d4 100644 --- a/build.zig +++ b/build.zig @@ -10,7 +10,7 @@ const min_zig_string = "0.12.0-dev.3071+6f7354a04"; const Build = blk: { const current_zig = builtin.zig_version; const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; - const is_current_zig_tagged_release = current_zig.pre == null; + const is_current_zig_tagged_release = current_zig.pre == null and current_zig.build == null; if (current_zig.order(min_zig) == .lt) { const message = std.fmt.comptimePrint( \\Your Zig version does not meet the minimum build requirement: @@ -20,6 +20,7 @@ const Build = blk: { \\ ++ if (is_current_zig_tagged_release) \\Please download or compile a tagged release of ZLS. + \\ -> https://github.com/zigtools/zls/releases \\ -> https://github.com/zigtools/zls/releases/tag/{[current_version]} else \\You can take one of the following actions to resolve this issue: @@ -344,3 +345,10 @@ fn getTracyModule( return tracy_module; } + +comptime { + const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; + const min_zig_simple = std.SemanticVersion{ .major = min_zig.major, .minor = min_zig.minor, .patch = 0 }; + const zls_version_simple = std.SemanticVersion{ .major = zls_version.major, .minor = zls_version.minor, .patch = 0 }; + std.debug.assert(zls_version_simple.order(min_zig_simple) == .eq); +} diff --git a/schema.json b/schema.json index a152c0a14..01c7e2d43 100644 --- a/schema.json +++ b/schema.json @@ -104,21 +104,6 @@ "type": "boolean", "default": true }, - "record_session": { - "description": "When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay`", - "type": "boolean", - "default": false - }, - "record_session_path": { - "description": "Output file path when `record_session` is set. The recommended file extension *.zlsreplay", - "type": "string", - "default": null - }, - "replay_session_path": { - "description": "Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path.", - "type": "string", - "default": null - }, "builtin_path": { "description": "Path to 'builtin;' useful for debugging, automatically set if let null", "type": "string", diff --git a/src/Config.zig b/src/Config.zig index 3190e6cc1..73d4a1ef5 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -65,15 +65,6 @@ skip_std_references: bool = false, /// Favor using `zig ast-check` instead of ZLS's fork prefer_ast_check_as_child_process: bool = true, -/// When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay` -record_session: bool = false, - -/// Output file path when `record_session` is set. The recommended file extension *.zlsreplay -record_session_path: ?[]const u8 = null, - -/// Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path. -replay_session_path: ?[]const u8 = null, - /// Path to 'builtin;' useful for debugging, automatically set if let null builtin_path: ?[]const u8 = null, diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 225239e95..c76e6e8ba 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -9,7 +9,6 @@ const BuildAssociatedConfig = @import("BuildAssociatedConfig.zig"); const BuildConfig = @import("build_runner/BuildConfig.zig"); const tracy = @import("tracy"); const Config = @import("Config.zig"); -const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); const translate_c = @import("translate_c.zig"); const ComptimeInterpreter = @import("ComptimeInterpreter.zig"); const AstGen = std.zig.AstGen; @@ -599,8 +598,6 @@ pub const ErrorMessage = struct { allocator: std.mem.Allocator, /// the DocumentStore assumes that `config` is not modified while calling one of its functions. config: *const Config, -/// the DocumentStore assumes that `runtime_zig_version` is not modified while calling one of its functions. -runtime_zig_version: *const ?ZigVersionWrapper, lock: std.Thread.RwLock = .{}, thread_pool: if (builtin.single_threaded) void else *std.Thread.Pool, handles: std.StringArrayHashMapUnmanaged(*Handle) = .{}, @@ -772,8 +769,7 @@ pub fn refreshDocument(self: *DocumentStore, uri: Uri, new_text: [:0]const u8) ! /// Invalidates a build files. /// **Thread safe** takes a shared lock pub fn invalidateBuildFile(self: *DocumentStore, build_file_uri: Uri) error{OutOfMemory}!void { - std.debug.assert(std.process.can_spawn); - if (!std.process.can_spawn) return; + comptime std.debug.assert(std.process.can_spawn); if (self.config.zig_exe_path == null) return; if (self.config.build_runner_path == null) return; diff --git a/src/Server.zig b/src/Server.zig index 6a51ec84c..105ba35c3 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -15,7 +15,6 @@ const tracy = @import("tracy"); const diff = @import("diff.zig"); const ComptimeInterpreter = @import("ComptimeInterpreter.zig"); const InternPool = @import("analyser/analyser.zig").InternPool; -const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); const Transport = @import("Transport.zig"); const known_folders = @import("known-folders"); const BuildRunnerVersion = @import("build_runner/BuildRunnerVersion.zig").BuildRunnerVersion; @@ -39,6 +38,8 @@ const log = std.log.scoped(.zls_server); allocator: std.mem.Allocator, // use updateConfiguration or updateConfiguration2 for setting config options config: Config = .{}, +/// will default to lookup in the system and user configuration folder provided by known-folders. +config_path: ?[]const u8 = null, document_store: DocumentStore, transport: ?*Transport = null, offset_encoding: offsets.Encoding = .@"utf-16", @@ -53,9 +54,6 @@ ip: InternPool = .{}, zig_exe_lock: std.Thread.Mutex = .{}, config_arena: std.heap.ArenaAllocator.State = .{}, client_capabilities: ClientCapabilities = .{}, -runtime_zig_version: ?ZigVersionWrapper = null, -recording_enabled: bool = false, -replay_enabled: bool = false, // Code was based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig @@ -522,43 +520,34 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi server.status = .initializing; - server.updateConfiguration(.{}) catch |err| { - log.err("failed to load configuration: {}", .{err}); - }; - - if (server.recording_enabled) { - server.showMessage(.Info, - \\This zls session is being recorded to {s}. - , .{server.config.record_session_path.?}); - } - - if (server.runtime_zig_version) |zig_version_wrapper| { - const zig_version = zig_version_wrapper.version; - - const zig_version_simple = std.SemanticVersion{ - .major = zig_version.major, - .minor = zig_version.minor, - .patch = 0, - }; - const zls_version_simple = std.SemanticVersion{ - .major = build_options.version.major, - .minor = build_options.version.minor, - .patch = 0, - }; - - switch (zig_version_simple.order(zls_version_simple)) { - .lt => { - server.showMessage(.Warning, - \\Zig `{s}` is older than ZLS `{s}`. Update Zig to avoid unexpected behavior. - , .{ zig_version_wrapper.raw_string, build_options.version_string }); - }, - .eq => {}, - .gt => { - server.showMessage(.Warning, - \\Zig `{s}` is newer than ZLS `{s}`. Update ZLS to avoid unexpected behavior. - , .{ zig_version_wrapper.raw_string, build_options.version_string }); - }, + if (!zig_builtin.is_test) { + var maybe_config_result = if (server.config_path) |config_path| + configuration.loadFromFile(server.allocator, config_path) + else + configuration.load(server.allocator); + + if (maybe_config_result) |*config_result| { + defer config_result.deinit(server.allocator); + switch (config_result.*) { + .success => |config_with_path| try server.updateConfiguration2(config_with_path.config.value), + .failure => |payload| blk: { + try server.updateConfiguration(.{}); + const message = try payload.toMessage(server.allocator) orelse break :blk; + defer server.allocator.free(message); + server.showMessage(.Error, "Failed to load configuration options:\n{s}", .{message}); + }, + .not_found => { + log.info("No config file zls.json found. This is not an error.", .{}); + try server.updateConfiguration(.{}); + }, + } + } else |err| { + log.err("failed to load configuration: {}", .{err}); } + } else { + server.updateConfiguration(.{}) catch |err| { + log.err("failed to load configuration: {}", .{err}); + }; } return .{ @@ -636,7 +625,7 @@ fn initializedHandler(server: *Server, _: std.mem.Allocator, notification: types server.status = .initialized; - if (!server.recording_enabled and server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration) { + if (server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration) { try server.registerCapability("workspace/didChangeConfiguration"); } @@ -689,11 +678,6 @@ fn registerCapability(server: *Server, method: []const u8) Error!void { } fn requestConfiguration(server: *Server) Error!void { - if (server.recording_enabled) { - log.info("workspace/configuration are disabled during a recording session!", .{}); - return; - } - const configuration_items = comptime config: { var comp_config: [std.meta.fields(Config).len]types.ConfigurationItem = undefined; for (std.meta.fields(Config), 0..) |field, index| { @@ -719,11 +703,6 @@ fn handleConfiguration(server: *Server, json: std.json.Value) error{OutOfMemory} const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); - if (server.replay_enabled) { - log.info("workspace/configuration are disabled during a replay!", .{}); - return; - } - const fields = std.meta.fields(configuration.Configuration); const result = switch (json) { .array => |arr| if (arr.items.len == fields.len) arr.items else { @@ -806,12 +785,10 @@ fn didChangeConfigurationHandler(server: *Server, arena: std.mem.Allocator, noti return error.ParseError; }; - server.updateConfiguration(new_config) catch |err| { - log.err("failed to update configuration: {}", .{err}); - }; + try server.updateConfiguration(new_config); } -pub fn updateConfiguration2(server: *Server, new_config: Config) !void { +pub fn updateConfiguration2(server: *Server, new_config: Config) error{OutOfMemory}!void { var cfg: configuration.Configuration = .{}; inline for (std.meta.fields(Config)) |field| { @field(cfg, field.name) = @field(new_config, field.name); @@ -819,7 +796,7 @@ pub fn updateConfiguration2(server: *Server, new_config: Config) !void { try server.updateConfiguration(cfg); } -pub fn updateConfiguration(server: *Server, new_config: configuration.Configuration) !void { +pub fn updateConfiguration(server: *Server, new_config: configuration.Configuration) error{OutOfMemory}!void { // NOTE every changed configuration will increase the amount of memory allocated by the arena // This is unlikely to cause any big issues since the user is probably not going set settings // often in one session @@ -832,9 +809,10 @@ pub fn updateConfiguration(server: *Server, new_config: configuration.Configurat @field(new_cfg, field.name) = if (@field(new_config, field.name)) |new_value| new_value else @field(server.config, field.name); } - try server.validateConfiguration(&new_cfg); - try server.resolveConfiguration(config_arena, &new_cfg); - try server.validateConfiguration(&new_cfg); + server.validateConfiguration(&new_cfg); + const resolve_result = try resolveConfiguration(server.allocator, config_arena, &new_cfg); + defer resolve_result.deinit(); + server.validateConfiguration(&new_cfg); // <----------------------------------------------------------> // apply changes @@ -878,13 +856,6 @@ pub fn updateConfiguration(server: *Server, new_config: configuration.Configurat } } - if (server.config.zig_exe_path == null and - server.runtime_zig_version != null) - { - server.runtime_zig_version.?.free(); - server.runtime_zig_version = null; - } - if (new_zig_exe_path or new_build_runner_path) blk: { if (!std.process.can_spawn) break :blk; @@ -925,6 +896,44 @@ pub fn updateConfiguration(server: *Server, new_config: configuration.Configurat server.showMessage(.Warning, "zig executable could not be found", .{}); } + if (resolve_result.zig_runtime_version) |zig_version| version_check: { + const min_zig_string = comptime std.SemanticVersion.parse(build_options.min_zig_string) catch unreachable; + + const zig_version_is_tagged = zig_version.pre == null and zig_version.build == null; + const zls_version_is_tagged = build_options.version.pre == null and build_options.version.build == null; + + const zig_version_simple = std.SemanticVersion{ .major = zig_version.major, .minor = zig_version.minor, .patch = 0 }; + const zls_version_simple = std.SemanticVersion{ .major = build_options.version.major, .minor = build_options.version.minor, .patch = 0 }; + + if (zig_version_is_tagged != zls_version_is_tagged) { + if (zig_version_is_tagged) { + server.showMessage( + .Warning, + "Zig {} should be used with ZLS {} but ZLS {} is being used.", + .{ zig_version, zig_version_simple, build_options.version }, + ); + } else if (zls_version_is_tagged) { + server.showMessage( + .Warning, + "ZLS {} should be used with Zig {} but found Zig {}. ", + .{ build_options.version, zls_version_simple, zig_version }, + ); + } else unreachable; + break :version_check; + } + + if (zig_version.order(min_zig_string) == .lt) { + // don't report a warning when using a Zig version that has a matching build runner + if (resolve_result.build_runner_version != null and resolve_result.build_runner_version.? != .master) break :version_check; + server.showMessage( + .Warning, + "ZLS {s} requires at least Zig {s} but got Zig {}. Update Zig to avoid unexpected behavior.", + .{ build_options.version_string, build_options.min_zig_string, zig_version }, + ); + break :version_check; + } + } + 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 your OS can't spawn a child process", .{}); @@ -934,7 +943,7 @@ pub fn updateConfiguration(server: *Server, new_config: configuration.Configurat } } -fn validateConfiguration(server: *Server, config: *configuration.Configuration) !void { +fn validateConfiguration(server: *Server, config: *configuration.Configuration) void { inline for (comptime std.meta.fieldNames(Config)) |field_name| { const FileCheckInfo = struct { kind: enum { file, directory }, @@ -1034,35 +1043,57 @@ fn validateConfiguration(server: *Server, config: *configuration.Configuration) @field(config, field_name) = null; } } +} + +const ResolveConfigurationResult = struct { + zig_env: ?std.json.Parsed(configuration.Env), + zig_runtime_version: ?std.SemanticVersion, + build_runner_version: ?BuildRunnerVersion, - // some config options can't be changed after initialization - if (server.status != .uninitialized) { - config.record_session = null; - config.record_session_path = null; - config.replay_session_path = null; + fn deinit(result: ResolveConfigurationResult) void { + if (result.zig_env) |parsed| parsed.deinit(); } -} +}; + +fn resolveConfiguration( + allocator: std.mem.Allocator, + /// try leaking as little memory as possible since the ArenaAllocator is only deinit on exit + config_arena: std.mem.Allocator, + config: *configuration.Configuration, +) error{OutOfMemory}!ResolveConfigurationResult { + var result: ResolveConfigurationResult = .{ + .zig_env = null, + .zig_runtime_version = null, + .build_runner_version = null, + }; + errdefer result.deinit(); -fn resolveConfiguration(server: *Server, config_arena: std.mem.Allocator, config: *configuration.Configuration) !void { if (config.zig_exe_path == null) blk: { - std.debug.assert(!zig_builtin.is_test); - if (zig_builtin.is_test or !std.process.can_spawn) break :blk; - config.zig_exe_path = try configuration.findZig(config_arena); + if (zig_builtin.is_test) unreachable; + if (!std.process.can_spawn) break :blk; + const zig_exe_path = try configuration.findZig(allocator) orelse break :blk; + config.zig_exe_path = try config_arena.dupe(u8, zig_exe_path); } if (config.zig_exe_path) |exe_path| blk: { if (!std.process.can_spawn) break :blk; - const env = configuration.getZigEnv(server.allocator, exe_path) orelse break :blk; - defer env.deinit(); + result.zig_env = configuration.getZigEnv(allocator, exe_path); + const env = result.zig_env orelse break :blk; if (config.zig_lib_path == null) { - if (env.value.lib_dir) |lib_dir| { - const cwd = try std.process.getCwdAlloc(server.allocator); - defer server.allocator.free(cwd); + if (env.value.lib_dir) |lib_dir| resolve_lib_failed: { if (std.fs.path.isAbsolute(lib_dir)) { config.zig_lib_path = try config_arena.dupe(u8, lib_dir); } else { - config.zig_lib_path = try std.fs.path.resolve(config_arena, &.{ cwd, lib_dir }); + const cwd = std.process.getCwdAlloc(allocator) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => |e| { + log.err("failed to resolve current working directory: {}", .{e}); + break :resolve_lib_failed; + }, + }; + defer allocator.free(cwd); + config.zig_lib_path = try std.fs.path.join(config_arena, &.{ cwd, lib_dir }); } } } @@ -1071,92 +1102,101 @@ fn resolveConfiguration(server: *Server, config_arena: std.mem.Allocator, config config.build_runner_global_cache_path = try config_arena.dupe(u8, env.value.global_cache_dir); } - if (server.runtime_zig_version) |current_version| current_version.free(); - server.runtime_zig_version = null; - - const duped_zig_version_string = try server.allocator.dupe(u8, env.value.version); - errdefer server.allocator.free(duped_zig_version_string); - - server.runtime_zig_version = .{ - .version = try std.SemanticVersion.parse(duped_zig_version_string), - .allocator = server.allocator, - .raw_string = duped_zig_version_string, + result.zig_runtime_version = std.SemanticVersion.parse(env.value.version) catch |err| { + log.err("zig env returned a zig version that is an invalid semantic version: {}", .{err}); + break :blk; }; } if (config.global_cache_path == null) blk: { - std.debug.assert(!zig_builtin.is_test); - if (zig_builtin.is_test) break :blk; - const cache_dir_path = (try known_folders.getPath(server.allocator, .cache)) orelse { + if (zig_builtin.is_test) unreachable; + const cache_dir_path = known_folders.getPath(allocator, .cache) catch null orelse { log.warn("Known-folders could not fetch the cache path", .{}); - return; + break :blk; }; - defer server.allocator.free(cache_dir_path); + defer allocator.free(cache_dir_path); - config.global_cache_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ cache_dir_path, "zls" }); + config.global_cache_path = try std.fs.path.join(config_arena, &[_][]const u8{ cache_dir_path, "zls" }); - try std.fs.cwd().makePath(config.global_cache_path.?); + std.fs.cwd().makePath(config.global_cache_path.?) catch |err| { + log.warn("failed to create directory '{s}': {}", .{ config.global_cache_path.?, err }); + config.global_cache_path = null; + }; } - if (config.build_runner_path == null and - config.global_cache_path != null and - config.zig_exe_path != null and - server.runtime_zig_version != null) - { - const build_runner_version = BuildRunnerVersion.selectBuildRunnerVersion(server.runtime_zig_version.?.version); + if (config.build_runner_path == null) blk: { + if (!std.process.can_spawn) break :blk; + const global_cache_path = config.global_cache_path orelse break :blk; + const zig_version = result.zig_runtime_version orelse break :blk; - const build_runner_file_name = try std.fmt.allocPrint(config_arena, "build_runner_{s}.zig", .{@tagName(build_runner_version)}); - const build_runner_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, build_runner_file_name }); + result.build_runner_version = BuildRunnerVersion.selectBuildRunnerVersion(zig_version) orelse break :blk; - const build_runner_file = try std.fs.createFileAbsolute(build_runner_path, .{}); - defer build_runner_file.close(); + const build_runner_file_name = try std.fmt.allocPrint(allocator, "build_runner_{s}.zig", .{@tagName(result.build_runner_version.?)}); + defer allocator.free(build_runner_file_name); - const build_config_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, "BuildConfig.zig" }); + const build_runner_path = try std.fs.path.join(config_arena, &[_][]const u8{ global_cache_path, build_runner_file_name }); - const build_config_file = try std.fs.createFileAbsolute(build_config_path, .{}); - defer build_config_file.close(); + const build_config_path = try std.fs.path.join(allocator, &[_][]const u8{ global_cache_path, "BuildConfig.zig" }); + defer allocator.free(build_config_path); - try build_config_file.writeAll(@embedFile("build_runner/BuildConfig.zig")); + std.fs.cwd().writeFile2(.{ + .sub_path = build_config_path, + .data = @embedFile("build_runner/BuildConfig.zig"), + }) catch |err| { + log.err("failed to write file '{s}': {}", .{ build_config_path, err }); + break :blk; + }; - try build_runner_file.writeAll( - switch (build_runner_version) { + std.fs.cwd().writeFile2(.{ + .sub_path = build_runner_path, + .data = switch (result.build_runner_version.?) { inline else => |tag| @embedFile("build_runner/" ++ @tagName(tag) ++ ".zig"), }, - ); + }) catch |err| { + log.err("failed to write file '{s}': {}", .{ build_config_path, err }); + break :blk; + }; config.build_runner_path = build_runner_path; } if (config.builtin_path == null) blk: { if (!std.process.can_spawn) break :blk; - if (config.zig_exe_path == null) break :blk; - if (config.global_cache_path == null) break :blk; - - const result = try std.process.Child.run(.{ - .allocator = server.allocator, - .argv = &.{ - config.zig_exe_path.?, - "build-exe", - "--show-builtin", - }, - .max_output_bytes = 1024 * 1024 * 50, - }); - defer server.allocator.free(result.stdout); - defer server.allocator.free(result.stderr); + const zig_exe_path = config.zig_exe_path orelse break :blk; + const global_cache_path = config.global_cache_path orelse break :blk; - var d = try std.fs.cwd().openDir(config.global_cache_path.?, .{}); - defer d.close(); + const argv = [_][]const u8{ + zig_exe_path, + "build-exe", + "--show-builtin", + }; - const f = d.createFile("builtin.zig", .{}) catch |err| switch (err) { - error.AccessDenied => break :blk, - else => |e| return e, + const run_result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &argv, + .max_output_bytes = 1024 * 1024 * 50, + }) catch |err| { + const args = std.mem.join(allocator, " ", &argv) catch break :blk; + log.err("failed to run command '{s}': {}", .{ args, err }); + break :blk; }; - defer f.close(); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); - try f.writeAll(result.stdout); + const builtin_path = try std.fs.path.join(config_arena, &.{ global_cache_path, "builtin.zig" }); - config.builtin_path = try std.fs.path.join(config_arena, &.{ config.global_cache_path.?, "builtin.zig" }); + std.fs.cwd().writeFile2(.{ + .sub_path = builtin_path, + .data = run_result.stdout, + }) catch |err| { + log.err("failed to write file '{s}': {}", .{ builtin_path, err }); + break :blk; + }; + + config.builtin_path = builtin_path; } + + return result; } fn openDocumentHandler(server: *Server, _: std.mem.Allocator, notification: types.DidOpenTextDocumentParams) Error!void { @@ -1780,7 +1820,6 @@ pub fn create(allocator: std.mem.Allocator) !*Server { .document_store = .{ .allocator = allocator, .config = &server.config, - .runtime_zig_version = &server.runtime_zig_version, .thread_pool = if (zig_builtin.single_threaded) {} else undefined, // set below }, .job_queue = std.fifo.LinearFifo(Job, .Dynamic).init(allocator), @@ -1814,7 +1853,6 @@ pub fn destroy(server: *Server) void { server.document_store.deinit(); server.ip.deinit(server.allocator); server.client_capabilities.deinit(server.allocator); - if (server.runtime_zig_version) |zig_version| zig_version.free(); server.config_arena.promote(server.allocator).deinit(); server.allocator.destroy(server); } diff --git a/src/Transport.zig b/src/Transport.zig index d1bf7073e..31c45d3f4 100644 --- a/src/Transport.zig +++ b/src/Transport.zig @@ -3,8 +3,6 @@ const Header = @import("Header.zig"); in: std.io.BufferedReader(4096, std.fs.File.Reader), out: std.fs.File.Writer, -// TODO can we move this out of Transport? -record_file: ?std.fs.File.Writer = null, in_lock: std.Thread.Mutex = .{}, out_lock: std.Thread.Mutex = .{}, message_tracing: bool = false, @@ -33,13 +31,6 @@ pub fn readJsonMessage(self: *Transport, allocator: std.mem.Allocator) ![]u8 { errdefer allocator.free(json_message); try reader.readNoEof(json_message); - if (self.record_file) |file| { - var buffer: [64]u8 = undefined; - const prefix = std.fmt.bufPrint(&buffer, "Content-Length: {d}\r\n\r\n", .{json_message.len}) catch unreachable; - try file.writeAll(prefix); - try file.writeAll(json_message); - } - break :blk json_message; }; diff --git a/src/ZigVersionWrapper.zig b/src/ZigVersionWrapper.zig deleted file mode 100644 index 7c9850631..000000000 --- a/src/ZigVersionWrapper.zig +++ /dev/null @@ -1,13 +0,0 @@ -const std = @import("std"); -const Self = @This(); - -// This is necessary as `std.SemanticVersion` keeps pointers into the parsed string - -version: std.SemanticVersion, - -allocator: std.mem.Allocator, -raw_string: []const u8, - -pub fn free(self: Self) void { - self.allocator.free(self.raw_string); -} diff --git a/src/build_runner/BuildRunnerVersion.zig b/src/build_runner/BuildRunnerVersion.zig index 24ef916ac..ab7ef8908 100644 --- a/src/build_runner/BuildRunnerVersion.zig +++ b/src/build_runner/BuildRunnerVersion.zig @@ -8,7 +8,7 @@ pub const BuildRunnerVersion = enum { @"0.11.0", @"0.10.0", - pub fn selectBuildRunnerVersion(runtime_zig_version: std.SemanticVersion) BuildRunnerVersion { + pub fn selectBuildRunnerVersion(runtime_zig_version: std.SemanticVersion) ?BuildRunnerVersion { const runtime_zig_version_simple = std.SemanticVersion{ .major = runtime_zig_version.major, .minor = runtime_zig_version.minor, @@ -22,18 +22,16 @@ pub const BuildRunnerVersion = enum { return switch (runtime_zig_version_simple.order(zls_version_simple)) { .eq, .gt => .master, - .lt => blk: { - const available_versions = std.meta.tags(BuildRunnerVersion); - for (available_versions[1..]) |build_runner_version| { - const version = std.SemanticVersion.parse(@tagName(build_runner_version)) catch unreachable; + .lt => { + const available_versions = comptime std.meta.tags(BuildRunnerVersion); + inline for (available_versions[1..]) |build_runner_version| { + const version = comptime std.SemanticVersion.parse(@tagName(build_runner_version)) catch unreachable; switch (runtime_zig_version.order(version)) { - .eq, .gt => break :blk build_runner_version, - .lt => {}, + .eq => return build_runner_version, + .lt, .gt => {}, } } - - // failed to find compatible build runner, falling back to oldest supported version - break :blk available_versions[available_versions.len - 1]; + return null; }, }; } diff --git a/src/config_gen/config.json b/src/config_gen/config.json index 7269f4234..149366aaa 100644 --- a/src/config_gen/config.json +++ b/src/config_gen/config.json @@ -119,24 +119,6 @@ "type": "bool", "default": true }, - { - "name": "record_session", - "description": "When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay`", - "type": "bool", - "default": false - }, - { - "name": "record_session_path", - "description": "Output file path when `record_session` is set. The recommended file extension *.zlsreplay", - "type": "?[]const u8", - "default": null - }, - { - "name": "replay_session_path", - "description": "Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path.", - "type": "?[]const u8", - "default": null - }, { "name": "builtin_path", "description": "Path to 'builtin;' useful for debugging, automatically set if let null", diff --git a/src/configuration.zig b/src/configuration.zig index ed651ce22..6330b2d2e 100644 --- a/src/configuration.zig +++ b/src/configuration.zig @@ -1,7 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); const tracy = @import("tracy"); const known_folders = @import("known-folders"); @@ -10,28 +9,89 @@ const offsets = @import("offsets.zig"); const logger = std.log.scoped(.zls_config); -pub const ConfigWithPath = struct { - config: Config, - arena: std.heap.ArenaAllocator.State, - /// The path to the file from which the config was read. - config_path: ?[]const u8, +pub 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 { + 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 { + const local_config_path = getLocalConfigPath(allocator) catch |err| blk: { + logger.warn("failed to resolve local configuration path: {}", .{err}); + break :blk null; + }; + defer if (local_config_path) |path| allocator.free(path); - pub fn deinit(self: *ConfigWithPath, allocator: std.mem.Allocator) void { - self.arena.promote(allocator).deinit(); - if (self.config_path) |path| allocator.free(path); - self.* = undefined; + const global_config_path = getGlobalConfigPath(allocator) catch |err| blk: { + logger.warn("failed to resolve global configuration path: {}", .{err}); + break :blk null; + }; + defer if (global_config_path) |path| allocator.free(path); + + for ([_]?[]const u8{ local_config_path, global_config_path }) |config_path| { + const result = try loadFromFile(allocator, config_path orelse continue); + switch (result) { + .success, .failure => return result, + .not_found => {}, + } + } + + return .not_found; +} + +pub const LoadConfigResult = union(enum) { + success: struct { + config: std.json.Parsed(Config), + /// file path of the config.json + path: []const u8, + }, + failure: struct { + /// `null` indicates that the error has already been logged + error_bundle: ?std.zig.ErrorBundle, + + pub fn toMessage(self: @This(), allocator: std.mem.Allocator) error{OutOfMemory}!?[]u8 { + const error_bundle = self.error_bundle orelse return null; + var msg: std.ArrayListUnmanaged(u8) = .{}; + errdefer msg.deinit(allocator); + error_bundle.renderToWriter(.{ .ttyconf = .no_color }, msg.writer(allocator)) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + else => unreachable, // why does renderToWriter return `anyerror!void`? + }; + return try msg.toOwnedSlice(allocator); + } + }, + not_found, + + pub fn deinit(self: *LoadConfigResult, allocator: std.mem.Allocator) void { + switch (self.*) { + .success => |*config_with_path| { + config_with_path.config.deinit(); + allocator.free(config_with_path.path); + }, + .failure => |*payload| { + if (payload.error_bundle) |*error_bundle| error_bundle.deinit(allocator); + }, + .not_found => {}, + } } }; -pub fn loadFromFile(allocator: std.mem.Allocator, file_path: []const u8) ?ConfigWithPath { +pub fn loadFromFile(allocator: std.mem.Allocator, file_path: []const u8) error{OutOfMemory}!LoadConfigResult { const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); - const file_buf = std.fs.cwd().readFileAlloc(allocator, file_path, std.math.maxInt(usize)) catch |err| switch (err) { - error.FileNotFound => return null, + const file_buf = std.fs.cwd().readFileAlloc(allocator, file_path, std.math.maxInt(u32)) catch |err| switch (err) { + error.FileNotFound => return .not_found, + error.OutOfMemory => |e| return e, else => { logger.warn("Error while reading configuration file: {}", .{err}); - return null; + return .{ .failure = .{ .error_bundle = null } }; }, }; defer allocator.free(file_buf); @@ -46,65 +106,40 @@ pub fn loadFromFile(allocator: std.mem.Allocator, file_path: []const u8) ?Config defer scanner.deinit(); scanner.enableDiagnostics(&parse_diagnostics); - var arena_allocator = std.heap.ArenaAllocator.init(allocator); - errdefer arena_allocator.deinit(); - @setEvalBranchQuota(10000); - // TODO: report errors using "textDocument/publishDiagnostics" - const config = std.json.parseFromTokenSourceLeaky( + const config = std.json.parseFromTokenSource( Config, - arena_allocator.allocator(), + allocator, &scanner, parse_options, ) catch |err| { - logger.warn( - "{s}:{d}:{d}: Error while parsing configuration file {}", - .{ file_path, parse_diagnostics.getLine(), parse_diagnostics.getColumn(), err }, - ); - return null; + var eb: std.zig.ErrorBundle.Wip = undefined; + try eb.init(allocator); + errdefer eb.deinit(); + + const src_path = try eb.addString(file_path); + const msg = try eb.addString(@errorName(err)); + + const src_loc = try eb.addSourceLocation(.{ + .src_path = src_path, + .line = @intCast(parse_diagnostics.getLine()), + .column = @intCast(parse_diagnostics.getColumn()), + .span_start = @intCast(parse_diagnostics.getByteOffset()), + .span_main = @intCast(parse_diagnostics.getByteOffset()), + .span_end = @intCast(parse_diagnostics.getByteOffset()), + }); + try eb.addRootErrorMessage(.{ + .msg = msg, + .src_loc = src_loc, + }); + + return .{ .failure = .{ .error_bundle = try eb.toOwnedBundle("") } }; }; - return .{ + return .{ .success = .{ .config = config, - .arena = arena_allocator.state, - .config_path = file_path, - }; -} - -pub fn getConfig(allocator: std.mem.Allocator, config_path: ?[]const u8) !ConfigWithPath { - if (config_path) |path| { - if (loadFromFile(allocator, path)) |config| { - var cfg = config; - errdefer cfg.deinit(allocator); - cfg.config_path = try allocator.dupe(u8, path); - return cfg; - } - logger.info( - \\Could not open configuration file '{s}' - \\Falling back to a lookup in the local and global configuration folders - \\ - , .{path}); - } - - if (try known_folders.getPath(allocator, .local_configuration)) |folder_path| { - defer allocator.free(folder_path); - const file_path = try std.fs.path.resolve(allocator, &.{ folder_path, "zls.json" }); - if (loadFromFile(allocator, file_path)) |config| return config; - allocator.free(file_path); - } - - if (try known_folders.getPath(allocator, .global_configuration)) |folder_path| { - defer allocator.free(folder_path); - const file_path = try std.fs.path.resolve(allocator, &.{ folder_path, "zls.json" }); - if (loadFromFile(allocator, file_path)) |config| return config; - allocator.free(file_path); - } - - return ConfigWithPath{ - .config = .{}, - .arena = .{}, - .config_path = null, - }; + .path = try allocator.dupe(u8, file_path), + } }; } pub const Env = struct { @@ -116,7 +151,6 @@ pub const Env = struct { target: ?[]const u8 = null, }; -/// result has to be freed with `json_compat.parseFree` pub fn getZigEnv(allocator: std.mem.Allocator, zig_exe_path: []const u8) ?std.json.Parsed(Env) { const zig_env_result = std.process.Child.run(.{ .allocator = allocator, @@ -173,32 +207,47 @@ fn getConfigurationType() type { return @Type(config_info); } -pub fn findZig(allocator: std.mem.Allocator) !?[]const u8 { +pub fn findZig(allocator: std.mem.Allocator) error{OutOfMemory}!?[]const u8 { const env_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { - error.EnvironmentVariableNotFound => { + error.EnvironmentVariableNotFound => return null, + error.OutOfMemory => |e| return e, + error.InvalidWtf8 => |e| { + logger.err("failed to load 'PATH' enviorment variable: {}", .{e}); return null; }, - else => return err, }; defer allocator.free(env_path); - const exe_extension = builtin.target.exeFileExt(); - const zig_exe = try std.fmt.allocPrint(allocator, "zig{s}", .{exe_extension}); - defer allocator.free(zig_exe); + const zig_exe = "zig" ++ comptime builtin.target.exeFileExt(); - var it = std.mem.tokenize(u8, env_path, &[_]u8{std.fs.path.delimiter}); + var it = std.mem.tokenizeScalar(u8, env_path, std.fs.path.delimiter); while (it.next()) |path| { - const full_path = try std.fs.path.join(allocator, &[_][]const u8{ path, zig_exe }); + var full_path = try std.fs.path.join(allocator, &[_][]const u8{ path, zig_exe }); defer allocator.free(full_path); - if (!std.fs.path.isAbsolute(full_path)) continue; + if (!std.fs.path.isAbsolute(full_path)) { + logger.warn("ignoring entry in PATH '{s}' because it is not an absolute file path", .{full_path}); + continue; + } - const file = std.fs.openFileAbsolute(full_path, .{}) catch continue; + const file = std.fs.openFileAbsolute(full_path, .{}) catch |err| switch (err) { + error.FileNotFound => continue, + else => |e| { + logger.warn("failed to open entry in PATH '{s}': {}", .{ full_path, e }); + continue; + }, + }; defer file.close(); - const stat = file.stat() catch continue; - if (stat.kind == .directory) continue; - return try allocator.dupe(u8, full_path); + stat_failed: { + const stat = file.stat() catch break :stat_failed; + if (stat.kind == .directory) { + logger.warn("ignoring entry in PATH '{s}' because it is a directory", .{full_path}); + } + } + + defer full_path = ""; + return full_path; } return null; } diff --git a/src/main.zig b/src/main.zig index fbba79738..538bc8dd6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,7 +4,6 @@ const zls = @import("zls"); const exe_options = @import("exe_options"); const tracy = @import("tracy"); -const known_folders = @import("known-folders"); const binned_allocator = @import("binned_allocator.zig"); const logger = std.log.scoped(.zls_main); @@ -41,101 +40,33 @@ pub const std_options = std.Options{ .logFn = logFn, }; -fn getRecordFile(config: zls.Config) ?std.fs.File { - if (!config.record_session) return null; - - if (config.record_session_path) |record_path| { - if (std.fs.createFileAbsolute(record_path, .{})) |file| { - logger.info("recording to {s}", .{record_path}); - return file; - } else |err| { - logger.err("failed to create record file at {s}: {}", .{ record_path, err }); - return null; - } - } else { - logger.err("`record_session` is set but `record_session_path` is unspecified", .{}); - return null; - } -} - -fn getReplayFile(config: zls.Config) ?std.fs.File { - const replay_path = config.replay_session_path orelse config.record_session_path orelse return null; - - if (std.fs.openFileAbsolute(replay_path, .{})) |file| { - logger.info("replaying from {s}", .{replay_path}); - return file; - } else |err| { - logger.err("failed to open replay file at {s}: {}", .{ replay_path, err }); - return null; - } -} - -/// when recording we add a message that saves the current configuration in the replay -/// when replaying we read this message and replace the current config -fn updateConfig( - allocator: std.mem.Allocator, - transport: *zls.Transport, - config: *zls.configuration.ConfigWithPath, - record_file: ?std.fs.File, - replay_file: ?std.fs.File, -) !void { - std.debug.assert(record_file == null or replay_file == null); - if (record_file) |file| { - var cfg = config.config; - cfg.record_session = false; - cfg.record_session_path = null; - cfg.replay_session_path = null; - - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); - try std.json.stringify(cfg, .{}, buffer.writer(allocator)); - - var header = zls.Header{ .content_length = buffer.items.len }; - try header.write(file.writer()); - try file.writeAll(buffer.items); - } - - if (replay_file != null) { - const json_message = try transport.readJsonMessage(allocator); - defer allocator.free(json_message); - - const new_config = try std.json.parseFromSlice( - zls.Config, - allocator, - json_message, - .{ .allocate = .alloc_always }, - ); - defer allocator.destroy(new_config.arena); - config.arena.promote(allocator).deinit(); - config.arena = new_config.arena.state; - config.config = new_config.value; - } -} - const ParseArgsResult = struct { action: enum { proceed, exit }, config_path: ?[]const u8, - replay_enabled: bool, message_tracing_enabled: bool, zls_exe_path: []const u8, + + fn deinit(self: ParseArgsResult, allocator: std.mem.Allocator) void { + defer if (self.config_path) |path| allocator.free(path); + defer allocator.free(self.zls_exe_path); + } }; fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult { var result = ParseArgsResult{ .action = .exit, .config_path = null, - .replay_enabled = false, .message_tracing_enabled = false, - .zls_exe_path = undefined, + .zls_exe_path = "", }; + errdefer result.deinit(allocator); const ArgId = enum { help, version, @"minimum-build-version", @"compiler-version", - replay, @"enable-debug-log", @"enable-message-tracing", @"show-config-path", @@ -155,7 +86,6 @@ fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult { .version = "Prints the version.", .@"minimum-build-version" = "Prints the minimum build version specified in build.zig.", .@"compiler-version" = "Prints the compiler version with which the server was compiled.", - .replay = "Replay a previous recorded zls session", .@"enable-debug-log" = "Enables debug logs.", .@"enable-message-tracing" = "Enables message tracing.", .@"config-path" = "Specify the path to a configuration file specifying LSP behaviour.", @@ -217,9 +147,6 @@ fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult { }; result.config_path = try allocator.dupe(u8, path); }, - .replay => { - result.replay_enabled = true; - }, } } @@ -251,25 +178,36 @@ fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult { std.debug.assert(result.config_path != null); } if (specified.get(.@"show-config-path")) { - var new_config = try zls.configuration.getConfig(allocator, result.config_path); - defer new_config.deinit(allocator); + var config_result = if (result.config_path) |config_path| + try zls.configuration.loadFromFile(allocator, config_path) + else + try zls.configuration.load(allocator); + defer config_result.deinit(allocator); - if (new_config.config_path) |path| { - try stdout.writeAll(path); - try stdout.writeByte('\n'); - return result; - } else { - const local_config_path = try known_folders.getPath(allocator, .local_configuration) orelse { - logger.err("failed to find local configuration folder", .{}); + switch (config_result) { + .success => |config_with_path| { + try stdout.writeAll(config_with_path.path); + try stdout.writeByte('\n'); return result; - }; - defer allocator.free(local_config_path); - const full_path = try std.fs.path.join(allocator, &.{ local_config_path, "zls.json" }); - defer allocator.free(full_path); - try stdout.writeAll(full_path); - try stdout.writeByte('\n'); - return result; + }, + .failure => |payload| blk: { + const message = try payload.toMessage(allocator) orelse break :blk; + defer allocator.free(message); + logger.err("Failed to load configuration options.", .{}); + logger.err("{s}", .{message}); + }, + .not_found => logger.info("No config file zls.json found.", .{}), } + + logger.info("A path to the local configuration folder will be printed instead.", .{}); + const local_config_path = zls.configuration.getLocalConfigPath(allocator) catch null orelse { + logger.err("failed to find local zls.json", .{}); + std.process.exit(1); + }; + defer allocator.free(local_config_path); + try stdout.writeAll(local_config_path); + try stdout.writeByte('\n'); + return result; } result.action = .proceed; @@ -301,8 +239,7 @@ pub fn main() !void { const allocator: std.mem.Allocator = if (exe_options.enable_failing_allocator) failing_allocator_state.allocator() else inner_allocator; const result = try parseArgs(allocator); - defer allocator.free(result.zls_exe_path); - defer if (result.config_path) |path| allocator.free(path); + defer result.deinit(allocator); switch (result.action) { .proceed => {}, .exit => return, @@ -310,39 +247,16 @@ pub fn main() !void { logger.info("Starting ZLS {s} @ '{s}'", .{ zls.build_options.version_string, result.zls_exe_path }); - var config = try zls.configuration.getConfig(allocator, result.config_path); - defer config.deinit(allocator); - - if (result.replay_enabled and config.config.record_session_path == null) { - logger.err("No replay file specified", .{}); - return; - } - - if (config.config_path == null) { - logger.info("No config file zls.json found.", .{}); - } - - const record_file = if (!result.replay_enabled) getRecordFile(config.config) else null; - defer if (record_file) |file| file.close(); - - const replay_file = if (result.replay_enabled) getReplayFile(config.config) else null; - defer if (replay_file) |file| file.close(); - var transport = zls.Transport.init( - if (replay_file) |file| file.reader() else std.io.getStdIn().reader(), + std.io.getStdIn().reader(), std.io.getStdOut().writer(), ); transport.message_tracing = result.message_tracing_enabled; - if (record_file) |file| transport.record_file = file.writer(); - - try updateConfig(allocator, &transport, &config, record_file, replay_file); const server = try zls.Server.create(allocator); defer server.destroy(); - try server.updateConfiguration2(config.config); - server.recording_enabled = record_file != null; - server.replay_enabled = replay_file != null; server.transport = &transport; + server.config_path = result.config_path; try server.loop(); diff --git a/src/zls.zig b/src/zls.zig index d07362ca3..a506dbae1 100644 --- a/src/zls.zig +++ b/src/zls.zig @@ -19,7 +19,6 @@ pub const ComptimeInterpreter = @import("ComptimeInterpreter.zig"); pub const diff = @import("diff.zig"); pub const analyser = @import("analyser/analyser.zig"); pub const configuration = @import("configuration.zig"); -pub const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); pub const DocumentScope = @import("DocumentScope.zig"); pub const signature_help = @import("features/signature_help.zig"); diff --git a/tests/language_features/comptime_interpreter.zig b/tests/language_features/comptime_interpreter.zig index 48fe2c214..16dec8fd7 100644 --- a/tests/language_features/comptime_interpreter.zig +++ b/tests/language_features/comptime_interpreter.zig @@ -3,7 +3,6 @@ const zls = @import("zls"); const builtin = @import("builtin"); const Ast = std.zig.Ast; -const ZigVersionWrapper = zls.ZigVersionWrapper; const ComptimeInterpreter = zls.ComptimeInterpreter; const InternPool = zls.analyser.InternPool; const Index = InternPool.Index;