diff --git a/src/Config.zig b/src/Config.zig index 8856dbc..641cf37 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -9,10 +9,10 @@ enable_unused_variable_warnings: bool = false, /// Whether to enable import/embedFile argument completions (NOTE: these are triggered manually as updating the autotrigger characters may cause issues) enable_import_embedfile_argument_completions: bool = false, -/// zig library path +/// Zig library path zig_lib_path: ?[]const u8 = null, -/// zig executable path used to run the custom build runner. +/// Zig executable path used to run the custom build runner. /// May be used to find a lib path if none is provided. zig_exe_path: ?[]const u8 = null, @@ -36,10 +36,11 @@ operator_completions: bool = true, include_at_in_builtins: bool = false, /// The detail field of completions is truncated to be no longer than this (in bytes). -max_detail_length: usize = 1024 * 1024, +max_detail_length: usize = 1048576, /// Skips references to std. This will improve lookup speeds. /// Going to definition however will continue to work skip_std_references: bool = false, +/// Path to "builtin;" useful for debugging, automatically set if let null builtin_path: ?[]const u8 = null, diff --git a/src/data/generate-vscode-config.js b/src/data/generate-vscode-config.js new file mode 100644 index 0000000..599566d --- /dev/null +++ b/src/data/generate-vscode-config.js @@ -0,0 +1,45 @@ +// Run with node + +const fs = require("fs"); +const path = require("path"); + +const sourceOfTruth = fs.readFileSync(path.join(__dirname, "..", "Config.zig")); + +const lines = sourceOfTruth.toString().split("\n"); + +function mapType(type) { + switch (type) { + case "?[]const u8": + return "string"; + + case "bool": + return "boolean"; + + case "usize": + return "integer"; + + default: + throw new Error("unknown type!"); + } +} + +let comment = null; +for (const line of lines) { + if (line.startsWith("///")) { + if (comment === null) comment = line.slice(3).trim(); + else comment += line.slice(3); + } else if (comment) { + const name = line.split(":")[0].trim(); + const type = line.split(":")[1].split("=")[0].trim(); + const defaultValue = line.split(":")[1].split("=")[1].trim().replace(",",""); + + console.log(`"zls.${name}": ${JSON.stringify({ + "scope": "resource", + "type": mapType(type), + "description": comment, + "default": JSON.parse(defaultValue) + })},`); + + comment = null; + } +} diff --git a/src/main.zig b/src/main.zig index 26eb85e..98a5763 100644 --- a/src/main.zig +++ b/src/main.zig @@ -91,6 +91,7 @@ const ClientCapabilities = struct { hover_supports_md: bool = false, completion_doc_supports_md: bool = false, label_details_support: bool = false, + supports_configuration: bool = false, }; var client_capabilities = ClientCapabilities{}; @@ -1741,15 +1742,9 @@ fn initializeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: }); if (req.params.capabilities.workspace) |workspace| { - if (workspace.configuration.value) { - try send(arena, types.Request{ - .method = "workspace/configuration", - .params = .{ - .ConfigurationParams = .{ - .items = &[_]types.ConfigurationParams.ConfigurationItem{}, - }, - }, - }); + client_capabilities.supports_configuration = workspace.configuration.value; + if (workspace.didChangeConfiguration != null and workspace.didChangeConfiguration.?.dynamicRegistration.value) { + try registerCapability(arena, "workspace/didChangeConfiguration"); } } @@ -1758,6 +1753,68 @@ fn initializeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: logger.info("Using offset encoding: {s}", .{std.meta.tagName(offset_encoding)}); } +fn registerCapability(arena: *std.heap.ArenaAllocator, method: []const u8) !void { + // NOTE: stage1 moment occurs if we dont do it like this :( + // long live stage2's not broken anon structs + + logger.debug("Dynamically registering method '{s}'", .{method}); + + const id = try std.fmt.allocPrint(arena.allocator(), "register-{s}", .{method}); + const reg = types.RegistrationParams.Registration{ + .id = id, + .method = method, + }; + const registrations = [1]types.RegistrationParams.Registration{reg}; + const params = types.RegistrationParams{ + .registrations = ®istrations, + }; + + const respp = types.ResponseParams{ + .RegistrationParams = params, + }; + + const req = types.Request{ + .id = .{ .String = id }, + .method = "client/registerCapability", + .params = respp, + }; + + try send(arena, req); +} + +fn requestConfiguration(arena: *std.heap.ArenaAllocator) !void { + const configuration_items = comptime confi: { + var comp_confi: [std.meta.fields(Config).len]types.ConfigurationParams.ConfigurationItem = undefined; + inline for (std.meta.fields(Config)) |field, index| { + comp_confi[index] = .{ + .scopeUri = "zls", + .section = "zls." ++ field.name, + }; + } + + break :confi comp_confi; + }; + + logger.info("Requesting configuration!", .{}); + try send(arena, types.Request{ + .id = .{ .String = "i_haz_configuration" }, + .method = "workspace/configuration", + .params = .{ + .ConfigurationParams = .{ + .items = &configuration_items, + }, + }, + }); +} + +fn initializedHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, config: *const Config) !void { + _ = id; + _ = config; + + if (client_capabilities.supports_configuration) + try requestConfiguration(arena); +} + var keep_running = true; fn shutdownHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, config: *const Config) !void { _ = config; @@ -2121,18 +2178,22 @@ fn renameHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requ } } -fn didChangeConfigurationHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.Configuration, config: *Config) !void { +fn didChangeConfigurationHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, maybe_req: std.json.Value, config: *Config) !void { const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); _ = arena; _ = id; - inline for (std.meta.fields(Config)) |field| { - if (@field(req.params.settings, field.name)) |value| { - logger.debug("setting configuration option '{s}' to '{any}'", .{ field.name, value }); - @field(config, field.name) = value; + if (maybe_req.Object.get("params").?.Object.get("settings").? == .Object) { + const req = try requests.fromDynamicTree(arena, requests.Configuration, maybe_req); + inline for (std.meta.fields(Config)) |field| { + if (@field(req.params.settings, field.name)) |value| { + logger.debug("setting configuration option '{s}' to '{any}'", .{ field.name, value }); + @field(config, field.name) = value; + } } - } + } else if (client_capabilities.supports_configuration) + try requestConfiguration(arena); } fn referencesHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.References, config: *const Config) !void { @@ -2179,6 +2240,51 @@ fn processJsonRpc(arena: *std.heap.ArenaAllocator, parser: *std.json.Parser, jso else => types.RequestId{ .Integer = 0 }, } else types.RequestId{ .Integer = 0 }; + if (id == .String and std.mem.startsWith(u8, id.String, "register")) + return; + if (id == .String and std.mem.eql(u8, id.String, "i_haz_configuration")) { + logger.info("Setting configuration...", .{}); + + // NOTE: Does this work with other editors? + // Yes, String ids are officially supported by LSP + // but not sure how standard this "standard" really is + + const result = tree.root.Object.get("result").?.Array; + + inline for (std.meta.fields(Config)) |field, index| { + const value = result.items[index]; + const ft = if (@typeInfo(field.field_type) == .Optional) + @typeInfo(field.field_type).Optional.child + else + field.field_type; + const ti = @typeInfo(ft); + + if (value != .Null) { + const new_value: field.field_type = switch (ft) { + []const u8 => switch (value) { + .String => |s| s, + else => @panic("Invalid configuration value"), // TODO: Handle this + }, + else => switch (ti) { + .Int => switch (value) { + .Integer => |s| std.math.cast(ft, s) orelse @panic("Invalid configuration value"), + else => @panic("Invalid configuration value"), // TODO: Handle this + }, + .Bool => switch (value) { + .Bool => |b| b, + else => @panic("Invalid configuration value"), // TODO: Handle this + }, + else => @compileError("Not implemented for " ++ @typeName(ft)), + }, + }; + logger.debug("setting configuration option '{s}' to '{any}'", .{ field.name, new_value }); + @field(config, field.name) = new_value; + } + } + + return; + } + std.debug.assert(tree.root.Object.get("method") != null); const method = tree.root.Object.get("method").?.String; @@ -2189,7 +2295,7 @@ fn processJsonRpc(arena: *std.heap.ArenaAllocator, parser: *std.json.Parser, jso } const method_map = .{ - .{"initialized"}, + .{ "initialized", void, initializedHandler }, .{"$/cancelRequest"}, .{"textDocument/willSave"}, .{ "initialize", requests.Initialize, initializeHandler }, @@ -2210,7 +2316,7 @@ fn processJsonRpc(arena: *std.heap.ArenaAllocator, parser: *std.json.Parser, jso .{ "textDocument/formatting", requests.Formatting, formattingHandler }, .{ "textDocument/rename", requests.Rename, renameHandler }, .{ "textDocument/references", requests.References, referencesHandler }, - .{ "workspace/didChangeConfiguration", requests.Configuration, didChangeConfigurationHandler }, + .{ "workspace/didChangeConfiguration", std.json.Value, didChangeConfigurationHandler }, }; // Hack to avoid `return`ing in the inline for, which causes bugs. diff --git a/src/requests.zig b/src/requests.zig index 8ebad11..7ca8295 100644 --- a/src/requests.zig +++ b/src/requests.zig @@ -148,6 +148,9 @@ pub const Initialize = struct { pub const ClientCapabilities = struct { workspace: ?struct { configuration: Default(bool, false), + didChangeConfiguration: ?struct { + dynamicRegistration: Default(bool, false), // NOTE: Should this be true? Seems like this critical feature should be nearly universal + }, workspaceFolders: Default(bool, false), }, textDocument: ?struct { diff --git a/src/types.zig b/src/types.zig index 38a65d4..f8ba92e 100644 --- a/src/types.zig +++ b/src/types.zig @@ -44,6 +44,7 @@ pub const ResponseParams = union(enum) { WorkspaceEdit: WorkspaceEdit, InitializeResult: InitializeResult, ConfigurationParams: ConfigurationParams, + RegistrationParams: RegistrationParams, }; /// JSONRPC notifications @@ -77,6 +78,7 @@ pub const Response = struct { pub const Request = struct { jsonrpc: string = "2.0", + id: RequestId, method: []const u8, params: ?ResponseParams, }; @@ -400,3 +402,14 @@ pub const ConfigurationParams = struct { section: ?[]const u8, }; }; + +pub const RegistrationParams = struct { + registrations: []const Registration, + + pub const Registration = struct { + id: string, + method: string, + + // registerOptions?: LSPAny; + }; +}; diff --git a/tests/sessions.zig b/tests/sessions.zig index 303912e..ca361c1 100644 --- a/tests/sessions.zig +++ b/tests/sessions.zig @@ -66,7 +66,7 @@ const Server = struct { const rest = response_bytes[json_fmt.len..]; const id_end = std.mem.indexOfScalar(u8, rest, ',') orelse return error.InvalidResponse; - const id = try std.fmt.parseInt(u32, rest[0..id_end], 10); + const id = std.fmt.parseInt(u32, rest[0..id_end], 10) catch continue; if (id != self.request_id) { continue; @@ -83,6 +83,7 @@ const Server = struct { } } } + fn extractError(msg: []const u8) !void { const log_request = \\"method":"window/logMessage","params":{"type":