diff --git a/src/main.zig b/src/main.zig index bce110a..b728f92 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const DocumentStore = @import("document_store.zig"); const DebugAllocator = @import("debug_allocator.zig"); const readRequestHeader = @import("header.zig").readRequestHeader; const data = @import("data/" ++ build_options.data_version ++ ".zig"); +const requests = @import("requests.zig"); const types = @import("types.zig"); const analysis = @import("analysis.zig"); const URI = @import("uri.zig"); @@ -25,7 +26,10 @@ pub fn log( comptime format: []const u8, args: var, ) void { - var message = std.fmt.allocPrint(allocator, "[{}-{}] " ++ format, .{ @tagName(message_level), @tagName(scope) } ++ args) catch |err| { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var message = std.fmt.allocPrint(&arena.allocator, "[{}-{}] " ++ format, .{ @tagName(message_level), @tagName(scope) } ++ args) catch |err| { std.debug.print("Failed to allocPrint message.", .{}); return; }; @@ -37,7 +41,7 @@ pub fn log( .err => .Error, else => .Error, }; - send(types.Notification{ + send(&arena, types.Notification{ .method = "window/showMessage", .params = types.NotificationParams{ .ShowMessageParams = .{ @@ -54,7 +58,7 @@ pub fn log( else .Info; - send(types.Notification{ + send(&arena, types.Notification{ .method = "window/logMessage", .params = types.NotificationParams{ .LogMessageParams = .{ @@ -113,14 +117,11 @@ const no_semantic_tokens_response = ; /// Sends a request or response -fn send(reqOrRes: var) !void { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - +fn send(arena: *std.heap.ArenaAllocator, reqOrRes: var) !void { var arr = std.ArrayList(u8).init(&arena.allocator); try std.json.stringify(reqOrRes, .{}, arr.writer()); - const stdout_stream = stdout.outStream(); + const stdout_stream = stdout.writer(); try stdout_stream.print("Content-Length: {}\r\n\r\n", .{arr.items.len}); try stdout_stream.writeAll(arr.items); try stdout.flush(); @@ -182,13 +183,9 @@ fn astLocationToRange(loc: std.zig.ast.Tree.Location) types.Range { }; } -fn publishDiagnostics(handle: DocumentStore.Handle, config: Config) !void { +fn publishDiagnostics(arena: *std.heap.ArenaAllocator, handle: DocumentStore.Handle, config: Config) !void { const tree = handle.tree; - // Use an arena for our local memory allocations. - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - var diagnostics = std.ArrayList(types.Diagnostic).init(&arena.allocator); for (tree.errors) |*err| { @@ -249,7 +246,7 @@ fn publishDiagnostics(handle: DocumentStore.Handle, config: Config) !void { } } - try send(types.Notification{ + try send(arena, types.Notification{ .method = "textDocument/publishDiagnostics", .params = .{ .PublishDiagnosticsParams = .{ @@ -1001,9 +998,127 @@ fn configFromUriOr(uri: []const u8, default: Config) Config { return default; } -// TODO Rewrite this, use a ComptimeStringMap that points to a fn pointer + Param type to decode into and pass to the function -// Split into multiple files? -fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config, keep_running: *bool) !void { +fn initializeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.Initialize, config: Config) !void { + if (req.params.capabilities.workspace) |workspace| { + client_capabilities.supports_workspace_folders = workspace.workspaceFolders.value; + } + + if (req.params.capabilities.textDocument) |textDocument| { + client_capabilities.supports_semantic_tokens = textDocument.semanticTokens.exists; + if (textDocument.hover) |hover| { + for (hover.contentFormat.value) |format| { + if (std.mem.eql(u8, "markdown", format)) { + client_capabilities.hover_supports_md = true; + } + } + } + if (textDocument.completion) |completion| { + if (completion.completionItem) |completionItem| { + client_capabilities.supports_snippets = completionItem.snippetSupport.value; + for (completionItem.documentationFormat.value) |documentationFormat| { + if (std.mem.eql(u8, "markdown", documentationFormat)) { + client_capabilities.completion_doc_supports_md = true; + } + } + } + } + } + + if (req.params.workspaceFolders) |workspaceFolders| { + if (workspaceFolders.len != 0) { + std.log.debug(.main, "Got workspace folders in initialization.\n", .{}); + } + for (workspaceFolders) |workspace_folder| { + std.log.debug(.main, "Loaded folder {}\n", .{workspace_folder.uri}); + const duped_uri = try std.mem.dupe(allocator, u8, workspace_folder.uri); + try workspace_folder_configs.putNoClobber(duped_uri, null); + } + try loadWorkspaceConfigs(); + } + + std.log.debug(.main, "{}\n", .{client_capabilities}); + try respondGeneric(id, initialize_response); + std.log.notice(.main, "zls initialized", .{}); +} + +var keep_running = true; +fn shutdownHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, config: Config) !void { + keep_running = false; + // Technically we should deinitialize first and send possible errors to the client + try respondGeneric(id, null_result_response); +} + +fn workspaceFoldersChangeHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.WorkspaceFoldersChange, config: Config) !void { + for (req.params.event.removed) |rem| { + if (workspace_folder_configs.remove(rem.uri)) |entry| { + allocator.free(entry.key); + if (entry.value) |c| { + std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator }); + } + } + } + + for (req.params.event.added) |add| { + const duped_uri = try std.mem.dupe(allocator, u8, add.uri); + if (try workspace_folder_configs.put(duped_uri, null)) |old| { + allocator.free(old.key); + if (old.value) |c| { + std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator }); + } + } + } + + try loadWorkspaceConfigs(); +} + +fn openDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.OpenDocument, config: Config) !void { + const handle = try document_store.openDocument(req.params.textDocument.uri, req.params.textDocument.text); + try publishDiagnostics(arena, handle.*, configFromUriOr(req.params.textDocument.uri, config)); +} + +fn changeDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.ChangeDocument, config: Config) !void { + const handle = document_store.getHandle(req.params.textDocument.uri) orelse { + std.log.debug(.main, "Trying to change non existent document {}", .{req.params.textDocument.uri}); + return; + }; + + const local_config = configFromUriOr(req.params.textDocument.uri, config); + try document_store.applyChanges(handle, req.params.contentChanges.Array, local_config.zig_lib_path); + try publishDiagnostics(arena, handle.*, local_config); +} + +fn saveDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.SaveDocument, config: Config) !void { + const handle = document_store.getHandle(req.params.textDocument.uri) orelse { + std.log.debug(.main, "Trying to save non existent document {}", .{req.params.textDocument.uri}); + return; + }; + try document_store.applySave(handle); +} + +fn closeDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.CloseDocument, config: Config) !void { + document_store.closeDocument(req.params.textDocument.uri); +} + +fn semanticTokensHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.SemanticTokens, config: Config) !void { + const this_config = configFromUriOr(req.params.textDocument.uri, config); + if (this_config.enable_semantic_tokens) { + const handle = document_store.getHandle(req.params.textDocument.uri) orelse { + std.log.debug(.main, "Trying to complete in non existent document {}", .{req.params.textDocument.uri}); + return try respondGeneric(id, no_semantic_tokens_response); + }; + + const semantic_tokens = @import("semantic_tokens.zig"); + const token_array = try semantic_tokens.writeAllSemanticTokens(arena, &document_store, handle); + + return try send(arena, types.Response{ + .id = id, + .result = .{ .SemanticTokens = .{ .data = token_array } }, + }); + } else + return try respondGeneric(id, no_semantic_tokens_response); +} + +fn processJsonRpc(arena: *std.heap.ArenaAllocator, parser: *std.json.Parser, json: []const u8, config: Config) !void { var tree = try parser.parse(json); defer tree.deinit(); @@ -1024,401 +1139,272 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config, ke std.log.debug(.main, "Took {}ms to process method {}\n", .{ end_time - start_time, method }); } - // Core - if (std.mem.eql(u8, method, "initialize")) { - const params = root.Object.getValue("params").?.Object; - const client_capabs = params.getValue("capabilities").?.Object; - if (client_capabs.getValue("workspace")) |workspace_capabs| { - if (workspace_capabs.Object.getValue("workspaceFolders")) |folders_capab| { - client_capabilities.supports_workspace_folders = folders_capab.Bool; - } - } + const method_map = .{ + .{ "initialize", .{ requests.Initialize, initializeHandler } }, + .{ "shutdown", .{ void, shutdownHandler } }, + .{ "initialized", .{} }, + .{ "$/cancelRequest", .{} }, + .{ "workspace/didChangeWorkspaceFolders", .{ requests.WorkspaceFoldersChange, workspaceFoldersChangeHandler } }, + .{ "textDocument/didOpen", .{ requests.OpenDocument, openDocumentHandler } }, + .{ "textDocument/didChange", .{ requests.ChangeDocument, changeDocumentHandler } }, + .{ "textDocument/didSave", .{ requests.SaveDocument, saveDocumentHandler } }, + .{ "textDocument/willSave", .{} }, + .{ "textDocument/didClose", .{ requests.CloseDocument, closeDocumentHandler } }, + .{ "textDocument/semanticTokens", .{ requests.SemanticTokens, semanticTokensHandler } }, + }; - if (client_capabs.getValue("textDocument")) |text_doc_capabs| { - if (text_doc_capabs.Object.getValue("semanticTokens")) |_| { - client_capabilities.supports_semantic_tokens = true; - } - - if (text_doc_capabs.Object.getValue("hover")) |hover_capabs| { - if (hover_capabs.Object.getValue("contentFormat")) |content_formats| { - for (content_formats.Array.items) |format| { - if (std.mem.eql(u8, "markdown", format.String)) { - client_capabilities.hover_supports_md = true; - } - } - } - } - - if (text_doc_capabs.Object.getValue("completion")) |completion_capabs| { - if (completion_capabs.Object.getValue("completionItem")) |item_capabs| { - const maybe_support_snippet = item_capabs.Object.getValue("snippetSupport"); - client_capabilities.supports_snippets = maybe_support_snippet != null and maybe_support_snippet.?.Bool; - - if (item_capabs.Object.getValue("documentationFormat")) |content_formats| { - for (content_formats.Array.items) |format| { - if (std.mem.eql(u8, "markdown", format.String)) { - client_capabilities.completion_doc_supports_md = true; - } - } - } - } - } - } - - if (params.getValue("workspaceFolders")) |workspace_folders| { - switch (workspace_folders) { - .Array => |folders| { - std.log.debug(.main, "Got workspace folders in initialization.\n", .{}); - - for (folders.items) |workspace_folder| { - const folder_uri = workspace_folder.Object.getValue("uri").?.String; - std.log.debug(.main, "Loaded folder {}\n", .{folder_uri}); - const duped_uri = try std.mem.dupe(allocator, u8, folder_uri); - try workspace_folder_configs.putNoClobber(duped_uri, null); - } - try loadWorkspaceConfigs(); - }, - else => {}, - } - } - - std.log.debug(.main, "{}\n", .{client_capabilities}); - try respondGeneric(id, initialize_response); - std.log.notice(.main, "zls initialized", .{}); - } else if (std.mem.eql(u8, method, "shutdown")) { - keep_running.* = false; - // Technically we shoudl deinitialize first and send possible errors to the client - try respondGeneric(id, null_result_response); - } else if (std.mem.eql(u8, method, "initialized")) { - // All gucci - } else if (std.mem.eql(u8, method, "$/cancelRequest")) { - // noop - } - // Workspace folder changes - else if (std.mem.eql(u8, method, "workspace/didChangeWorkspaceFolders")) { - const params = root.Object.getValue("params").?.Object; - const event = params.getValue("event").?.Object; - const added = event.getValue("added").?.Array; - const removed = event.getValue("removed").?.Array; - - for (removed.items) |rem| { - const uri = rem.Object.getValue("uri").?.String; - if (workspace_folder_configs.remove(uri)) |entry| { - allocator.free(entry.key); - if (entry.value) |c| { - std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator }); - } - } - } - - for (added.items) |add| { - const duped_uri = try std.mem.dupe(allocator, u8, add.Object.getValue("uri").?.String); - if (try workspace_folder_configs.put(duped_uri, null)) |old| { - allocator.free(old.key); - if (old.value) |c| { - std.json.parseFree(Config, c, std.json.ParseOptions{ .allocator = allocator }); - } - } - } - - try loadWorkspaceConfigs(); - } - // File changes - else if (std.mem.eql(u8, method, "textDocument/didOpen")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - const text = document.getValue("text").?.String; - - const handle = try document_store.openDocument(uri, text); - try publishDiagnostics(handle.*, configFromUriOr(uri, config)); - } else if (std.mem.eql(u8, method, "textDocument/didChange")) { - const params = root.Object.getValue("params").?.Object; - const text_document = params.getValue("textDocument").?.Object; - const uri = text_document.getValue("uri").?.String; - const content_changes = params.getValue("contentChanges").?.Array; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to change non existent document {}", .{uri}); - return; - }; - - const local_config = configFromUriOr(uri, config); - try document_store.applyChanges(handle, content_changes, local_config.zig_lib_path); - try publishDiagnostics(handle.*, local_config); - } else if (std.mem.eql(u8, method, "textDocument/didSave")) { - const params = root.Object.getValue("params").?.Object; - const text_document = params.getValue("textDocument").?.Object; - const uri = text_document.getValue("uri").?.String; - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to save non existent document {}", .{uri}); - return; - }; - - try document_store.applySave(handle); - } else if (std.mem.eql(u8, method, "textDocument/willSave")) { - // noop - } else if (std.mem.eql(u8, method, "textDocument/didClose")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - - document_store.closeDocument(uri); - } - // Semantic highlighting - else if (std.mem.eql(u8, method, "textDocument/semanticTokens")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - - const this_config = configFromUriOr(uri, config); - if (this_config.enable_semantic_tokens) { - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to complete in non existent document {}", .{uri}); - return try respondGeneric(id, no_semantic_tokens_response); - }; - - const semantic_tokens = @import("semantic_tokens.zig"); - const token_array = try semantic_tokens.writeAllSemanticTokens(allocator, &document_store, handle); - defer allocator.free(token_array); - - return try send(types.Response{ - .id = id, - .result = .{ .SemanticTokens = .{ .data = token_array } }, - }); - } else - return try respondGeneric(id, no_semantic_tokens_response); - } - // Autocomplete / Signatures - else if (std.mem.eql(u8, method, "textDocument/completion")) { - const params = root.Object.getValue("params").?.Object; - const text_document = params.getValue("textDocument").?.Object; - const uri = text_document.getValue("uri").?.String; - const position = params.getValue("position").?.Object; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to complete in non existent document {}", .{uri}); - return try respondGeneric(id, no_completions_response); - }; - - const pos = types.Position{ - .line = position.getValue("line").?.Integer, - .character = position.getValue("character").?.Integer, - }; - if (pos.character >= 0) { - const pos_index = try handle.document.positionToIndex(pos); - const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); - - const this_config = configFromUriOr(uri, config); - const use_snippets = this_config.enable_snippets and client_capabilities.supports_snippets; - switch (pos_context) { - .builtin => try send(types.Response{ - .id = id, - .result = .{ - .CompletionList = .{ - .isIncomplete = false, - .items = builtin_completions[@boolToInt(use_snippets)][0..], - }, - }, - }), - .var_access, .empty => try completeGlobal(id, pos_index, handle, this_config), - .field_access => |range| try completeFieldAccess(id, handle, pos, range, this_config), - .global_error_set => try send(types.Response{ - .id = id, - .result = .{ - .CompletionList = .{ - .isIncomplete = false, - .items = document_store.error_completions.completions.items, - }, - }, - }), - .enum_literal => try send(types.Response{ - .id = id, - .result = .{ - .CompletionList = .{ - .isIncomplete = false, - .items = document_store.enum_completions.completions.items, - }, - }, - }), - .label => try completeLabel(id, pos_index, handle, this_config), - else => try respondGeneric(id, no_completions_response), - } - } else { - try respondGeneric(id, no_completions_response); - } - } else if (std.mem.eql(u8, method, "textDocument/signatureHelp")) { - // TODO: Implement this - try respondGeneric(id, - \\,"result":{"signatures":[]}} - ); - } else if (std.mem.eql(u8, method, "textDocument/definition") or - std.mem.eql(u8, method, "textDocument/declaration") or - std.mem.eql(u8, method, "textDocument/typeDefinition") or - std.mem.eql(u8, method, "textDocument/implementation")) - { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - const position = params.getValue("position").?.Object; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); - return try respondGeneric(id, null_result_response); - }; - - const pos = types.Position{ - .line = position.getValue("line").?.Integer, - .character = position.getValue("character").?.Integer, - }; - if (pos.character >= 0) { - const resolve_alias = !std.mem.eql(u8, method, "textDocument/declaration"); - const pos_index = try handle.document.positionToIndex(pos); - const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); - - switch (pos_context) { - .var_access => try gotoDefinitionGlobal(id, pos_index, handle, configFromUriOr(uri, config), resolve_alias), - .field_access => |range| try gotoDefinitionFieldAccess(id, handle, pos, range, configFromUriOr(uri, config), resolve_alias), - .string_literal => try gotoDefinitionString(id, pos_index, handle, config), - .label => try gotoDefinitionLabel(id, pos_index, handle, configFromUriOr(uri, config)), - else => try respondGeneric(id, null_result_response), - } - } else { - try respondGeneric(id, null_result_response); - } - } else if (std.mem.eql(u8, method, "textDocument/hover")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - const position = params.getValue("position").?.Object; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); - return try respondGeneric(id, null_result_response); - }; - - const pos = types.Position{ - .line = position.getValue("line").?.Integer, - .character = position.getValue("character").?.Integer, - }; - if (pos.character >= 0) { - const pos_index = try handle.document.positionToIndex(pos); - const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); - - switch (pos_context) { - .var_access => try hoverDefinitionGlobal(id, pos_index, handle, configFromUriOr(uri, config)), - .field_access => |range| try hoverDefinitionFieldAccess(id, handle, pos, range, configFromUriOr(uri, config)), - .label => try hoverDefinitionLabel(id, pos_index, handle, configFromUriOr(uri, config)), - else => try respondGeneric(id, null_result_response), - } - } else { - try respondGeneric(id, null_result_response); - } - } else if (std.mem.eql(u8, method, "textDocument/documentSymbol")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); - return try respondGeneric(id, null_result_response); - }; - - try documentSymbol(id, handle); - } else if (std.mem.eql(u8, method, "textDocument/formatting")) { - if (config.zig_exe_path) |zig_exe_path| { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); - return try respondGeneric(id, null_result_response); - }; - - var process = try std.ChildProcess.init(&[_][]const u8{ zig_exe_path, "fmt", "--stdin" }, allocator); - defer process.deinit(); - process.stdin_behavior = .Pipe; - process.stdout_behavior = .Pipe; - - process.spawn() catch |err| { - std.log.debug(.main, "Failed to spawn zig fmt process, error: {}\n", .{err}); - return try respondGeneric(id, null_result_response); - }; - try process.stdin.?.writeAll(handle.document.text); - process.stdin.?.close(); - process.stdin = null; - - const stdout_bytes = try process.stdout.?.reader().readAllAlloc(allocator, std.math.maxInt(usize)); - defer allocator.free(stdout_bytes); - - switch (try process.wait()) { - .Exited => |code| if (code == 0) { - try send(types.Response{ - .id = id, - .result = .{ - .TextEdits = &[1]types.TextEdit{ - .{ - .range = handle.document.range(), - .newText = stdout_bytes, - }, + inline for (method_map) |method_info| { + if (std.mem.eql(u8, method_info[0], method)) { + if (method_info[1].len != 0) { + const info = method_info[1]; + if (info[0] != void) { + const request_obj = requests.fromDynamicTree(arena, info[0], tree.root) catch |err| { + switch (err) { + error.MalformedJson => { + std.log.debug(.main, "Could not create request type {} from JSON {}\n", .{ @typeName(info[0]), json }); + // @TODO What should we return to the client in this case? + return; }, - }, - }); - }, - else => {}, + error.OutOfMemory => return err, + } + }; + return try info[1](arena, id, request_obj, config); + } else { + return try info[1](arena, id, config); + } } } - return try respondGeneric(id, null_result_response); - } else if (std.mem.eql(u8, method, "textDocument/rename")) { - const params = root.Object.getValue("params").?.Object; - const document = params.getValue("textDocument").?.Object; - const uri = document.getValue("uri").?.String; - const position = params.getValue("position").?.Object; - - const handle = document_store.getHandle(uri) orelse { - std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); - return try respondGeneric(id, null_result_response); - }; - - const pos = types.Position{ - .line = position.getValue("line").?.Integer, - .character = position.getValue("character").?.Integer, - }; - if (pos.character >= 0) { - const new_name = params.getValue("newName").?.String; - const pos_index = try handle.document.positionToIndex(pos); - const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); - - const this_config = configFromUriOr(uri, config); - switch (pos_context) { - .var_access => try renameDefinitionGlobal(id, handle, pos_index, new_name), - .field_access => |range| try renameDefinitionFieldAccess(id, handle, pos, range, new_name, this_config), - .label => try renameDefinitionLabel(id, handle, pos_index, new_name), - else => try respondGeneric(id, null_result_response), - } - } else { - try respondGeneric(id, null_result_response); - } - } else if (std.mem.eql(u8, method, "textDocument/references") or - std.mem.eql(u8, method, "textDocument/documentHighlight") or - std.mem.eql(u8, method, "textDocument/codeAction") or - std.mem.eql(u8, method, "textDocument/codeLens") or - std.mem.eql(u8, method, "textDocument/documentLink") or - std.mem.eql(u8, method, "textDocument/rangeFormatting") or - std.mem.eql(u8, method, "textDocument/onTypeFormatting") or - std.mem.eql(u8, method, "textDocument/prepareRename") or - std.mem.eql(u8, method, "textDocument/foldingRange") or - std.mem.eql(u8, method, "textDocument/selectionRange")) - { - // TODO: Unimplemented methods, implement them and add them to server capabilities. - try respondGeneric(id, null_result_response); - } else if (root.Object.getValue("id")) |_| { - std.log.debug(.main, "Method with return value not implemented: {}", .{method}); - try respondGeneric(id, not_implemented_response); - } else { - std.log.debug(.main, "Method without return value not implemented: {}", .{method}); } + + // if (std.mem.eql(u8, method, "textDocument/completion")) { + // const params = root.Object.getValue("params").?.Object; + // const text_document = params.getValue("textDocument").?.Object; + // const uri = text_document.getValue("uri").?.String; + // const position = params.getValue("position").?.Object; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to complete in non existent document {}", .{uri}); + // return try respondGeneric(id, no_completions_response); + // }; + + // const pos = types.Position{ + // .line = position.getValue("line").?.Integer, + // .character = position.getValue("character").?.Integer, + // }; + // if (pos.character >= 0) { + // const pos_index = try handle.document.positionToIndex(pos); + // const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); + + // const this_config = configFromUriOr(uri, config); + // const use_snippets = this_config.enable_snippets and client_capabilities.supports_snippets; + // switch (pos_context) { + // .builtin => try send(types.Response{ + // .id = id, + // .result = .{ + // .CompletionList = .{ + // .isIncomplete = false, + // .items = builtin_completions[@boolToInt(use_snippets)][0..], + // }, + // }, + // }), + // .var_access, .empty => try completeGlobal(id, pos_index, handle, this_config), + // .field_access => |range| try completeFieldAccess(id, handle, pos, range, this_config), + // .global_error_set => try send(types.Response{ + // .id = id, + // .result = .{ + // .CompletionList = .{ + // .isIncomplete = false, + // .items = document_store.error_completions.completions.items, + // }, + // }, + // }), + // .enum_literal => try send(types.Response{ + // .id = id, + // .result = .{ + // .CompletionList = .{ + // .isIncomplete = false, + // .items = document_store.enum_completions.completions.items, + // }, + // }, + // }), + // .label => try completeLabel(id, pos_index, handle, this_config), + // else => try respondGeneric(id, no_completions_response), + // } + // } else { + // try respondGeneric(id, no_completions_response); + // } + // } else if (std.mem.eql(u8, method, "textDocument/signatureHelp")) { + // // TODO: Implement this + // try respondGeneric(id, + // \\,"result":{"signatures":[]}} + // ); + // } else if (std.mem.eql(u8, method, "textDocument/definition") or + // std.mem.eql(u8, method, "textDocument/declaration") or + // std.mem.eql(u8, method, "textDocument/typeDefinition") or + // std.mem.eql(u8, method, "textDocument/implementation")) + // { + // const params = root.Object.getValue("params").?.Object; + // const document = params.getValue("textDocument").?.Object; + // const uri = document.getValue("uri").?.String; + // const position = params.getValue("position").?.Object; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); + // return try respondGeneric(id, null_result_response); + // }; + + // const pos = types.Position{ + // .line = position.getValue("line").?.Integer, + // .character = position.getValue("character").?.Integer, + // }; + // if (pos.character >= 0) { + // const resolve_alias = !std.mem.eql(u8, method, "textDocument/declaration"); + // const pos_index = try handle.document.positionToIndex(pos); + // const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); + + // switch (pos_context) { + // .var_access => try gotoDefinitionGlobal(id, pos_index, handle, configFromUriOr(uri, config), resolve_alias), + // .field_access => |range| try gotoDefinitionFieldAccess(id, handle, pos, range, configFromUriOr(uri, config), resolve_alias), + // .string_literal => try gotoDefinitionString(id, pos_index, handle, config), + // .label => try gotoDefinitionLabel(id, pos_index, handle, configFromUriOr(uri, config)), + // else => try respondGeneric(id, null_result_response), + // } + // } else { + // try respondGeneric(id, null_result_response); + // } + // } else if (std.mem.eql(u8, method, "textDocument/hover")) { + // const params = root.Object.getValue("params").?.Object; + // const document = params.getValue("textDocument").?.Object; + // const uri = document.getValue("uri").?.String; + // const position = params.getValue("position").?.Object; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); + // return try respondGeneric(id, null_result_response); + // }; + + // const pos = types.Position{ + // .line = position.getValue("line").?.Integer, + // .character = position.getValue("character").?.Integer, + // }; + // if (pos.character >= 0) { + // const pos_index = try handle.document.positionToIndex(pos); + // const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); + + // switch (pos_context) { + // .var_access => try hoverDefinitionGlobal(id, pos_index, handle, configFromUriOr(uri, config)), + // .field_access => |range| try hoverDefinitionFieldAccess(id, handle, pos, range, configFromUriOr(uri, config)), + // .label => try hoverDefinitionLabel(id, pos_index, handle, configFromUriOr(uri, config)), + // else => try respondGeneric(id, null_result_response), + // } + // } else { + // try respondGeneric(id, null_result_response); + // } + // } else if (std.mem.eql(u8, method, "textDocument/documentSymbol")) { + // const params = root.Object.getValue("params").?.Object; + // const document = params.getValue("textDocument").?.Object; + // const uri = document.getValue("uri").?.String; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); + // return try respondGeneric(id, null_result_response); + // }; + + // try documentSymbol(id, handle); + // } else if (std.mem.eql(u8, method, "textDocument/formatting")) { + // if (config.zig_exe_path) |zig_exe_path| { + // const params = root.Object.getValue("params").?.Object; + // const document = params.getValue("textDocument").?.Object; + // const uri = document.getValue("uri").?.String; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); + // return try respondGeneric(id, null_result_response); + // }; + + // var process = try std.ChildProcess.init(&[_][]const u8{ zig_exe_path, "fmt", "--stdin" }, allocator); + // defer process.deinit(); + // process.stdin_behavior = .Pipe; + // process.stdout_behavior = .Pipe; + + // process.spawn() catch |err| { + // std.log.debug(.main, "Failed to spawn zig fmt process, error: {}\n", .{err}); + // return try respondGeneric(id, null_result_response); + // }; + // try process.stdin.?.writeAll(handle.document.text); + // process.stdin.?.close(); + // process.stdin = null; + + // const stdout_bytes = try process.stdout.?.reader().readAllAlloc(allocator, std.math.maxInt(usize)); + // defer allocator.free(stdout_bytes); + + // switch (try process.wait()) { + // .Exited => |code| if (code == 0) { + // try send(types.Response{ + // .id = id, + // .result = .{ + // .TextEdits = &[1]types.TextEdit{ + // .{ + // .range = handle.document.range(), + // .newText = stdout_bytes, + // }, + // }, + // }, + // }); + // }, + // else => {}, + // } + // } + // return try respondGeneric(id, null_result_response); + // } else if (std.mem.eql(u8, method, "textDocument/rename")) { + // const params = root.Object.getValue("params").?.Object; + // const document = params.getValue("textDocument").?.Object; + // const uri = document.getValue("uri").?.String; + // const position = params.getValue("position").?.Object; + + // const handle = document_store.getHandle(uri) orelse { + // std.log.debug(.main, "Trying to got to definition in non existent document {}", .{uri}); + // return try respondGeneric(id, null_result_response); + // }; + + // const pos = types.Position{ + // .line = position.getValue("line").?.Integer, + // .character = position.getValue("character").?.Integer, + // }; + // if (pos.character >= 0) { + // const new_name = params.getValue("newName").?.String; + // const pos_index = try handle.document.positionToIndex(pos); + // const pos_context = try analysis.documentPositionContext(allocator, handle.document, pos); + + // const this_config = configFromUriOr(uri, config); + // switch (pos_context) { + // .var_access => try renameDefinitionGlobal(id, handle, pos_index, new_name), + // .field_access => |range| try renameDefinitionFieldAccess(id, handle, pos, range, new_name, this_config), + // .label => try renameDefinitionLabel(id, handle, pos_index, new_name), + // else => try respondGeneric(id, null_result_response), + // } + // } else { + // try respondGeneric(id, null_result_response); + // } + // } else if (std.mem.eql(u8, method, "textDocument/references") or + // std.mem.eql(u8, method, "textDocument/documentHighlight") or + // std.mem.eql(u8, method, "textDocument/codeAction") or + // std.mem.eql(u8, method, "textDocument/codeLens") or + // std.mem.eql(u8, method, "textDocument/documentLink") or + // std.mem.eql(u8, method, "textDocument/rangeFormatting") or + // std.mem.eql(u8, method, "textDocument/onTypeFormatting") or + // std.mem.eql(u8, method, "textDocument/prepareRename") or + // std.mem.eql(u8, method, "textDocument/foldingRange") or + // std.mem.eql(u8, method, "textDocument/selectionRange")) + // { + // // TODO: Unimplemented methods, implement them and add them to server capabilities. + // try respondGeneric(id, null_result_response); + // } else if (root.Object.getValue("id")) |_| { + // std.log.debug(.main, "Method with return value not implemented: {}", .{method}); + // try respondGeneric(id, not_implemented_response); + // } else { + // std.log.debug(.main, "Method without return value not implemented: {}", .{method}); + // } } var debug_alloc_state: DebugAllocator = undefined; @@ -1545,18 +1531,22 @@ pub fn main() anyerror!void { var json_parser = std.json.Parser.init(allocator, false); defer json_parser.deinit(); - var keep_running = true; + // Arena used for temporary allocations while handlign a request + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + while (keep_running) { - const headers = readRequestHeader(allocator, reader) catch |err| { + const headers = readRequestHeader(&arena.allocator, reader) catch |err| { std.log.debug(.main, "{}; exiting!", .{@errorName(err)}); return; }; - defer headers.deinit(allocator); - const buf = try allocator.alloc(u8, headers.content_length); - defer allocator.free(buf); + const buf = try arena.allocator.alloc(u8, headers.content_length); try reader.readNoEof(buf); - try processJsonRpc(&json_parser, buf, config, &keep_running); + + try processJsonRpc(&arena, &json_parser, buf, config); json_parser.reset(); + arena.deinit(); + arena.state.buffer_list = .{}; if (debug_alloc) |dbg| { std.log.debug(.main, "{}\n", .{dbg.info}); diff --git a/src/requests.zig b/src/requests.zig new file mode 100644 index 0000000..efd4f06 --- /dev/null +++ b/src/requests.zig @@ -0,0 +1,188 @@ +const std = @import("std"); +const types = @import("types.zig"); + +/// Only check for the field's existence. +const Exists = struct { + exists: bool, +}; + +fn Default(comptime T: type, comptime default_value: T) type { + return struct { + pub const value_type = T; + pub const default = default_value; + value: T, + }; +} + +fn Transform(comptime Original: type, comptime transform_fn: var) type { + return struct { + pub const original_type = Original; + pub const transform = transform_fn; + + value: @TypeOf(transform(@as(Original, undefined))), + }; +} + +inline fn fromDynamicTreeInternal(arena: *std.heap.ArenaAllocator, value: std.json.Value, out: var) error{ MalformedJson, OutOfMemory }!void { + const T = comptime std.meta.Child(@TypeOf(out)); + + if (comptime std.meta.trait.is(.Struct)(T)) { + if (value != .Object) return error.MalformedJson; + + var err = false; + inline for (std.meta.fields(T)) |field| { + const is_exists = field.field_type == Exists; + + const is_optional = comptime std.meta.trait.is(.Optional)(field.field_type); + const actual_type = if (is_optional) std.meta.Child(field.field_type) else field.field_type; + + const is_struct = comptime std.meta.trait.is(.Struct)(actual_type); + const is_default = comptime if (is_struct) std.meta.trait.hasDecls(actual_type, .{ "default", "value_type" }) else false; + const is_transform = comptime if (is_struct) std.meta.trait.hasDecls(actual_type, .{ "original_type", "transform" }) else false; + + if (value.Object.getValue(field.name)) |json_field| { + if (is_exists) { + @field(out, field.name) = Exists{ .exists = true }; + } else if (is_transform) { + var original_value: actual_type.original_type = undefined; + try fromDynamicTreeInternal(arena, json_field, &original_value); + @field(out, field.name) = actual_type{ .value = actual_type.transform(original_value) catch return error.MalformedJson }; + } else if (is_default) { + try fromDynamicTreeInternal(arena, json_field, &@field(out, field.name).value); + } else if (is_optional) { + if (json_field == .Null) { + @field(out, field.name) = null; + } else { + var actual_value: actual_type = undefined; + try fromDynamicTreeInternal(arena, json_field, &actual_value); + @field(out, field.name) = actual_value; + } + } else { + try fromDynamicTreeInternal(arena, json_field, &@field(out, field.name)); + } + } else { + if (is_exists) { + @field(out, field.name) = Exists{ .exists = false }; + } else if (is_optional) { + @field(out, field.name) = null; + } else if (is_default) { + @field(out, field.name) = actual_type{ .value = actual_type.default }; + } else { + err = true; + } + } + } + if (err) return error.MalformedJson; + } else if (comptime (std.meta.trait.isSlice(T) and T != []const u8)) { + if (value != .Array) return error.MalformedJson; + const Child = std.meta.Child(T); + + if (value.Array.items.len == 0) { + out.* = &[0]Child{}; + } else { + var slice = try arena.allocator.alloc(Child, value.Array.items.len); + for (value.Array.items) |arr_item, idx| { + try fromDynamicTreeInternal(arena, arr_item, &slice[idx]); + } + out.* = slice; + } + } else if (T == std.json.Value) { + out.* = value; + } else { + switch (T) { + bool => { + if (value != .Bool) return error.MalformedJson; + out.* = value.Bool; + }, + i64 => { + if (value != .Integer) return error.MalformedJson; + out.* = value.Integer; + }, + f64 => { + if (value != .Float) return error.MalformedJson; + out.* = value.Float; + }, + []const u8 => { + if (value != .String) return error.MalformedJson; + out.* = value.String; + }, + else => @compileError("Invalid type " ++ @typeName(T)), + } + } +} + +pub fn fromDynamicTree(arena: *std.heap.ArenaAllocator, comptime T: type, value: std.json.Value) error{ MalformedJson, OutOfMemory }!T { + var out: T = undefined; + try fromDynamicTreeInternal(arena, value, &out); + return out; +} + +//! This file contains request types zls handles. +//! Note that the parameter types may be incomplete. +//! We only define what we actually use. + +const MaybeStringArray = Default([]const types.String, &[0]types.String{}); + +pub const Initialize = struct { + pub const ClientCapabilities = struct { + workspace: ?struct { + workspaceFolders: Default(bool, false), + }, + textDocument: ?struct { + semanticTokens: Exists, + hover: ?struct { + contentFormat: MaybeStringArray, + }, + completion: ?struct { + completionItem: ?struct { + snippetSupport: Default(bool, false), + documentationFormat: MaybeStringArray, + }, + }, + }, + }; + + params: struct { + capabilities: ClientCapabilities, + workspaceFolders: ?[]const types.WorkspaceFolder, + }, +}; + +pub const WorkspaceFoldersChange = struct { + params: struct { + event: struct { + added: []const types.WorkspaceFolder, + removed: []const types.WorkspaceFolder, + }, + }, +}; + +pub const OpenDocument = struct { + params: struct { + textDocument: struct { + uri: types.String, + text: types.String, + }, + }, +}; + +const TextDocumentIdentifier = struct { + uri: types.String, +}; + +pub const ChangeDocument = struct { + params: struct { + textDocument: TextDocumentIdentifier, + contentChanges: std.json.Value, + }, +}; + +const TextDocumentIdentifierRequest = struct { + params: struct { + textDocument: TextDocumentIdentifier, + }, +}; + +pub const SaveDocument = TextDocumentIdentifierRequest; +pub const CloseDocument = TextDocumentIdentifierRequest; +pub const SemanticTokens = TextDocumentIdentifierRequest; diff --git a/src/semantic_tokens.zig b/src/semantic_tokens.zig index d408a26..92a493d 100644 --- a/src/semantic_tokens.zig +++ b/src/semantic_tokens.zig @@ -722,11 +722,8 @@ fn writeNodeTokens(builder: *Builder, arena: *std.heap.ArenaAllocator, store: *D } // TODO Range version, edit version. -pub fn writeAllSemanticTokens(allocator: *std.mem.Allocator, store: *DocumentStore, handle: *DocumentStore.Handle) ![]u32 { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var builder = Builder.init(allocator, handle); - try writeNodeTokens(&builder, &arena, store, &handle.tree.root_node.base); +pub fn writeAllSemanticTokens(arena: *std.heap.ArenaAllocator, store: *DocumentStore, handle: *DocumentStore.Handle) ![]u32 { + var builder = Builder.init(arena.child_allocator, handle); + try writeNodeTokens(&builder, arena, store, &handle.tree.root_node.base); return builder.toOwnedSlice(); } diff --git a/src/types.zig b/src/types.zig index c8d3bc1..8cbd937 100644 --- a/src/types.zig +++ b/src/types.zig @@ -358,9 +358,21 @@ const SymbolKind = enum { }; pub const DocumentSymbol = struct { - name: String, detail: ?String = null, kind: SymbolKind, deprecated: bool = false, range: Range, selectionRange: Range, children: []DocumentSymbol = &[_]DocumentSymbol{} + name: String, + detail: ?String = null, + kind: SymbolKind, + deprecated: bool = false, + range: Range, + selectionRange: Range, + children: []DocumentSymbol = &[_]DocumentSymbol{}, }; pub const ShowMessageParams = struct { - type: MessageType, message: String + type: MessageType, + message: String, +}; + +pub const WorkspaceFolder = struct { + uri: DocumentUri, + name: String, };