Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/DocumentStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion src/build_runner/build_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
15 changes: 13 additions & 2 deletions src/build_runner/shared.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
115 changes: 115 additions & 0 deletions src/custom_features/list_build_steps.zig
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/add_module.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/define_c_macro.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/empty.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/module_root_source_file_collision.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/module_self_import.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/multiple_module_import_names.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/no_root_source_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
10 changes: 8 additions & 2 deletions tests/build_runner_cases/public_module_with_generated_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}