roc/Server.zig
2025-11-30 03:43:48 -05:00

2014 lines
85 KiB
Zig

//! - Store global state
//! - The main loop
//! - Job/Request scheduling
//! - many Request handlers defined here. Except for the major ones which are in `src/features`
const Server = @This();
const std = @import("std");
const zig_builtin = @import("builtin");
const build_options = @import("build_options");
const Config = @import("Config.zig");
const configuration = @import("configuration.zig");
const DocumentStore = @import("DocumentStore.zig");
const lsp = @import("lsp");
const types = lsp.types;
const Analyser = @import("analysis.zig");
const offsets = @import("offsets.zig");
const tracy = @import("tracy");
const diff = @import("diff.zig");
const Uri = @import("Uri.zig");
const InternPool = @import("analyser/analyser.zig").InternPool;
const DiagnosticsCollection = @import("DiagnosticsCollection.zig");
const build_runner_shared = @import("build_runner/shared.zig");
const signature_help = @import("features/signature_help.zig");
const references = @import("features/references.zig");
const semantic_tokens = @import("features/semantic_tokens.zig");
const inlay_hints = @import("features/inlay_hints.zig");
const code_actions = @import("features/code_actions.zig");
const folding_range = @import("features/folding_range.zig");
const document_symbol = @import("features/document_symbol.zig");
const completions = @import("features/completions.zig");
const goto = @import("features/goto.zig");
const hover_handler = @import("features/hover.zig");
const selection_range = @import("features/selection_range.zig");
const diagnostics_gen = @import("features/diagnostics.zig");
const BuildOnSave = diagnostics_gen.BuildOnSave;
const BuildOnSaveSupport = build_runner_shared.BuildOnSaveSupport;
const log = std.log.scoped(.server);
// public fields
io: std.Io,
allocator: std.mem.Allocator,
config_manager: configuration.Manager,
document_store: DocumentStore,
transport: ?*lsp.Transport = null,
offset_encoding: offsets.Encoding = .@"utf-16",
status: Status = .uninitialized,
// private fields
thread_pool: std.Thread.Pool,
wait_group: std.Thread.WaitGroup = .{},
ip: InternPool = undefined,
/// avoid Zig deadlocking when spawning multiple `zig ast-check` processes at the same time.
/// See https://github.com/ziglang/zig/issues/16369
zig_ast_check_lock: std.Thread.Mutex = .{},
/// Stores messages that should be displayed with `window/showMessage` once the server has been initialized.
pending_show_messages: std.ArrayList(types.window.ShowMessageParams) = .empty,
client_capabilities: ClientCapabilities = .{},
diagnostics_collection: DiagnosticsCollection,
workspaces: std.ArrayList(Workspace) = .empty,
// Code was based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig
const ClientCapabilities = struct {
supports_snippets: bool = false,
supports_apply_edits: bool = false,
supports_will_save_wait_until: bool = false,
supports_publish_diagnostics: bool = false,
supports_code_action_fixall: bool = false,
supports_semantic_tokens_overlapping: bool = false,
hover_supports_md: bool = false,
signature_help_supports_md: bool = false,
completion_doc_supports_md: bool = false,
supports_completion_insert_replace_support: bool = false,
/// deprecated can be marked through the `CompletionItem.deprecated` field
supports_completion_deprecated_old: bool = false,
/// deprecated can be marked through the `CompletionItem.tags` field
supports_completion_deprecated_tag: bool = false,
label_details_support: bool = false,
/// The client supports `workspace/configuration` requests.
supports_configuration: bool = false,
/// The client supports dynamically registering for the `workspace/didChangeConfiguration` notification.
supports_workspace_did_change_configuration_dynamic_registration: bool = false,
/// The client supports dynamically registering for the `workspace/didChangeWatchedFiles` notification.
supports_workspace_did_change_watched_files: bool = false,
supports_textDocument_definition_linkSupport: bool = false,
/// The detail entries for big structs such as std.zig.CrossTarget were
/// bricking the preview window in Sublime Text.
/// https://github.com/zigtools/zls/pull/261
max_detail_length: u32 = 1024 * 1024,
client_name: ?[]const u8 = null,
fn deinit(self: *ClientCapabilities, allocator: std.mem.Allocator) void {
if (self.client_name) |name| allocator.free(name);
self.* = undefined;
}
};
pub const Error = error{
OutOfMemory,
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
InternalError,
/// Error code indicating that a server received a notification or
/// request before the server has received the `initialize` request.
ServerNotInitialized,
/// A request failed but it was syntactically correct, e.g the
/// method name was known and the parameters were valid. The error
/// message should contain human readable information about why
/// the request failed.
///
/// @since 3.17.0
RequestFailed,
/// The server cancelled the request. This error code should
/// only be used for requests that explicitly support being
/// server cancellable.
///
/// @since 3.17.0
ServerCancelled,
/// The server detected that the content of a document got
/// modified outside normal conditions. A server should
/// NOT send this error code if it detects a content change
/// in it unprocessed messages. The result even computed
/// on an older state might still be useful for the client.
///
/// If a client decides that a result is not of any use anymore
/// the client should cancel the request.
ContentModified,
/// The client has canceled a request and a server as detected
/// the cancel.
RequestCancelled,
};
pub const Status = enum {
/// the server has not received a `initialize` request
uninitialized,
/// the server has received a `initialize` request and is awaiting the `initialized` notification
initializing,
/// the server has been initialized and is ready to received requests
initialized,
/// the server has been shutdown and can't handle any more requests
shutdown,
/// the server is received a `exit` notification and has been shutdown
exiting_success,
/// the server is received a `exit` notification but has not been shutdown
exiting_failure,
};
fn sendToClientResponse(server: *Server, id: lsp.JsonRPCMessage.ID, result: anytype) error{OutOfMemory}![]u8 {
const tracy_zone = tracy.traceNamed(@src(), "sendToClientResponse(" ++ @typeName(@TypeOf(result)) ++ ")");
defer tracy_zone.end();
// TODO validate result type is a possible response
// TODO validate response is from a client to server request
// TODO validate result type
const response: lsp.TypedJsonRPCResponse(@TypeOf(result)) = .{
.id = id,
.result_or_error = .{ .result = result },
};
return try sendToClientInternal(server.allocator, server.transport, response);
}
fn sendToClientRequest(server: *Server, id: lsp.JsonRPCMessage.ID, method: []const u8, params: anytype) error{OutOfMemory}![]u8 {
const tracy_zone = tracy.traceNamed(@src(), "sendToClientRequest(" ++ @typeName(@TypeOf(params)) ++ ")");
defer tracy_zone.end();
// TODO validate method is a request
// TODO validate method is server to client
// TODO validate params type
const request: lsp.TypedJsonRPCRequest(@TypeOf(params)) = .{
.id = id,
.method = method,
.params = params,
};
return try sendToClientInternal(server.allocator, server.transport, request);
}
fn sendToClientNotification(server: *Server, method: []const u8, params: anytype) error{OutOfMemory}![]u8 {
const tracy_zone = tracy.traceNamed(@src(), "sendToClientRequest(" ++ @typeName(@TypeOf(params)) ++ ")");
defer tracy_zone.end();
// TODO validate method is a notification
// TODO validate method is server to client
// TODO validate params type
const notification: lsp.TypedJsonRPCNotification(@TypeOf(params)) = .{
.method = method,
.params = params,
};
return try sendToClientInternal(server.allocator, server.transport, notification);
}
fn sendToClientResponseError(server: *Server, id: lsp.JsonRPCMessage.ID, err: lsp.JsonRPCMessage.Response.Error) error{OutOfMemory}![]u8 {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
const response: lsp.JsonRPCMessage = .{
.response = .{ .id = id, .result_or_error = .{ .@"error" = err } },
};
return try sendToClientInternal(server.allocator, server.transport, response);
}
fn sendToClientInternal(allocator: std.mem.Allocator, transport: ?*lsp.Transport, message: anytype) error{OutOfMemory}![]u8 {
const message_stringified = try std.json.Stringify.valueAlloc(allocator, message, .{
.emit_null_optional_fields = false,
});
errdefer allocator.free(message_stringified);
if (transport) |t| {
const tracy_zone = tracy.traceNamed(@src(), "Transport.writeJsonMessage");
defer tracy_zone.end();
t.writeJsonMessage(message_stringified) catch |err| {
log.err("failed to write message: {}", .{err});
};
}
return message_stringified;
}
/// Send a `window/showMessage` notification to the client that will display a message in the user interface.
pub fn showMessage(
server: *Server,
message_type: types.window.MessageType,
comptime fmt: []const u8,
args: anytype,
) void {
var message = std.fmt.allocPrint(server.allocator, fmt, args) catch return;
defer server.allocator.free(message);
switch (message_type) {
.Error => log.err("{s}", .{message}),
.Warning => log.warn("{s}", .{message}),
.Info => log.info("{s}", .{message}),
.Log, .Debug => log.debug("{s}", .{message}),
_ => log.debug("{s}", .{message}),
}
switch (server.status) {
.uninitialized => {
server.pending_show_messages.ensureUnusedCapacity(server.allocator, 1) catch return;
server.pending_show_messages.appendAssumeCapacity(.{
.type = message_type,
.message = message,
});
message = "";
return;
},
.initializing,
.initialized,
=> {},
.shutdown,
.exiting_success,
.exiting_failure,
=> return,
}
if (server.sendToClientNotification("window/showMessage", types.window.ShowMessageParams{
.type = message_type,
.message = message,
})) |json_message| {
server.allocator.free(json_message);
} else |err| {
log.warn("failed to show message: {}", .{err});
}
}
pub fn initAnalyser(server: *Server, arena: std.mem.Allocator, handle: ?*DocumentStore.Handle) Analyser {
return .init(
server.allocator,
arena,
&server.document_store,
&server.ip,
handle,
);
}
/// If `force_autofix` is enabled, implement autofix without relying on a `source.fixall` code action.
pub fn autofixWorkaround(server: *Server) enum {
/// Autofix is implemented using `textDocument/willSaveWaitUntil`.
will_save_wait_until,
/// Autofix is implemented by send a `workspace/applyEdit` request after receiving a `textDocument/didSave` notification.
on_save,
/// No workaround implementation of autofix is possible.
unavailable,
/// The `force_autofix` config option is disabled.
none,
} {
if (!server.config_manager.config.force_autofix) return .none;
if (server.client_capabilities.supports_will_save_wait_until) return .will_save_wait_until;
if (server.client_capabilities.supports_apply_edits) return .on_save;
return .unavailable;
}
/// caller owns returned memory.
fn autofix(server: *Server, arena: std.mem.Allocator, handle: *DocumentStore.Handle) error{OutOfMemory}!std.ArrayList(types.TextEdit) {
if (handle.tree.errors.len != 0) return .empty;
if (handle.tree.mode == .zon) return .empty;
var error_bundle = try diagnostics_gen.getAstCheckDiagnostics(server, handle);
defer error_bundle.deinit(server.allocator);
if (error_bundle.errorMessageCount() == 0) return .empty;
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
var builder: code_actions.Builder = .{
.arena = arena,
.analyser = &analyser,
.handle = handle,
.offset_encoding = server.offset_encoding,
.only_kinds = .init(.{
.@"source.fixAll" = true,
}),
};
try builder.generateCodeAction(error_bundle);
for (builder.actions.items) |action| {
std.debug.assert(action.kind.?.eql(.@"source.fixAll")); // We request only source.fixall code actions
}
defer builder.fixall_text_edits = .empty;
return builder.fixall_text_edits;
}
fn generateDiagnostics(server: *Server, handle: *DocumentStore.Handle) void {
if (!server.client_capabilities.supports_publish_diagnostics) return;
const do = struct {
fn do(param_server: *Server, param_handle: *DocumentStore.Handle) void {
diagnostics_gen.generateDiagnostics(param_server, param_handle) catch |err| switch (err) {
error.OutOfMemory => {},
};
}
}.do;
server.thread_pool.spawnWg(&server.wait_group, do, .{ server, handle });
}
fn initializeHandler(server: *Server, arena: std.mem.Allocator, request: types.InitializeParams) Error!types.InitializeResult {
var support_full_semantic_tokens = true;
if (request.clientInfo) |clientInfo| {
server.client_capabilities.client_name = try server.allocator.dupe(u8, clientInfo.name);
if (std.mem.startsWith(u8, clientInfo.name, "Visual Studio Code") or
std.mem.startsWith(u8, clientInfo.name, "VSCodium") or
std.mem.startsWith(u8, clientInfo.name, "Code - OSS"))
{
// VS Code doesn't really utilize `textDocument/semanticTokens/range`.
// This will cause some visual artifacts when scrolling through the
// document quickly but will considerably improve performance
// especially on large files.
support_full_semantic_tokens = false;
} else if (std.mem.eql(u8, clientInfo.name, "Sublime Text LSP")) {
server.client_capabilities.max_detail_length = 256;
} else if (std.mem.startsWith(u8, clientInfo.name, "emacs")) {
// Assumes that `emacs` means `emacs-lsp/lsp-mode`. Eglot uses `Eglot`.
}
}
if (request.capabilities.general) |general| {
if (general.positionEncodings) |position_encodings| {
server.offset_encoding = outer: for (position_encodings) |encoding| {
switch (encoding) {
.@"utf-8" => break :outer .@"utf-8",
.@"utf-16" => break :outer .@"utf-16",
.@"utf-32" => break :outer .@"utf-32",
.custom_value => {},
}
} else server.offset_encoding;
}
}
server.diagnostics_collection.offset_encoding = server.offset_encoding;
if (request.capabilities.textDocument) |textDocument| {
server.client_capabilities.supports_publish_diagnostics = textDocument.publishDiagnostics != null;
if (textDocument.hover) |hover| {
if (hover.contentFormat) |content_format| {
for (content_format) |format| {
if (format == .plaintext) {
break;
}
if (format == .markdown) {
server.client_capabilities.hover_supports_md = true;
break;
}
}
}
}
if (textDocument.completion) |completion| {
if (completion.completionItem) |completionItem| {
server.client_capabilities.label_details_support = completionItem.labelDetailsSupport orelse false;
server.client_capabilities.supports_snippets = completionItem.snippetSupport orelse false;
server.client_capabilities.supports_completion_deprecated_old = completionItem.deprecatedSupport orelse false;
server.client_capabilities.supports_completion_insert_replace_support = completionItem.insertReplaceSupport orelse false;
if (completionItem.tagSupport) |tagSupport| {
for (tagSupport.valueSet) |tag| {
switch (tag) {
.Deprecated => {
server.client_capabilities.supports_completion_deprecated_tag = true;
break;
},
_ => {},
}
}
}
if (completionItem.documentationFormat) |documentation_format| {
for (documentation_format) |format| {
if (format == .plaintext) {
break;
}
if (format == .markdown) {
server.client_capabilities.completion_doc_supports_md = true;
break;
}
}
}
}
}
if (textDocument.synchronization) |synchronization| {
server.client_capabilities.supports_will_save_wait_until = synchronization.willSaveWaitUntil orelse false;
}
if (textDocument.definition) |definition| {
server.client_capabilities.supports_textDocument_definition_linkSupport = definition.linkSupport orelse false;
}
if (textDocument.signatureHelp) |signature_help_capabilities| {
if (signature_help_capabilities.signatureInformation) |signature_information| {
if (signature_information.documentationFormat) |content_format| {
for (content_format) |format| {
if (format == .plaintext) {
break;
}
if (format == .markdown) {
server.client_capabilities.signature_help_supports_md = true;
break;
}
}
}
}
}
if (textDocument.semanticTokens) |semanticTokens| {
server.client_capabilities.supports_semantic_tokens_overlapping = semanticTokens.overlappingTokenSupport orelse false;
}
}
if (request.capabilities.window) |window| {
if (window.workDoneProgress) |wdp| {
server.document_store.lsp_capabilities.supports_work_done_progress = wdp;
}
}
if (request.capabilities.workspace) |workspace| {
server.client_capabilities.supports_apply_edits = workspace.applyEdit orelse false;
server.client_capabilities.supports_configuration = workspace.configuration orelse false;
if (workspace.didChangeConfiguration) |did_change| {
if (did_change.dynamicRegistration orelse false) {
server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration = true;
}
}
if (workspace.didChangeWatchedFiles) |did_change| {
if (did_change.dynamicRegistration orelse false) {
server.client_capabilities.supports_workspace_did_change_watched_files = true;
}
}
if (workspace.semanticTokens) |workspace_semantic_tokens| {
server.document_store.lsp_capabilities.supports_semantic_tokens_refresh = workspace_semantic_tokens.refreshSupport orelse false;
}
if (workspace.inlayHint) |inlay_hint| {
server.document_store.lsp_capabilities.supports_inlay_hints_refresh = inlay_hint.refreshSupport orelse false;
}
}
if (request.clientInfo) |clientInfo| {
log.info("Client Info: {s} ({s})", .{ clientInfo.name, clientInfo.version orelse "unknown version" });
}
log.debug("Offset Encoding: '{t}'", .{server.offset_encoding});
if (request.workspaceFolders) |workspace_folders| {
for (workspace_folders) |src| {
const uri = Uri.parse(arena, src.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
try server.addWorkspace(uri);
}
}
server.status = .initializing;
{
for (server.pending_show_messages.items) |params| {
if (server.sendToClientNotification("window/showMessage", params)) |json_message| {
server.allocator.free(json_message);
} else |err| {
log.warn("failed to show message: {}", .{err});
}
}
for (server.pending_show_messages.items) |params| server.allocator.free(params.message);
server.pending_show_messages.clearAndFree(server.allocator);
}
if (request.initializationOptions) |initialization_options| {
if (std.json.parseFromValueLeaky(configuration.UnresolvedConfig, arena, initialization_options, .{
.ignore_unknown_fields = true,
})) |*new_cfg| {
try server.config_manager.setConfiguration(.lsp_initialization, new_cfg);
if (server.client_capabilities.supports_configuration) {
// Do not resolve configuration until we received `workspace/configuration`.
} else {
try server.resolveConfiguration();
}
} else |err| {
log.err("failed to read initialization_options: {}", .{err});
}
}
return .{
.serverInfo = .{
.name = "zls",
.version = build_options.version_string,
},
.capabilities = .{
.positionEncoding = switch (server.offset_encoding) {
.@"utf-8" => .@"utf-8",
.@"utf-16" => .@"utf-16",
.@"utf-32" => .@"utf-32",
},
.signatureHelpProvider = .{
.triggerCharacters = &.{"("},
.retriggerCharacters = &.{","},
},
.textDocumentSync = .{
.text_document_sync_options = .{
.openClose = true,
.change = .Incremental,
.save = .{ .bool = true },
.willSaveWaitUntil = true,
},
},
.renameProvider = .{
.rename_options = .{ .prepareProvider = true },
},
.completionProvider = .{
.resolveProvider = false,
.triggerCharacters = &.{ ".", ":", "@", "]", "\"", "/" },
.completionItem = .{ .labelDetailsSupport = true },
},
.documentHighlightProvider = .{ .bool = true },
.hoverProvider = .{ .bool = true },
.codeActionProvider = .{ .code_action_options = .{ .codeActionKinds = code_actions.supported_code_actions } },
.declarationProvider = .{ .bool = true },
.definitionProvider = .{ .bool = true },
.typeDefinitionProvider = .{ .bool = true },
.implementationProvider = .{ .bool = false },
.referencesProvider = .{ .bool = true },
.documentSymbolProvider = .{ .bool = true },
.colorProvider = .{ .bool = false },
.documentFormattingProvider = .{ .bool = true },
.documentRangeFormattingProvider = .{ .bool = false },
.foldingRangeProvider = .{ .bool = true },
.selectionRangeProvider = .{ .bool = true },
.workspaceSymbolProvider = .{ .bool = false },
.workspace = .{
.workspaceFolders = .{
.supported = true,
.changeNotifications = .{ .bool = true },
},
},
.semanticTokensProvider = .{
.semantic_tokens_options = .{
.full = .{ .bool = support_full_semantic_tokens },
.range = .{ .bool = true },
.legend = .{
.tokenTypes = std.meta.fieldNames(semantic_tokens.TokenType),
.tokenModifiers = std.meta.fieldNames(semantic_tokens.TokenModifiers),
},
},
},
.inlayHintProvider = .{ .bool = true },
},
};
}
fn initializedHandler(server: *Server, arena: std.mem.Allocator, notification: types.InitializedParams) Error!void {
_ = notification;
if (server.status != .initializing) {
log.warn("received a initialized notification but the server has not send a initialize request!", .{});
}
server.status = .initialized;
if (server.client_capabilities.supports_configuration and
server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration)
{
try server.registerCapability("workspace/didChangeConfiguration", null);
}
if (server.client_capabilities.supports_workspace_did_change_watched_files) {
// `{ "watchers": [ { "globPattern": "**/*.{zig,zon}" } ] }`
var watcher: std.json.ObjectMap = .init(arena);
try watcher.putNoClobber("globPattern", .{ .string = "**/*.{zig,zon}" });
var watchers_arr: std.json.Array = try .initCapacity(arena, 1);
watchers_arr.appendAssumeCapacity(.{ .object = watcher });
var fs_watcher_obj: std.json.ObjectMap = .init(arena);
try fs_watcher_obj.putNoClobber("watchers", .{ .array = watchers_arr });
const json_val: std.json.Value = .{ .object = fs_watcher_obj };
try server.registerCapability("workspace/didChangeWatchedFiles", json_val);
}
if (server.client_capabilities.supports_configuration) {
// We defer calling `server.resolveConfiguration()` until after workspace configuration has been received.
try server.requestConfiguration();
} else {
// The client does not support the `workspace/configuration` (pull model) request
// and it is unknown whether the client will use the
// `workspace/didChangeConfiguration` (push model) notification instead.
// In case they don't, we resolve configuration early and re-resolve if push model is used.
try server.resolveConfiguration();
}
if (std.crypto.random.intRangeLessThan(usize, 0, 32768) == 0) {
server.showMessage(.Warning, "HELP ME, I AM STUCK INSIDE AN LSP!", .{});
}
}
fn shutdownHandler(server: *Server, _: std.mem.Allocator, _: void) Error!?void {
defer server.status = .shutdown;
if (server.status != .initialized) return error.InvalidRequest; // received a shutdown request but the server is not initialized!
}
fn exitHandler(server: *Server, _: std.mem.Allocator, _: void) Error!void {
server.status = switch (server.status) {
.initialized => .exiting_failure,
.shutdown => .exiting_success,
else => unreachable,
};
}
fn registerCapability(server: *Server, method: []const u8, registersOptions: ?types.LSPAny) Error!void {
const id = try std.fmt.allocPrint(server.allocator, "register-{s}", .{method});
defer server.allocator.free(id);
log.debug("Dynamically registering method '{s}'", .{method});
const json_message = try server.sendToClientRequest(
.{ .string = id },
"client/registerCapability",
types.Registration.Params{ .registrations = &.{
.{
.id = id,
.method = method,
.registerOptions = registersOptions,
},
} },
);
server.allocator.free(json_message);
}
/// Request configuration options with the `workspace/configuration` request.
fn requestConfiguration(server: *Server) Error!void {
const configuration_items: [1]types.workspace.configuration.Item = .{
.{
.section = "zls",
.scopeUri = if (server.workspaces.items.len == 1) server.workspaces.items[0].uri.raw else null,
},
};
const json_message = try server.sendToClientRequest(
.{ .string = "i_haz_configuration" },
"workspace/configuration",
types.workspace.configuration.Params{
.items = &configuration_items,
},
);
server.allocator.free(json_message);
}
/// Handle the response of the `workspace/configuration` request.
fn handleConfiguration(server: *Server, json: std.json.Value) error{OutOfMemory}!void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
const result: std.json.Value = switch (json) {
.array => |arr| blk: {
if (arr.items.len != 1) {
log.err("Response to 'workspace/configuration' expects an array of size 1 but received {d}", .{arr.items.len});
break :blk null;
}
break :blk switch (arr.items[0]) {
.object => arr.items[0],
.null => null,
else => {
log.err("Response to 'workspace/configuration' expects an array of objects but got an array of {t}.", .{json});
break :blk null;
},
};
},
else => blk: {
log.err("Response to 'workspace/configuration' expects an array but received {t}", .{json});
break :blk null;
},
} orelse {
try server.resolveConfiguration();
return;
};
var arena_allocator: std.heap.ArenaAllocator = .init(server.allocator);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
var new_config = std.json.parseFromValueLeaky(
configuration.UnresolvedConfig,
arena,
result,
.{ .ignore_unknown_fields = true },
) catch |err| {
log.err("Failed to parse response from 'workspace/configuration': {}", .{err});
try server.resolveConfiguration();
return;
};
const maybe_root_dir: ?[]const u8 = dir: {
if (server.workspaces.items.len != 1) break :dir null;
const workspace = server.workspaces.items[0];
break :dir workspace.uri.toFsPath(arena) catch |err| {
log.err("failed to parse root uri for workspace {s}: {}", .{ workspace.uri.raw, err });
break :dir null;
};
};
inline for (configuration.file_system_config_options) |file_config| {
const field: *?[]const u8 = &@field(new_config, file_config.name);
if (field.*) |maybe_relative| resolve: {
if (maybe_relative.len == 0) break :resolve;
if (std.fs.path.isAbsolute(maybe_relative)) break :resolve;
const root_dir = maybe_root_dir orelse {
log.err("relative path only supported for {s} with exactly one workspace", .{file_config.name});
break;
};
const absolute = try std.fs.path.resolve(arena, &.{
root_dir, maybe_relative,
});
field.* = absolute;
}
}
try server.config_manager.setConfiguration(.lsp_configuration, &new_config);
try server.resolveConfiguration();
}
const Workspace = struct {
uri: Uri,
build_on_save: if (BuildOnSaveSupport.isSupportedComptime()) ?BuildOnSave else void,
build_on_save_mode: if (BuildOnSaveSupport.isSupportedComptime()) ?enum { watch, manual } else void,
fn init(server: *Server, uri: Uri) error{OutOfMemory}!Workspace {
const duped_uri = try uri.dupe(server.allocator);
errdefer duped_uri.deinit(server.allocator);
return .{
.uri = duped_uri,
.build_on_save = if (BuildOnSaveSupport.isSupportedComptime()) null else {},
.build_on_save_mode = if (BuildOnSaveSupport.isSupportedComptime()) null else {},
};
}
fn deinit(workspace: *Workspace, allocator: std.mem.Allocator) void {
if (BuildOnSaveSupport.isSupportedComptime()) {
if (workspace.build_on_save) |*build_on_save| build_on_save.deinit();
}
workspace.uri.deinit(allocator);
}
fn sendManualWatchUpdate(workspace: *Workspace) void {
comptime std.debug.assert(BuildOnSaveSupport.isSupportedComptime());
const build_on_save = if (workspace.build_on_save) |*build_on_save| build_on_save else return;
const mode = workspace.build_on_save_mode orelse return;
if (mode != .manual) return;
build_on_save.sendManualWatchUpdate();
}
fn refreshBuildOnSave(workspace: *Workspace, args: struct {
server: *Server,
/// Whether the build on save process should be restarted if it is already running.
restart: bool,
}) error{OutOfMemory}!void {
comptime std.debug.assert(BuildOnSaveSupport.isSupportedComptime());
const config = &args.server.config_manager.config;
if (args.server.config_manager.zig_exe) |zig_exe| {
workspace.build_on_save_mode = switch (BuildOnSaveSupport.isSupportedRuntime(zig_exe.version)) {
.supported => .watch,
// If if build on save has been explicitly enabled, fallback to the implementation with manual updates
else => if (config.enable_build_on_save orelse false) .manual else null,
};
} else {
workspace.build_on_save_mode = null;
}
const build_on_save_supported = workspace.build_on_save_mode != null;
const build_on_save_wanted = config.enable_build_on_save orelse true;
const enable = build_on_save_supported and build_on_save_wanted;
if (workspace.build_on_save) |*build_on_save| {
if (enable and !args.restart) return;
log.debug("stopped Build-On-Save for '{s}'", .{workspace.uri.raw});
build_on_save.deinit();
workspace.build_on_save = null;
}
if (!enable) return;
const zig_exe_path = config.zig_exe_path orelse return;
const zig_lib_path = config.zig_lib_path orelse return;
const build_runner_path = config.build_runner_path orelse return;
const workspace_path = workspace.uri.toFsPath(args.server.allocator) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.UnsupportedScheme => return,
};
defer args.server.allocator.free(workspace_path);
std.debug.assert(workspace.build_on_save == null);
workspace.build_on_save = BuildOnSave.init(.{
.io = args.server.io,
.allocator = args.server.allocator,
.workspace_path = workspace_path,
.build_on_save_args = config.build_on_save_args,
.check_step_only = config.enable_build_on_save == null,
.zig_exe_path = zig_exe_path,
.zig_lib_path = zig_lib_path,
.build_runner_path = build_runner_path,
.collection = &args.server.diagnostics_collection,
}) catch |err| {
log.err("failed to initilize Build-On-Save for '{s}': {}", .{ workspace.uri.raw, err });
return;
};
log.info("trying to start Build-On-Save for '{s}'", .{workspace.uri.raw});
}
};
fn addWorkspace(server: *Server, uri: Uri) error{OutOfMemory}!void {
try server.workspaces.ensureUnusedCapacity(server.allocator, 1);
server.workspaces.appendAssumeCapacity(try Workspace.init(server, uri));
log.info("added Workspace Folder: {s}", .{uri.raw});
if (BuildOnSaveSupport.isSupportedComptime() and
// Don't initialize build on save until initialization finished.
// If the client supports the `workspace/configuration` request, wait
// until we have received workspace configuration from the server.
(server.status == .initialized and !server.client_capabilities.supports_configuration))
{
try server.workspaces.items[server.workspaces.items.len - 1].refreshBuildOnSave(.{
.server = server,
.restart = false,
});
}
}
fn removeWorkspace(server: *Server, uri: Uri) void {
for (server.workspaces.items, 0..) |workspace, i| {
if (workspace.uri.eql(uri)) {
var removed_workspace = server.workspaces.swapRemove(i);
removed_workspace.deinit(server.allocator);
log.info("removed Workspace Folder: {s}", .{uri.raw});
break;
}
} else {
log.warn("could not remove Workspace Folder: {s}", .{uri.raw});
}
}
fn didChangeWatchedFilesHandler(server: *Server, arena: std.mem.Allocator, notification: types.workspace.did_change_watched_files.Params) Error!void {
var updated_files: usize = 0;
for (notification.changes) |change| {
const uri = Uri.parse(arena, change.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const file_extension = std.fs.path.extension(uri.raw);
if (!std.mem.eql(u8, file_extension, ".zig") and !std.mem.eql(u8, file_extension, ".zon")) continue;
switch (change.type) {
.Created, .Changed, .Deleted => |kind| {
const did_update_file = try server.document_store.refreshDocumentFromFileSystem(uri, kind == .Deleted);
updated_files += @intFromBool(did_update_file);
},
else => {},
}
}
if (updated_files != 0) {
log.debug("updated {d} watched file(s)", .{updated_files});
}
}
fn didChangeWorkspaceFoldersHandler(server: *Server, arena: std.mem.Allocator, notification: types.workspace.folders.DidChangeParams) Error!void {
for (notification.event.added) |folder| {
const uri = Uri.parse(arena, folder.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
try server.addWorkspace(uri);
}
for (notification.event.removed) |folder| {
const uri = Uri.parse(arena, folder.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
server.removeWorkspace(uri);
}
}
fn didChangeConfigurationHandler(server: *Server, arena: std.mem.Allocator, notification: types.workspace.configuration.did_change.Params) Error!void {
const settings = switch (notification.settings) {
.null => {
if (server.client_capabilities.supports_configuration and
server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration)
{
// The client has informed us that the configuration options have
// changed. The will request them with `workspace/configuration`.
try server.requestConfiguration();
}
return;
},
.object => |object| blk: {
if (server.client_capabilities.supports_configuration and
server.client_capabilities.supports_workspace_did_change_configuration_dynamic_registration)
{
log.debug("Ignoring 'workspace/didChangeConfiguration' notification in favor of 'workspace/configuration'", .{});
try server.requestConfiguration();
return;
}
break :blk object.get("zls") orelse notification.settings;
},
else => notification.settings, // We will definitely fail to parse this
};
const new_config = std.json.parseFromValueLeaky(
configuration.UnresolvedConfig,
arena,
settings,
.{ .ignore_unknown_fields = true },
) catch |err| {
log.err("failed to parse 'workspace/didChangeConfiguration' response: {}", .{err});
return error.ParseError;
};
try server.config_manager.setConfiguration(.lsp_configuration, &new_config);
try server.resolveConfiguration();
}
pub fn resolveConfiguration(server: *Server) error{OutOfMemory}!void {
var result = try server.config_manager.resolveConfiguration(server.allocator);
defer result.deinit(server.allocator);
for (result.messages) |msg| {
server.showMessage(.Error, "{s}", .{msg});
}
inline for (std.meta.fields(Config)) |field| {
if (@field(result.did_change, field.name)) {
const new_value = @field(server.config_manager.config, field.name);
log.info("Set config option '{s}' to {f}", .{ field.name, std.json.fmt(new_value, .{}) });
}
}
const new_zig_exe_path: bool = result.did_change.zig_exe_path;
const new_zig_lib_path: bool = result.did_change.zig_lib_path;
const new_build_runner_path: bool = result.did_change.build_runner_path;
const new_enable_build_on_save: bool = result.did_change.enable_build_on_save;
const new_build_on_save_args: bool = result.did_change.build_on_save_args;
const new_force_autofix: bool = result.did_change.force_autofix;
server.document_store.config = createDocumentStoreConfig(&server.config_manager);
if (BuildOnSaveSupport.isSupportedComptime() and
// If the client supports the `workspace/configuration` request, defer
// build on save initialization until after we have received workspace
// configuration from the server
(!server.client_capabilities.supports_configuration or server.status == .initialized))
{
const should_restart =
new_zig_exe_path or
new_zig_lib_path or
new_build_runner_path or
new_enable_build_on_save or
new_build_on_save_args;
for (server.workspaces.items) |*workspace| {
try workspace.refreshBuildOnSave(.{
.server = server,
.restart = should_restart,
});
}
}
if (DocumentStore.supports_build_system) {
if (new_zig_exe_path or new_zig_lib_path or new_build_runner_path) {
for (server.document_store.build_files.keys()) |build_file_uri| {
server.document_store.invalidateBuildFile(build_file_uri);
}
}
if (new_zig_exe_path or new_zig_lib_path) {
for (server.document_store.cimports.values()) |*cimport| {
cimport.deinit(server.document_store.allocator);
}
server.document_store.cimports.clearAndFree(server.document_store.allocator);
}
}
if (server.status == .initialized and
(new_zig_exe_path or new_zig_lib_path) and
server.client_capabilities.supports_publish_diagnostics)
{
for (server.document_store.handles.values()) |handle| {
if (!handle.isLspSynced()) continue;
server.generateDiagnostics(handle);
}
}
// <---------------------------------------------------------->
// don't modify config options after here, only show messages
// <---------------------------------------------------------->
check: {
if (!std.process.can_spawn) break :check;
if (server.status != .initialized) break :check;
// TODO there should a way to suppress this message
if (server.config_manager.zig_exe == null) {
server.showMessage(.Warning, "zig executable could not be found", .{});
} else if (server.config_manager.zig_lib_dir == null) {
server.showMessage(.Warning, "zig standard library directory could not be resolved", .{});
}
}
check: {
if (server.status != .initialized) break :check;
switch (server.config_manager.build_runner_supported) {
.yes, .no_dont_error => break :check,
.no => {},
}
const zig_version = server.config_manager.zig_exe.?.version;
const zls_version = build_options.version;
const zig_version_is_tagged = zig_version.pre == null and zig_version.build == null;
const zls_version_is_tagged = zls_version.pre == null and zls_version.build == null;
if (zig_version_is_tagged) {
server.showMessage(
.Warning,
"ZLS '{f}' does not support Zig '{f}'. A ZLS '{}.{}' release should be used instead.",
.{ zls_version, zig_version, zig_version.major, zig_version.minor },
);
} else if (zls_version_is_tagged) {
server.showMessage(
.Warning,
"ZLS '{f}' should be used with a Zig '{}.{}' release but found Zig '{f}'.",
.{ zls_version, zls_version.major, zls_version.minor, zig_version },
);
} else {
server.showMessage(
.Warning,
"ZLS '{f}' requires at least Zig '{s}' but got Zig '{f}'. Update Zig to avoid unexpected behavior.",
.{ zls_version, build_options.minimum_runtime_zig_version_string, zig_version },
);
}
}
if (server.config_manager.config.enable_build_on_save orelse false) {
if (!BuildOnSaveSupport.isSupportedComptime()) {
// This message is not very helpful but it relatively uncommon to happen anyway.
log.info("'enable_build_on_save' is ignored because build on save is not supported by this ZLS build", .{});
} else if (server.status == .initialized and (server.config_manager.config.zig_exe_path == null or server.config_manager.zig_lib_dir == null)) {
log.warn("'enable_build_on_save' is ignored because Zig could not be found", .{});
} else if (!server.client_capabilities.supports_publish_diagnostics) {
log.warn("'enable_build_on_save' is ignored because it is not supported by {s}", .{server.client_capabilities.client_name orelse "your editor"});
} else if (server.status == .initialized and server.config_manager.build_runner_supported == .no and server.config_manager.config.build_runner_path == null) {
log.warn("'enable_build_on_save' is ignored because no build runner is available", .{});
} else if (server.status == .initialized and server.config_manager.zig_exe != null) {
switch (BuildOnSaveSupport.isSupportedRuntime(server.config_manager.zig_exe.?.version)) {
.supported => {},
.invalid_linux_kernel_version => |*utsname_release| log.warn("Build-On-Save cannot run in watch mode because the Linux version '{s}' could not be parsed", .{std.mem.sliceTo(utsname_release, 0)}),
.unsupported_linux_kernel_version => |kernel_version| log.warn("Build-On-Save cannot run in watch mode because it is not supported by Linux '{f}' (requires at least {f})", .{ kernel_version, BuildOnSaveSupport.minimum_linux_version }),
.unsupported_zig_version => log.warn("Build-On-Save cannot run in watch mode because it is not supported on {t} by Zig {f} (requires at least {f})", .{ zig_builtin.os.tag, server.resolved_config.zig_runtime_version.?, BuildOnSaveSupport.minimum_zig_version }),
.unsupported_os => log.warn("Build-On-Save cannot run in watch mode because it is not supported on {t}", .{zig_builtin.os.tag}),
}
}
}
if (new_force_autofix) {
switch (server.autofixWorkaround()) {
.none => {},
.unavailable => {
log.warn("`force_autofix` is ignored because it is not supported by {s}", .{server.client_capabilities.client_name orelse "your editor"});
},
.on_save, .will_save_wait_until => |workaround| {
log.info("Autofix workaround enabled: '{t}'", .{workaround});
},
}
}
}
fn createDocumentStoreConfig(config_manager: *const configuration.Manager) DocumentStore.Config {
return .{
.zig_exe_path = config_manager.config.zig_exe_path,
.zig_lib_dir = config_manager.zig_lib_dir,
.build_runner_path = config_manager.config.build_runner_path,
.builtin_path = config_manager.config.builtin_path,
.global_cache_dir = config_manager.global_cache_dir,
};
}
fn openDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: types.TextDocument.DidOpenParams) Error!void {
if (notification.textDocument.text.len > DocumentStore.max_document_size) {
log.err("open document '{s}' failed: text size ({d}) is above maximum length ({d})", .{
notification.textDocument.uri,
notification.textDocument.text.len,
DocumentStore.max_document_size,
});
return error.InternalError;
}
const document_uri = Uri.parse(arena, notification.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
try server.document_store.openLspSyncedDocument(document_uri, notification.textDocument.text);
server.generateDiagnostics(server.document_store.getHandle(document_uri).?);
}
fn changeDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: types.TextDocument.DidChangeParams) Error!void {
if (notification.contentChanges.len == 0) return;
const document_uri = Uri.parse(arena, notification.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return;
const new_text = try diff.applyContentChanges(server.allocator, handle.tree.source, notification.contentChanges, server.offset_encoding);
if (new_text.len > DocumentStore.max_document_size) {
log.err("change document '{s}' failed: text size ({d}) is above maximum length ({d})", .{
document_uri.raw,
new_text.len,
DocumentStore.max_document_size,
});
server.allocator.free(new_text);
return error.InternalError;
}
try server.document_store.refreshLspSyncedDocument(handle.uri, new_text);
server.generateDiagnostics(handle);
}
fn saveDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: types.TextDocument.DidSaveParams) Error!void {
const document_uri = Uri.parse(arena, notification.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
if (std.process.can_spawn and DocumentStore.isBuildFile(document_uri)) {
server.document_store.invalidateBuildFile(document_uri);
}
if (server.autofixWorkaround() == .on_save) {
const handle = server.document_store.getHandle(document_uri) orelse return;
var text_edits = try server.autofix(arena, handle);
var workspace_edit: types.WorkspaceEdit = .{ .changes = .{} };
try workspace_edit.changes.?.map.putNoClobber(arena, document_uri.raw, try text_edits.toOwnedSlice(arena));
const json_message = try server.sendToClientRequest(
.{ .string = "apply_edit" },
"workspace/applyEdit",
types.workspace.apply_workspace_edit.Params{
.label = "autofix",
.edit = workspace_edit,
},
);
server.allocator.free(json_message);
}
if (BuildOnSaveSupport.isSupportedComptime()) {
for (server.workspaces.items) |*workspace| {
workspace.sendManualWatchUpdate();
}
}
}
fn closeDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: types.TextDocument.DidCloseParams) Error!void {
const document_uri = Uri.parse(arena, notification.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
server.document_store.closeLspSyncedDocument(document_uri);
if (server.client_capabilities.supports_publish_diagnostics) {
server.diagnostics_collection.clearSingleDocumentDiagnostics(document_uri);
server.diagnostics_collection.publishDiagnostics() catch |err| {
std.log.err("failed to publish diagnostics: {}", .{err});
};
}
}
fn willSaveWaitUntilHandler(server: *Server, arena: std.mem.Allocator, request: types.TextDocument.WillSaveParams) Error!?[]types.TextEdit {
if (server.autofixWorkaround() != .will_save_wait_until) return null;
switch (request.reason) {
.Manual => {},
.AfterDelay,
.FocusOut,
=> return null,
_ => return null,
}
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
var text_edits = try server.autofix(arena, handle);
return try text_edits.toOwnedSlice(arena);
}
fn semanticTokensFullHandler(server: *Server, arena: std.mem.Allocator, request: types.semantic_tokens.Params) Error!?types.semantic_tokens.Result {
if (server.config_manager.config.semantic_tokens == .none) return null;
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
// Workaround: The Ast on .zon files is unusable when an error occured on the root expr
if (handle.tree.mode == .zon and handle.tree.errors.len > 0) return null;
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
// semantic tokens can be quite expensive to compute on large files
// and disabling callsite references can help with bringing the cost down.
analyser.collect_callsite_references = false;
return try semantic_tokens.writeSemanticTokens(
arena,
&analyser,
handle,
null,
server.offset_encoding,
server.config_manager.config.semantic_tokens == .partial,
server.client_capabilities.supports_semantic_tokens_overlapping,
);
}
fn semanticTokensRangeHandler(server: *Server, arena: std.mem.Allocator, request: types.semantic_tokens.Params.Range) Error!?types.semantic_tokens.Result {
if (server.config_manager.config.semantic_tokens == .none) return null;
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
// Workaround: The Ast on .zon files is unusable when an error occured on the root expr
if (handle.tree.mode == .zon and handle.tree.errors.len > 0) return null;
const loc = offsets.rangeToLoc(handle.tree.source, request.range, server.offset_encoding);
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
// semantic tokens can be quite expensive to compute on large files
// and disabling callsite references can help with bringing the cost down.
analyser.collect_callsite_references = false;
return try semantic_tokens.writeSemanticTokens(
arena,
&analyser,
handle,
loc,
server.offset_encoding,
server.config_manager.config.semantic_tokens == .partial,
server.client_capabilities.supports_semantic_tokens_overlapping,
);
}
fn completionHandler(server: *Server, arena: std.mem.Allocator, request: types.completion.Params) Error!?types.completion.Result {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.mode == .zon) return null;
const source_index = offsets.positionToIndex(handle.tree.source, request.position, server.offset_encoding);
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
return .{
.completion_list = try completions.completionAtIndex(server, &analyser, arena, handle, source_index) orelse return null,
};
}
fn signatureHelpHandler(server: *Server, arena: std.mem.Allocator, request: types.SignatureHelp.Params) Error!?types.SignatureHelp {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.mode == .zon) return null;
const source_index = offsets.positionToIndex(handle.tree.source, request.position, server.offset_encoding);
const markup_kind: types.MarkupKind = if (server.client_capabilities.signature_help_supports_md) .markdown else .plaintext;
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
const signature_info = (try signature_help.getSignatureInfo(
&analyser,
arena,
handle,
source_index,
markup_kind,
)) orelse return null;
var signatures = try arena.alloc(types.SignatureHelp.Signature, 1);
signatures[0] = signature_info;
return .{
.signatures = signatures,
.activeSignature = 0,
.activeParameter = signature_info.activeParameter,
};
}
fn gotoDefinitionHandler(
server: *Server,
arena: std.mem.Allocator,
request: types.Definition.Params,
) Error!?types.Definition.Result {
return goto.gotoHandler(server, arena, .definition, request);
}
fn gotoTypeDefinitionHandler(server: *Server, arena: std.mem.Allocator, request: types.type_definition.Params) Error!?types.Definition.Result {
return try goto.gotoHandler(server, arena, .type_definition, .{
.textDocument = request.textDocument,
.position = request.position,
.workDoneToken = request.workDoneToken,
.partialResultToken = request.partialResultToken,
});
}
fn gotoImplementationHandler(server: *Server, arena: std.mem.Allocator, request: types.implementation.Params) Error!?types.Definition.Result {
return try goto.gotoHandler(server, arena, .definition, .{
.textDocument = request.textDocument,
.position = request.position,
.workDoneToken = request.workDoneToken,
.partialResultToken = request.partialResultToken,
});
}
fn gotoDeclarationHandler(server: *Server, arena: std.mem.Allocator, request: types.declaration.Params) Error!?types.Definition.Result {
return try goto.gotoHandler(server, arena, .declaration, .{
.textDocument = request.textDocument,
.position = request.position,
.workDoneToken = request.workDoneToken,
.partialResultToken = request.partialResultToken,
});
}
fn hoverHandler(server: *Server, arena: std.mem.Allocator, request: types.Hover.Params) Error!?types.Hover {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.mode == .zon) return null;
const source_index = offsets.positionToIndex(handle.tree.source, request.position, server.offset_encoding);
const markup_kind: types.MarkupKind = if (server.client_capabilities.hover_supports_md) .markdown else .plaintext;
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
return hover_handler.hover(
&analyser,
arena,
handle,
source_index,
markup_kind,
server.offset_encoding,
);
}
fn documentSymbolsHandler(server: *Server, arena: std.mem.Allocator, request: types.DocumentSymbol.Params) Error!lsp.ResultType("textDocument/documentSymbol") {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.mode == .zon) return null;
return .{
.document_symbols = try document_symbol.getDocumentSymbols(arena, &handle.tree, server.offset_encoding),
};
}
fn formattingHandler(server: *Server, arena: std.mem.Allocator, request: types.document_formatting.Params) Error!?[]types.TextEdit {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.errors.len != 0) return null;
const formatted = try handle.tree.renderAlloc(arena);
if (std.mem.eql(u8, handle.tree.source, formatted)) return null;
const text_edits = try diff.edits(arena, handle.tree.source, formatted, server.offset_encoding);
return text_edits.items;
}
fn renameHandler(server: *Server, arena: std.mem.Allocator, request: types.rename.Params) Error!?types.WorkspaceEdit {
const response = try references.referencesHandler(server, arena, .{ .rename = request });
return if (response) |rep| rep.rename else null;
}
fn prepareRenameHandler(server: *Server, arena: std.mem.Allocator, request: types.prepare_rename.Params) Error!?types.prepare_rename.Result {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
const source_index = offsets.positionToIndex(handle.tree.source, request.position, server.offset_encoding);
const name_loc = Analyser.identifierLocFromIndex(&handle.tree, source_index) orelse return null;
const name = offsets.locToSlice(handle.tree.source, name_loc);
return .{
.prepare_rename_placeholder = .{
.range = offsets.locToRange(handle.tree.source, name_loc, server.offset_encoding),
.placeholder = name,
},
};
}
fn referencesHandler(server: *Server, arena: std.mem.Allocator, request: types.reference.Params) Error!?[]types.Location {
const response = try references.referencesHandler(server, arena, .{ .references = request });
return if (response) |rep| rep.references else null;
}
fn documentHighlightHandler(server: *Server, arena: std.mem.Allocator, request: types.DocumentHighlight.Params) Error!?[]types.DocumentHighlight {
const response = try references.referencesHandler(server, arena, .{ .highlight = request });
return if (response) |rep| rep.highlight else null;
}
fn inlayHintHandler(server: *Server, arena: std.mem.Allocator, request: types.InlayHint.Params) Error!?[]types.InlayHint {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
if (handle.tree.mode == .zon) return null;
// The Language Server Specification does not provide a client capabilities that allows the client to specify the MarkupKind of inlay hints.
const hover_kind: types.MarkupKind = if (server.client_capabilities.hover_supports_md) .markdown else .plaintext;
const loc = offsets.rangeToLoc(handle.tree.source, request.range, server.offset_encoding);
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
return try inlay_hints.writeRangeInlayHint(
arena,
&server.config_manager.config,
&analyser,
handle,
loc,
hover_kind,
server.offset_encoding,
);
}
fn codeActionHandler(server: *Server, arena: std.mem.Allocator, request: types.CodeAction.Params) Error!?[]const lsp.types.CodeAction.Result {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
// as of right now, only ast-check errors may get a code action
if (handle.tree.errors.len != 0) return null;
if (handle.tree.mode == .zon) return null;
var error_bundle = try diagnostics_gen.getAstCheckDiagnostics(server, handle);
defer error_bundle.deinit(server.allocator);
var analyser = server.initAnalyser(arena, handle);
defer analyser.deinit();
const only_kinds = if (request.context.only) |kinds| blk: {
var set: std.EnumSet(std.meta.Tag(types.CodeAction.Kind)) = .initEmpty();
for (kinds) |kind| {
set.setPresent(kind, true);
}
break :blk set;
} else null;
var builder: code_actions.Builder = .{
.arena = arena,
.analyser = &analyser,
.handle = handle,
.offset_encoding = server.offset_encoding,
.only_kinds = only_kinds,
};
try builder.generateCodeAction(error_bundle);
try builder.generateCodeActionsInRange(request.range);
const result = try arena.alloc(types.CodeAction.Result, builder.actions.items.len);
for (builder.actions.items, result) |action, *out| {
out.* = .{ .code_action = action };
}
return result;
}
fn foldingRangeHandler(server: *Server, arena: std.mem.Allocator, request: types.FoldingRange.Params) Error!?[]types.FoldingRange {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
return try folding_range.generateFoldingRanges(arena, &handle.tree, server.offset_encoding);
}
fn selectionRangeHandler(server: *Server, arena: std.mem.Allocator, request: types.SelectionRange.Params) Error!?[]types.SelectionRange {
const document_uri = Uri.parse(arena, request.textDocument.uri) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.InvalidParams,
};
const handle = server.document_store.getHandle(document_uri) orelse return null;
return try selection_range.generateSelectionRanges(arena, handle, request.positions, server.offset_encoding);
}
const HandledRequestParams = union(enum) {
initialize: types.InitializeParams,
shutdown,
@"textDocument/willSaveWaitUntil": types.TextDocument.WillSaveParams,
@"textDocument/semanticTokens/full": types.semantic_tokens.Params,
@"textDocument/semanticTokens/range": types.semantic_tokens.Params.Range,
@"textDocument/inlayHint": types.InlayHint.Params,
@"textDocument/completion": types.completion.Params,
@"textDocument/signatureHelp": types.SignatureHelp.Params,
@"textDocument/definition": types.Definition.Params,
@"textDocument/typeDefinition": types.type_definition.Params,
@"textDocument/implementation": types.implementation.Params,
@"textDocument/declaration": types.declaration.Params,
@"textDocument/hover": types.Hover.Params,
@"textDocument/documentSymbol": types.DocumentSymbol.Params,
@"textDocument/formatting": types.document_formatting.Params,
@"textDocument/rename": types.rename.Params,
@"textDocument/prepareRename": types.prepare_rename.Params,
@"textDocument/references": types.reference.Params,
@"textDocument/documentHighlight": types.DocumentHighlight.Params,
@"textDocument/codeAction": types.CodeAction.Params,
@"textDocument/foldingRange": types.FoldingRange.Params,
@"textDocument/selectionRange": types.SelectionRange.Params,
other: lsp.MethodWithParams,
};
const HandledNotificationParams = union(enum) {
initialized: types.InitializedParams,
exit,
@"textDocument/didOpen": types.TextDocument.DidOpenParams,
@"textDocument/didChange": types.TextDocument.DidChangeParams,
@"textDocument/didSave": types.TextDocument.DidSaveParams,
@"textDocument/didClose": types.TextDocument.DidCloseParams,
@"workspace/didChangeWatchedFiles": types.workspace.did_change_watched_files.Params,
@"workspace/didChangeWorkspaceFolders": types.workspace.folders.DidChangeParams,
@"workspace/didChangeConfiguration": types.workspace.configuration.did_change.Params,
other: lsp.MethodWithParams,
};
const Message = lsp.Message(HandledRequestParams, HandledNotificationParams, .{});
fn isBlockingMessage(msg: Message) bool {
switch (msg) {
.request => |request| switch (request.params) {
.initialize,
.shutdown,
=> return true,
.@"textDocument/willSaveWaitUntil",
.@"textDocument/semanticTokens/full",
.@"textDocument/semanticTokens/range",
.@"textDocument/inlayHint",
.@"textDocument/completion",
.@"textDocument/signatureHelp",
.@"textDocument/definition",
.@"textDocument/typeDefinition",
.@"textDocument/implementation",
.@"textDocument/declaration",
.@"textDocument/hover",
.@"textDocument/documentSymbol",
.@"textDocument/formatting",
.@"textDocument/rename",
.@"textDocument/prepareRename",
.@"textDocument/references",
.@"textDocument/documentHighlight",
.@"textDocument/codeAction",
.@"textDocument/foldingRange",
.@"textDocument/selectionRange",
=> return false,
.other => return false,
},
.notification => |notification| switch (notification.params) {
.initialized,
.exit,
.@"textDocument/didOpen",
.@"textDocument/didChange",
.@"textDocument/didSave",
.@"textDocument/didClose",
.@"workspace/didChangeWatchedFiles",
.@"workspace/didChangeWorkspaceFolders",
.@"workspace/didChangeConfiguration",
=> return true,
.other => return false,
},
.response => return true,
}
}
pub const CreateOptions = struct {
/// Must support `concurrent` unless the ZLS module is in single_threaded mode.
io: std.Io,
/// Must be thread-safe unless the ZLS module is in single_threaded mode.
allocator: std.mem.Allocator,
/// Must be set when running `loop`. Controls how the server will send and receive messages.
transport: ?*lsp.Transport,
/// The `global_cache_path` will not be resolve automatically.
config: ?*const Config,
config_manager: ?configuration.Manager = null,
max_thread_count: usize = 4, // what is a good value here?
};
pub fn create(options: CreateOptions) (std.mem.Allocator.Error || std.Thread.SpawnError)!*Server {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
const allocator = options.allocator;
const io = options.io;
const server = try allocator.create(Server);
errdefer allocator.destroy(server);
server.* = .{
.io = io,
.allocator = allocator,
.config_manager = options.config_manager orelse .init(allocator),
.document_store = .{
.io = io,
.allocator = allocator,
.config = undefined, // set below
.thread_pool = &server.thread_pool,
.diagnostics_collection = &server.diagnostics_collection,
},
.thread_pool = undefined, // set below
.diagnostics_collection = .{ .allocator = allocator },
};
server.document_store.config = createDocumentStoreConfig(&server.config_manager);
try server.thread_pool.init(.{
.allocator = allocator,
.n_jobs = @min(4, std.Thread.getCpuCount() catch 1), // what is a good value here?
});
errdefer server.thread_pool.deinit();
server.ip = try InternPool.init(allocator);
errdefer server.ip.deinit(allocator);
if (options.transport) |transport| {
server.setTransport(transport);
}
if (options.config) |config| {
try server.config_manager.setConfiguration2(.frontend, config);
}
return server;
}
pub fn destroy(server: *Server) void {
server.thread_pool.deinit();
server.document_store.deinit();
server.ip.deinit(server.allocator);
for (server.workspaces.items) |*workspace| workspace.deinit(server.allocator);
server.workspaces.deinit(server.allocator);
server.diagnostics_collection.deinit();
server.client_capabilities.deinit(server.allocator);
server.config_manager.deinit();
for (server.pending_show_messages.items) |params| server.allocator.free(params.message);
server.pending_show_messages.deinit(server.allocator);
server.allocator.destroy(server);
}
pub fn setTransport(server: *Server, transport: *lsp.Transport) void {
server.transport = transport;
server.diagnostics_collection.transport = transport;
server.document_store.transport = transport;
}
pub fn keepRunning(server: Server) bool {
switch (server.status) {
.exiting_success, .exiting_failure => return false,
else => return true,
}
}
/// The main loop of ZLS
pub fn loop(server: *Server) !void {
std.debug.assert(server.transport != null);
while (server.keepRunning()) {
const json_message = try server.transport.?.readJsonMessage(server.allocator);
defer server.allocator.free(json_message);
var arena_allocator: std.heap.ArenaAllocator = .init(server.allocator);
errdefer arena_allocator.deinit();
const message = message: {
const tracy_zone = tracy.traceNamed(@src(), "Message.parse");
defer tracy_zone.end();
break :message Message.parseFromSliceLeaky(
arena_allocator.allocator(),
json_message,
.{ .ignore_unknown_fields = true, .max_value_len = null, .allocate = .alloc_always },
) catch return error.ParseError;
};
errdefer comptime unreachable;
if (zig_builtin.single_threaded) {
server.processMessageReportError(arena_allocator.state, message);
continue;
}
if (isBlockingMessage(message)) {
server.thread_pool.waitAndWork(&server.wait_group);
server.wait_group.reset();
server.processMessageReportError(arena_allocator.state, message);
} else {
server.thread_pool.spawnWg(&server.wait_group, processMessageReportError, .{ server, arena_allocator.state, message });
}
}
}
pub fn sendJsonMessageSync(server: *Server, json_message: []const u8) Error!?[]u8 {
const parsed_message = Message.parseFromSlice(
server.allocator,
json_message,
.{ .ignore_unknown_fields = true, .max_value_len = null, .allocate = .alloc_always },
) catch return error.ParseError;
defer parsed_message.deinit();
return try server.processMessage(parsed_message.arena.allocator(), parsed_message.value);
}
pub fn sendRequestSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: lsp.ParamsType(method)) Error!lsp.ResultType(method) {
comptime std.debug.assert(lsp.isRequestMethod(method));
const tracy_zone = tracy.traceNamed(@src(), "sendRequestSync(" ++ method ++ ")");
defer tracy_zone.end();
tracy_zone.setName(method);
const Params = std.meta.Tag(HandledRequestParams);
if (!@hasField(Params, method)) return null;
return switch (@field(Params, method)) {
.initialize => try server.initializeHandler(arena, params),
.shutdown => try server.shutdownHandler(arena, params),
.@"textDocument/willSaveWaitUntil" => try server.willSaveWaitUntilHandler(arena, params),
.@"textDocument/semanticTokens/full" => try server.semanticTokensFullHandler(arena, params),
.@"textDocument/semanticTokens/range" => try server.semanticTokensRangeHandler(arena, params),
.@"textDocument/inlayHint" => try server.inlayHintHandler(arena, params),
.@"textDocument/completion" => try server.completionHandler(arena, params),
.@"textDocument/signatureHelp" => try server.signatureHelpHandler(arena, params),
.@"textDocument/definition" => try server.gotoDefinitionHandler(arena, params),
.@"textDocument/typeDefinition" => try server.gotoTypeDefinitionHandler(arena, params),
.@"textDocument/implementation" => try server.gotoImplementationHandler(arena, params),
.@"textDocument/declaration" => try server.gotoDeclarationHandler(arena, params),
.@"textDocument/hover" => try server.hoverHandler(arena, params),
.@"textDocument/documentSymbol" => try server.documentSymbolsHandler(arena, params),
.@"textDocument/formatting" => try server.formattingHandler(arena, params),
.@"textDocument/rename" => try server.renameHandler(arena, params),
.@"textDocument/prepareRename" => try server.prepareRenameHandler(arena, params),
.@"textDocument/references" => try server.referencesHandler(arena, params),
.@"textDocument/documentHighlight" => try server.documentHighlightHandler(arena, params),
.@"textDocument/codeAction" => try server.codeActionHandler(arena, params),
.@"textDocument/foldingRange" => try server.foldingRangeHandler(arena, params),
.@"textDocument/selectionRange" => try server.selectionRangeHandler(arena, params),
.other => return null,
};
}
pub fn sendNotificationSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: lsp.ParamsType(method)) Error!void {
comptime std.debug.assert(lsp.isNotificationMethod(method));
const tracy_zone = tracy.traceNamed(@src(), "sendNotificationSync(" ++ method ++ ")");
defer tracy_zone.end();
tracy_zone.setName(method);
const Params = std.meta.Tag(HandledNotificationParams);
if (!@hasField(Params, method)) return null;
return switch (@field(Params, method)) {
.initialized => try server.initializedHandler(arena, params),
.exit => try server.exitHandler(arena, params),
.@"textDocument/didOpen" => try server.openDocumentHandler(arena, params),
.@"textDocument/didChange" => try server.changeDocumentHandler(arena, params),
.@"textDocument/didSave" => try server.saveDocumentHandler(arena, params),
.@"textDocument/didClose" => try server.closeDocumentHandler(arena, params),
.@"workspace/didChangeWatchedFiles" => try server.didChangeWatchedFilesHandler(arena, params),
.@"workspace/didChangeWorkspaceFolders" => try server.didChangeWorkspaceFoldersHandler(arena, params),
.@"workspace/didChangeConfiguration" => try server.didChangeConfigurationHandler(arena, params),
.other => {},
};
}
pub fn sendMessageSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: lsp.ParamsType(method)) Error!lsp.ResultType(method) {
comptime std.debug.assert(lsp.isRequestMethod(method) or lsp.isNotificationMethod(method));
if (comptime lsp.isRequestMethod(method)) {
return try server.sendRequestSync(arena, method, params);
} else if (comptime lsp.isNotificationMethod(method)) {
return try server.sendNotificationSync(arena, method, params);
} else unreachable;
}
fn processMessage(server: *Server, arena: std.mem.Allocator, message: Message) Error!?[]u8 {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
try server.validateMessage(message);
switch (message) {
.request => |request| switch (request.params) {
.other => 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);
},
},
.notification => |notification| switch (notification.params) {
.other => {},
inline else => |params, method| try server.sendNotificationSync(arena, @tagName(method), params),
},
.response => |response| try server.handleResponse(response),
}
return null;
}
fn processMessageReportError(server: *Server, arena_state: std.heap.ArenaAllocator.State, message: Message) void {
var arena_allocator = arena_state.promote(server.allocator);
defer arena_allocator.deinit();
if (server.processMessage(arena_allocator.allocator(), message)) |json_message| {
server.allocator.free(json_message orelse return);
} else |err| {
log.err("failed to process {f}: {}", .{ fmtMessage(message), err });
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace);
}
switch (message) {
.request => |request| {
const json_message = server.sendToClientResponseError(request.id, .{
.code = @enumFromInt(switch (err) {
error.OutOfMemory => @intFromEnum(types.ErrorCodes.InternalError),
error.ParseError => @intFromEnum(types.ErrorCodes.ParseError),
error.InvalidRequest => @intFromEnum(types.ErrorCodes.InvalidRequest),
error.MethodNotFound => @intFromEnum(types.ErrorCodes.MethodNotFound),
error.InvalidParams => @intFromEnum(types.ErrorCodes.InvalidParams),
error.InternalError => @intFromEnum(types.ErrorCodes.InternalError),
error.ServerNotInitialized => @intFromEnum(types.ErrorCodes.ServerNotInitialized),
error.RequestFailed => @intFromEnum(types.LSPErrorCodes.RequestFailed),
error.ServerCancelled => @intFromEnum(types.LSPErrorCodes.ServerCancelled),
error.ContentModified => @intFromEnum(types.LSPErrorCodes.ContentModified),
error.RequestCancelled => @intFromEnum(types.LSPErrorCodes.RequestCancelled),
}),
.message = @errorName(err),
}) catch return;
server.allocator.free(json_message);
},
.notification, .response => return,
}
}
}
fn validateMessage(server: *const Server, message: Message) Error!void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
const method = switch (message) {
.request => |request| switch (request.params) {
.other => |info| info.method,
else => @tagName(request.params),
},
.notification => |notification| switch (notification.params) {
.other => |info| info.method,
else => @tagName(notification.params),
},
.response => return, // validation happens in `handleResponse`
};
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#dollarRequests
if (message == .request and std.mem.startsWith(u8, method, "$/")) return error.MethodNotFound;
if (message == .notification and std.mem.startsWith(u8, method, "$/")) return;
switch (server.status) {
.uninitialized => blk: {
if (std.mem.eql(u8, method, "initialize")) break :blk;
if (std.mem.eql(u8, method, "exit")) break :blk;
return error.ServerNotInitialized; // server received a request before being initialized!
},
.initializing => blk: {
if (std.mem.eql(u8, method, "initialized")) break :blk;
if (std.mem.eql(u8, method, "$/progress")) break :blk;
return error.InvalidRequest; // server received a request during initialization!
},
.initialized => {},
.shutdown => blk: {
if (std.mem.eql(u8, method, "exit")) break :blk;
return error.InvalidRequest; // server received a request after shutdown!
},
.exiting_success,
.exiting_failure,
=> unreachable,
}
}
fn handleResponse(server: *Server, response: lsp.JsonRPCMessage.Response) Error!void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (response.id == null) {
log.warn("received response from client without id!", .{});
return;
}
const id: []const u8 = switch (response.id.?) {
.string => |id| id,
.number => |id| {
log.warn("received response from client with id '{d}' that has no handler!", .{id});
return;
},
};
const result = switch (response.result_or_error) {
.result => |result| result,
.@"error" => |err| {
log.err("Error response for '{s}': {}, {s}", .{ id, err.code, err.message });
if (std.mem.eql(u8, id, "i_haz_configuration")) {
try server.resolveConfiguration();
}
return;
},
};
if (std.mem.eql(u8, id, "semantic_tokens_refresh")) {
//
} else if (std.mem.eql(u8, id, "inlay_hints_refresh")) {
//
} else if (std.mem.eql(u8, id, "progress")) {
//
} else if (std.mem.startsWith(u8, id, "register")) {
//
} else if (std.mem.eql(u8, id, "apply_edit")) {
//
} else if (std.mem.eql(u8, id, "i_haz_configuration")) {
try server.handleConfiguration(result orelse .null);
} else {
log.warn("received response from client with id '{s}' that has no handler!", .{id});
}
}
fn formatMessage(message: Message, writer: *std.Io.Writer) std.Io.Writer.Error!void {
switch (message) {
.request => |request| try writer.print("request-{f}-{t}", .{ std.json.fmt(request.id, .{}), request.params }),
.notification => |notification| try writer.print("notification-{t}", .{notification.params}),
.response => |response| try writer.print("response-{f}", .{std.json.fmt(response.id, .{})}),
}
}
fn fmtMessage(message: Message) std.fmt.Alt(Message, formatMessage) {
return .{ .data = message };
}