Skip to content

Commit

Permalink
implement build on save diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
Techatrix authored and leecannon committed Aug 16, 2023
1 parent aeb0521 commit 895a888
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 6 deletions.
89 changes: 84 additions & 5 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ replay_enabled: bool = false,

// Code was based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig

const ClientCapabilities = packed struct {
const ClientCapabilities = struct {
supports_snippets: bool = false,
supports_apply_edits: bool = false,
supports_will_save: bool = false,
Expand All @@ -71,6 +71,13 @@ const ClientCapabilities = packed struct {
supports_configuration: bool = false,
supports_workspace_did_change_configuration_dynamic_registration: bool = false,
supports_textDocument_definition_linkSupport: bool = false,
workspace_folders: []types.URI = &.{},

fn deinit(self: *ClientCapabilities, allocator: std.mem.Allocator) void {
for (self.workspace_folders) |uri| allocator.free(uri);
allocator.free(self.workspace_folders);
self.* = undefined;
}
};

pub const Error = error{
Expand Down Expand Up @@ -129,12 +136,14 @@ const Job = union(enum) {
incoming_message: std.json.Parsed(Message),
generate_diagnostics: DocumentStore.Uri,
load_build_configuration: DocumentStore.Uri,
run_build_on_save,

fn deinit(self: Job, allocator: std.mem.Allocator) void {
switch (self) {
.incoming_message => |parsed_message| parsed_message.deinit(),
.generate_diagnostics => |uri| allocator.free(uri),
.load_build_configuration => |uri| allocator.free(uri),
.run_build_on_save => {},
}
}

Expand All @@ -153,7 +162,9 @@ const Job = union(enum) {
return switch (self) {
.incoming_message => |parsed_message| if (parsed_message.value.isBlocking()) .exclusive else .shared,
.generate_diagnostics => .shared,
.load_build_configuration => .atomic,
.load_build_configuration,
.run_build_on_save,
=> .atomic,
};
}
};
Expand Down Expand Up @@ -448,6 +459,14 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi
}
}

if (request.workspaceFolders) |workspace_folders| {
server.client_capabilities.workspace_folders = try server.allocator.alloc(types.URI, workspace_folders.len);
@memset(server.client_capabilities.workspace_folders, "");
for (server.client_capabilities.workspace_folders, workspace_folders) |*dest, src| {
dest.* = try server.allocator.dupe(u8, src.uri);
}
}

if (request.trace) |trace| {
// To support --enable-message-tracing, only allow turning this on here
if (trace != .off) {
Expand Down Expand Up @@ -549,8 +568,8 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi
.workspaceSymbolProvider = .{ .bool = false },
.workspace = .{
.workspaceFolders = .{
.supported = false,
.changeNotifications = .{ .bool = false },
.supported = true,
.changeNotifications = .{ .bool = true },
},
},
.semanticTokensProvider = .{
Expand Down Expand Up @@ -715,6 +734,33 @@ fn handleConfiguration(server: *Server, json: std.json.Value) error{OutOfMemory}
};
}

fn didChangeWorkspaceFoldersHandler(server: *Server, arena: std.mem.Allocator, notification: types.DidChangeWorkspaceFoldersParams) Error!void {
_ = arena;

var folders = std.ArrayListUnmanaged(types.URI).fromOwnedSlice(server.client_capabilities.workspace_folders);
errdefer folders.deinit(server.allocator);

var i: usize = 0;
while (i < folders.items.len) {
const uri = folders.items[i];
for (notification.event.removed) |removed| {
if (std.mem.eql(u8, removed.uri, uri)) {
server.allocator.free(folders.swapRemove(i));
break;
}
} else {
i += 1;
}
}

try folders.ensureUnusedCapacity(server.allocator, notification.event.added.len);
for (notification.event.added) |added| {
folders.appendAssumeCapacity(try server.allocator.dupe(u8, added.uri));
}

server.client_capabilities.workspace_folders = try folders.toOwnedSlice(server.allocator);
}

fn didChangeConfigurationHandler(server: *Server, arena: std.mem.Allocator, notification: types.DidChangeConfigurationParams) Error!void {
const settings = switch (notification.settings) {
.null => {
Expand Down Expand Up @@ -1080,6 +1126,10 @@ fn saveDocumentHandler(server: *Server, arena: std.mem.Allocator, notification:
});
}

if (std.process.can_spawn and server.config.enable_build_on_save) {
try server.pushJob(.run_build_on_save);
}

if (server.getAutofixMode() == .on_save) {
const handle = server.document_store.getHandle(uri) orelse return;
var text_edits = try server.autofix(arena, handle);
Expand Down Expand Up @@ -1282,7 +1332,9 @@ pub fn hoverHandler(server: *Server, arena: std.mem.Allocator, request: types.Ho
const response = hover_handler.hover(&analyser, arena, handle, source_index, markup_kind, server.offset_encoding);

// TODO: Figure out a better solution for comptime interpreter diags
if (server.client_capabilities.supports_publish_diagnostics) {
if (server.config.dangerous_comptime_experiments_do_not_enable and
server.client_capabilities.supports_publish_diagnostics)
{
try server.pushJob(.{
.generate_diagnostics = try server.allocator.dupe(u8, handle.uri),
});
Expand Down Expand Up @@ -1555,6 +1607,7 @@ pub const Message = union(enum) {
@"textDocument/didChange": types.DidChangeTextDocumentParams,
@"textDocument/didSave": types.DidSaveTextDocumentParams,
@"textDocument/didClose": types.DidCloseTextDocumentParams,
@"workspace/didChangeWorkspaceFolders": types.DidChangeWorkspaceFoldersParams,
@"workspace/didChangeConfiguration": types.DidChangeConfigurationParams,
unknown: []const u8,
};
Expand Down Expand Up @@ -1696,6 +1749,7 @@ pub const Message = union(enum) {
.@"textDocument/didChange",
.@"textDocument/didSave",
.@"textDocument/didClose",
.@"workspace/didChangeWorkspaceFolders",
.@"workspace/didChangeConfiguration",
=> return true,
.unknown => return false,
Expand Down Expand Up @@ -1761,6 +1815,7 @@ pub fn destroy(server: *Server) void {
server.job_queue.deinit();
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);
Expand Down Expand Up @@ -1893,6 +1948,7 @@ pub fn sendNotificationSync(server: *Server, arena: std.mem.Allocator, comptime
.@"textDocument/didChange" => try server.changeDocumentHandler(arena, params),
.@"textDocument/didSave" => try server.saveDocumentHandler(arena, params),
.@"textDocument/didClose" => try server.closeDocumentHandler(arena, params),
.@"workspace/didChangeWorkspaceFolders" => try server.didChangeWorkspaceFoldersHandler(arena, params),
.@"workspace/didChangeConfiguration" => try server.didChangeConfigurationHandler(arena, params),
.unknown => return,
};
Expand Down Expand Up @@ -1985,6 +2041,7 @@ fn processMessageReportError(server: *Server, message: Message) ?[]const u8 {
fn processJob(server: *Server, job: Job, wait_group: ?*std.Thread.WaitGroup) void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
tracy_zone.setName(@tagName(job));
defer if (!zig_builtin.single_threaded and wait_group != null) wait_group.?.finish();

defer job.deinit(server.allocator);
Expand All @@ -2007,6 +2064,28 @@ fn processJob(server: *Server, job: Job, wait_group: ?*std.Thread.WaitGroup) voi
if (!std.process.can_spawn) return;
server.document_store.invalidateBuildFile(build_file_uri) catch return;
},
.run_build_on_save => {
std.debug.assert(std.process.can_spawn);
if (!std.process.can_spawn) return;

for (server.client_capabilities.workspace_folders) |workspace_folder_uri| {
var arena_allocator = std.heap.ArenaAllocator.init(server.allocator);
defer arena_allocator.deinit();
var diagnostic_set = std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged(types.Diagnostic)){};
diagnostics_gen.generateBuildOnSaveDiagnostics(server, workspace_folder_uri, arena_allocator.allocator(), &diagnostic_set) catch |err| {
log.err("failed to run build on save on {s}: {}", .{ workspace_folder_uri, err });
};

for (diagnostic_set.keys(), diagnostic_set.values()) |document_uri, diagnostics| {
if (diagnostics.items.len == 0) continue;
const json_message = server.sendToClientNotification("textDocument/publishDiagnostics", .{
.uri = document_uri,
.diagnostics = diagnostics.items,
}) catch return;
server.allocator.free(json_message);
}
}
},
}
}

Expand Down
171 changes: 171 additions & 0 deletions src/features/diagnostics.zig
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const std = @import("std");
const builtin = @import("builtin");
const Ast = std.zig.Ast;
const log = std.log.scoped(.zls_diagnostics);

const Server = @import("../Server.zig");
const DocumentStore = @import("../DocumentStore.zig");
const BuildAssociatedConfig = @import("../BuildAssociatedConfig.zig");
const types = @import("../lsp.zig");
const Analyser = @import("../analysis.zig");
const ast = @import("../ast.zig");
const offsets = @import("../offsets.zig");
const URI = @import("../uri.zig");
const tracy = @import("../tracy.zig");

const Module = @import("../stage2/Module.zig");
Expand Down Expand Up @@ -179,6 +182,174 @@ pub fn generateDiagnostics(server: *Server, arena: std.mem.Allocator, handle: Do
};
}

pub fn generateBuildOnSaveDiagnostics(
server: *Server,
workspace_uri: types.URI,
arena: std.mem.Allocator,
diagnostics: *std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged(types.Diagnostic)),
) !void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
comptime std.debug.assert(std.process.can_spawn);

const workspace_path = URI.parse(server.allocator, workspace_uri) catch |err| {
log.err("failed to parse invalid uri `{s}`: {}", .{ workspace_uri, err });
return;
};
defer server.allocator.free(workspace_path);

std.debug.assert(std.fs.path.isAbsolute(workspace_path));

const build_zig_path = try std.fs.path.join(server.allocator, &.{ workspace_path, "build.zig" });
defer server.allocator.free(build_zig_path);

std.fs.accessAbsolute(build_zig_path, .{}) catch |err| switch (err) {
error.FileNotFound => return,
else => |e| {
log.err("failed to load build.zig at `{s}`: {}", .{ build_zig_path, e });
return e;
},
};

const base_args = &[_][]const u8{
server.config.zig_exe_path orelse return,
"build",
"--zig-lib-dir",
server.config.zig_lib_path orelse return,
"--cache-dir",
server.config.global_cache_path.?,
"-fno-reference-trace",
"--summary",
"none",
};

var argv = try std.ArrayListUnmanaged([]const u8).initCapacity(arena, base_args.len);
defer argv.deinit(arena);
argv.appendSliceAssumeCapacity(base_args);

blk: {
server.document_store.lock.lockShared();
defer server.document_store.lock.unlockShared();
const build_file = server.document_store.build_files.get(build_zig_path) orelse break :blk;
const build_associated_config = build_file.build_associated_config orelse break :blk;
const build_options = build_associated_config.value.build_options orelse break :blk;

try argv.ensureUnusedCapacity(arena, build_options.len);
for (build_options) |build_option| {
argv.appendAssumeCapacity(try build_option.formatParam(arena));
}
}

const result = blk: {
server.zig_exe_lock.lock();
defer server.zig_exe_lock.unlock();

break :blk std.ChildProcess.exec(.{
.allocator = server.allocator,
.argv = argv.items,
.cwd = workspace_path,
.max_output_bytes = 1024 * 1024,
}) catch |err| {
const joined = std.mem.join(server.allocator, " ", argv.items) catch return;
defer server.allocator.free(joined);
log.err("failed zig build command:\n{s}\nerror:{}\n", .{ joined, err });
return err;
};
};
defer server.allocator.free(result.stdout);
defer server.allocator.free(result.stderr);

switch (result.term) {
.Exited => |code| if (code == 0) return else {},
else => {
const joined = std.mem.join(server.allocator, " ", argv.items) catch return;
defer server.allocator.free(joined);
log.err("failed zig build command:\n{s}\nstderr:{s}\n\n", .{ joined, result.stderr });
},
}

var last_diagnostic_uri: ?types.URI = null;
var last_diagnostic: ?types.Diagnostic = null;
// we don't store DiagnosticRelatedInformation in last_diagnostic instead
// its stored in last_related_diagnostics because we need an ArrayList
var last_related_diagnostics: std.ArrayListUnmanaged(types.DiagnosticRelatedInformation) = .{};

// NOTE: I believe that with color off it's one diag per line; is this correct?
var line_iterator = std.mem.splitScalar(u8, result.stderr, '\n');

while (line_iterator.next()) |line| {
var pos_and_diag_iterator = std.mem.splitScalar(u8, line, ':');

const src_path = pos_and_diag_iterator.next() orelse continue;
const absolute_src_path = if (std.fs.path.isAbsolute(src_path)) src_path else blk: {
const absolute_src_path = std.fs.path.join(arena, &.{ workspace_path, src_path }) catch continue;
if (!std.fs.path.isAbsolute(absolute_src_path)) continue;
break :blk absolute_src_path;
};

const src_line = pos_and_diag_iterator.next() orelse continue;
const src_character = pos_and_diag_iterator.next() orelse continue;

// TODO zig uses utf-8 encoding for character offsets
// convert them to the desired offset encoding would require loading every file that contains errors
// is there some efficient way to do this?
const utf8_position = types.Position{
.line = (std.fmt.parseInt(u32, src_line, 10) catch continue) - 1,
.character = (std.fmt.parseInt(u32, src_character, 10) catch continue) - 1,
};
const range = types.Range{ .start = utf8_position, .end = utf8_position };

const msg = pos_and_diag_iterator.rest()[1..];

if (std.mem.startsWith(u8, msg, "note: ")) {
try last_related_diagnostics.append(arena, .{
.location = .{
.uri = try URI.fromPath(arena, absolute_src_path),
.range = range,
},
.message = try arena.dupe(u8, msg["note: ".len..]),
});
continue;
}

if (last_diagnostic) |*diagnostic| {
diagnostic.relatedInformation = try last_related_diagnostics.toOwnedSlice(arena);
const entry = try diagnostics.getOrPutValue(arena, last_diagnostic_uri.?, .{});
try entry.value_ptr.append(arena, diagnostic.*);
last_diagnostic_uri = null;
last_diagnostic = null;
}

if (std.mem.startsWith(u8, msg, "error: ")) {
last_diagnostic_uri = try URI.fromPath(arena, absolute_src_path);
last_diagnostic = types.Diagnostic{
.range = range,
.severity = .Error,
.code = .{ .string = "zig_build" },
.source = "zls",
.message = try arena.dupe(u8, msg["error: ".len..]),
};
} else {
last_diagnostic_uri = try URI.fromPath(arena, absolute_src_path);
last_diagnostic = types.Diagnostic{
.range = range,
.severity = .Error,
.code = .{ .string = "zig_build" },
.source = "zls",
.message = try arena.dupe(u8, msg),
};
}
}

if (last_diagnostic) |*diagnostic| {
diagnostic.relatedInformation = try last_related_diagnostics.toOwnedSlice(arena);
const entry = try diagnostics.getOrPutValue(arena, last_diagnostic_uri.?, .{});
try entry.value_ptr.append(arena, diagnostic.*);
last_diagnostic_uri = null;
last_diagnostic = null;
}
}

pub fn getAstCheckDiagnostics(
server: *Server,
arena: std.mem.Allocator,
Expand Down
Loading

0 comments on commit 895a888

Please sign in to comment.