diff --git a/src/Server.zig b/src/Server.zig index 0a6632776..bf900ebaa 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -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, @@ -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{ @@ -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 => {}, } } @@ -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, }; } }; @@ -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) { @@ -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 = .{ @@ -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 => { @@ -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); @@ -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), }); @@ -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, }; @@ -1696,6 +1749,7 @@ pub const Message = union(enum) { .@"textDocument/didChange", .@"textDocument/didSave", .@"textDocument/didClose", + .@"workspace/didChangeWorkspaceFolders", .@"workspace/didChangeConfiguration", => return true, .unknown => return false, @@ -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); @@ -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, }; @@ -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); @@ -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); + } + } + }, } } diff --git a/src/features/diagnostics.zig b/src/features/diagnostics.zig index 967bce62a..df57fdff7 100644 --- a/src/features/diagnostics.zig +++ b/src/features/diagnostics.zig @@ -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"); @@ -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, diff --git a/src/lsp.zig b/src/lsp.zig index 9b8b9222f..205011bdb 100644 --- a/src/lsp.zig +++ b/src/lsp.zig @@ -2,7 +2,7 @@ const std = @import("std"); -const URI = []const u8; +pub const URI = []const u8; /// The URI of a document pub const DocumentUri = []const u8; /// A JavaScript regular expression; never used