diff --git a/README.md b/README.md index 54db2f3ce..5403ae361 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ The following LSP features are supported: - Selection ranges - Folding regions +## Custom features + +Custom (non-standard) server endpoints: + +- **`customZig/listBuildSteps`**: Can be used by the client to request the list of top-level steps for a given workspace. The endpoint expects a mandatory `{workspaceUri: string}` as the `params` object. Internally, the `workspaceUri` property is used to identify the root `build.zig` file for the workspace, from which the top-level steps (and their corresponding descriptions) are extracted and returned as `{name: string, description: string}[]`. The main use case for this endpoint is to allow client IDEs to generate automatic tasks (e.g., to implement a `vscode.TaskProvider` in VS Code) based on the top-level steps in a workspace's root `build.zig` without the IDEs having to implement a custom build runner themselves (since that job is already being done by ZLS). + ## Related Projects - [`sublime-zig-language` by @prime31](https://github.com/prime31/sublime-zig-language) diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index e40a9881a..34c280704 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -835,6 +835,22 @@ fn getOrLoadBuildFile(self: *DocumentStore, uri: Uri) error{ Canceled, OutOfMemo return new_build_file; } +/// When a new workspace containing build.zig files is added, those files are loaded lazily. +/// While this is desirable for most cases, certain edge cases benefit from having +/// `BuildConfig` objects readily available. This method primes the internal worker process +/// to immediately analyze the target build.zig file. +pub fn primeBuildFile(self: *DocumentStore, build_file_uri: Uri) error{ Canceled, OutOfMemory, InvalidBuildFileUri, DocumentStoreDoesNotSupportBuildSystem }!void { + if (!isBuildFile(build_file_uri)) { + return error.InvalidBuildFileUri; + } + + if (!supports_build_system) { + return error.DocumentStoreDoesNotSupportBuildSystem; + } + + _ = try self.getOrLoadBuildFile(build_file_uri); +} + /// Opens a document that is synced over the LSP protocol (`textDocument/didOpen`). /// **Not thread safe** pub fn openLspSyncedDocument(self: *DocumentStore, uri: Uri, text: []const u8) error{ Canceled, OutOfMemory }!void { diff --git a/src/Server.zig b/src/Server.zig index b7d48eb2b..cf751d304 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -35,6 +35,8 @@ const hover_handler = @import("features/hover.zig"); const selection_range = @import("features/selection_range.zig"); const diagnostics_gen = @import("features/diagnostics.zig"); +const list_build_steps = @import("custom_features/list_build_steps.zig"); + const BuildOnSave = diagnostics_gen.BuildOnSave; const BuildOnSaveSupport = build_runner_shared.BuildOnSaveSupport; @@ -1869,7 +1871,13 @@ fn processMessage(server: *Server, arena: std.mem.Allocator, message: Message) E switch (message) { .request => |request| switch (request.params) { - .other => return try server.sendToClientResponse(request.id, @as(?void, null)), + .other => |method_with_params| { + if (std.mem.eql(u8, method_with_params.method, list_build_steps.method_name)) { + const result = try list_build_steps.extractBuildStepsInfoHandler(server, arena, method_with_params.params); + return try server.sendToClientResponse(request.id, result.items); + } + return try server.sendToClientResponse(request.id, @as(?void, null)); + }, inline else => |params, method| { const result = try server.sendRequestSync(arena, @tagName(method), params); return try server.sendToClientResponse(request.id, result); diff --git a/src/build_runner/build_runner.zig b/src/build_runner/build_runner.zig index d5e28a04f..9195f91ca 100644 --- a/src/build_runner/build_runner.zig +++ b/src/build_runner/build_runner.zig @@ -1266,13 +1266,19 @@ fn extractBuildInformation( available_options.putAssumeCapacityNoClobber(available_option.key_ptr.*, available_option.value_ptr.*); } + const top_level_steps = try gpa.alloc(BuildConfig.TopLevelStepInfo, b.top_level_steps.count()); + defer gpa.free(top_level_steps); + for (b.top_level_steps.values(), top_level_steps) |v, *i| { + i.* = .{ .name = v.step.name, .description = v.description }; + } + const stringified_build_config = try std.json.Stringify.valueAlloc( gpa, BuildConfig{ .dependencies = .{ .map = root_dependencies }, .modules = .{ .map = modules }, .compilations = compilations.items, - .top_level_steps = b.top_level_steps.keys(), + .top_level_steps = top_level_steps, .available_options = .{ .map = available_options }, }, .{ .whitespace = .indent_2 }, diff --git a/src/build_runner/shared.zig b/src/build_runner/shared.zig index 7047f5def..ff112c01f 100644 --- a/src/build_runner/shared.zig +++ b/src/build_runner/shared.zig @@ -11,8 +11,8 @@ pub const BuildConfig = struct { modules: std.json.ArrayHashMap(Module), /// List of all compilations units. compilations: []const Compile, - /// The names of all top level steps. - top_level_steps: []const []const u8, + /// The "name and description" pairs of all top level steps. + top_level_steps: []const TopLevelStepInfo, available_options: std.json.ArrayHashMap(AvailableOption), pub const Module = struct { @@ -30,6 +30,17 @@ pub const BuildConfig = struct { /// Equivalent to `std.Build.AvailableOption` which is not accessible because it non-pub. pub const AvailableOption = @FieldType(@FieldType(std.Build, "available_options_map").KV, "value"); + + pub const TopLevelStepInfo = struct { + name: []const u8, + description: []const u8, + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.name); + allocator.free(self.description); + self.* = undefined; + } + }; }; pub const ServerToClient = struct { diff --git a/src/custom_features/list_build_steps.zig b/src/custom_features/list_build_steps.zig new file mode 100644 index 000000000..ac25269c9 --- /dev/null +++ b/src/custom_features/list_build_steps.zig @@ -0,0 +1,115 @@ +//! Request handling structures and properties used for the custom `customZig/listBuildSteps` +//! server endpoint. The endpoint expects a `params` object of type `{ workspaceUri: string }`. +//! The handler method is `extractBuildStepsInfoHandler` and the method name is contained in +//! the `method_name` property. +//! +//! Upon successful extraction, the handler returns an array of type `BuildStepInfo`, +//! which is a simple structure containing the step's name and description, as defined +//! in the workspace's root `build.zig` file. + +const std = @import("std"); +const DocumentStore = @import("../DocumentStore.zig"); +const Server = @import("../Server.zig"); +const tracy = @import("tracy"); +const Uri = @import("../Uri.zig"); + +const log = std.log.scoped(.list_build_steps); + +/// The method name for the custom server endpoint. +pub const method_name = "customZig/listBuildSteps"; + +const Params = struct { + const workspace_uri_key = "workspaceUri"; + + workspace_uri: []const u8, + + pub fn fromJson(value: ?std.json.Value) ?@This() { + const v = value orelse return null; + if (v != .object) return null; + const workspace_uri_value = v.object.get(workspace_uri_key) orelse return null; + switch (workspace_uri_value) { + .string => |s| return .{ .workspace_uri = s }, + else => return null, + } + } +}; + +pub const BuildStepInfo = struct { + name: []const u8, + description: []const u8, + + pub fn init(arena: std.mem.Allocator, name: []const u8, description: []const u8) std.mem.Allocator.Error!@This() { + const allocated_name = try std.fmt.allocPrint(arena, "{s}", .{name}); + const allocated_description = try std.fmt.allocPrint(arena, "{s}", .{description}); + return .{ .name = allocated_name, .description = allocated_description }; + } +}; + +/// A request handler that extracts top-level build steps from the document +/// store using the workspace URI provided in the `params` object. +pub fn extractBuildStepsInfoHandler(server: *Server, arena: std.mem.Allocator, params: ?std.json.Value) error{ InvalidParams, OutOfMemory, Canceled }!std.ArrayList(BuildStepInfo) { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + if (!DocumentStore.supports_build_system) { + return .empty; + } + + var workspace_uri: Uri = undefined; + if (Params.fromJson(params)) |p| { + _ = blk: { + for (server.workspaces.items) |w| { + if (std.mem.eql(u8, w.uri.raw, p.workspace_uri)) { + workspace_uri = w.uri; + break :blk; + } + } + // log.debug("Could not find {s} in workspace list", .{p.workspace_uri}); + return error.InvalidParams; + }; + } else { + // log.debug("No information available regarding root 'build.zig' file, so returning empty array", .{}); + return error.InvalidParams; + } + + const build_file_path = try std.fmt.allocPrint(arena, "{s}/build.zig", .{workspace_uri.raw}); + const build_file_uri = Uri.parse(arena, build_file_path) catch |err| { + // log.err("Failed to parse build file path {s} as URI: {}", .{ build_file_path, err }); + switch (err) { + error.OutOfMemory => |e| return e, + else => return error.InvalidParams, + } + }; + + server.document_store.primeBuildFile(build_file_uri) catch |err| { + switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => { // `InvalidBuildFileUri` (should not be possible) or `DocumentStoreDoesNotSupportBuildSystem` (not possible at this point) + log.err("Failed to prime build file {s}: {}", .{ build_file_uri.raw, err }); + return .empty; + }, + } + }; + + try server.document_store.mutex.lock(server.io); + defer server.document_store.mutex.unlock(server.io); + + const build_file = server.document_store.build_files.get(build_file_uri) orelse { + return .empty; + }; + + var items: std.ArrayList(BuildStepInfo) = .empty; + + if (build_file.tryLockConfig(server.io)) |config| { + defer build_file.unlockConfig(server.io); + for (config.top_level_steps) |step_info| { + log.debug("Build step found in {s}: name = {s}; description = {s}", .{ build_file.uri.raw, step_info.name, step_info.description }); + const item: BuildStepInfo = try .init(arena, step_info.name, step_info.description); + try items.append(arena, item); + } + } else { + log.debug("Failed to lock build file for {s}", .{build_file.uri.raw}); + } + + return items; +} diff --git a/tests/build_runner_cases/add_module.json b/tests/build_runner_cases/add_module.json index bbd40dca0..1c710e4ff 100644 --- a/tests/build_runner_cases/add_module.json +++ b/tests/build_runner_cases/add_module.json @@ -9,8 +9,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/define_c_macro.json b/tests/build_runner_cases/define_c_macro.json index 080938f0e..bb1790d9c 100644 --- a/tests/build_runner_cases/define_c_macro.json +++ b/tests/build_runner_cases/define_c_macro.json @@ -11,8 +11,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/empty.json b/tests/build_runner_cases/empty.json index 8faf4b959..ad0bd1f31 100644 --- a/tests/build_runner_cases/empty.json +++ b/tests/build_runner_cases/empty.json @@ -3,8 +3,14 @@ "modules": {}, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/module_root_source_file_collision.json b/tests/build_runner_cases/module_root_source_file_collision.json index 33e14abbd..3682ab825 100644 --- a/tests/build_runner_cases/module_root_source_file_collision.json +++ b/tests/build_runner_cases/module_root_source_file_collision.json @@ -32,8 +32,14 @@ } ], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/module_self_import.json b/tests/build_runner_cases/module_self_import.json index 13e4c5c8a..ce8907e28 100644 --- a/tests/build_runner_cases/module_self_import.json +++ b/tests/build_runner_cases/module_self_import.json @@ -11,8 +11,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/multiple_module_import_names.json b/tests/build_runner_cases/multiple_module_import_names.json index 5072d1e2c..e73c09564 100644 --- a/tests/build_runner_cases/multiple_module_import_names.json +++ b/tests/build_runner_cases/multiple_module_import_names.json @@ -26,8 +26,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/no_root_source_file.json b/tests/build_runner_cases/no_root_source_file.json index 74269b1b8..f41fef4dd 100644 --- a/tests/build_runner_cases/no_root_source_file.json +++ b/tests/build_runner_cases/no_root_source_file.json @@ -16,8 +16,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file diff --git a/tests/build_runner_cases/public_module_with_generated_file.json b/tests/build_runner_cases/public_module_with_generated_file.json index f170bf379..169f8b8ad 100644 --- a/tests/build_runner_cases/public_module_with_generated_file.json +++ b/tests/build_runner_cases/public_module_with_generated_file.json @@ -9,8 +9,14 @@ }, "compilations": [], "top_level_steps": [ - "install", - "uninstall" + { + "name": "install", + "description": "Copy build artifacts to prefix path" + }, + { + "name": "uninstall", + "description": "Remove build artifacts from prefix path" + } ], "available_options": {} } \ No newline at end of file