|
| 1 | +//! Implements a language server using the `lsp.basic_server` abstraction. |
| 2 | + |
| 3 | +const std = @import("std"); |
| 4 | +const builtin = @import("builtin"); |
| 5 | +const lsp = @import("lsp"); |
| 6 | + |
| 7 | +var debug_allocator: std.heap.DebugAllocator(.{}) = .init; |
| 8 | +var log_transport: ?lsp.AnyTransport = null; |
| 9 | + |
| 10 | +pub fn main() !void { |
| 11 | + const gpa, const is_debug = switch (builtin.mode) { |
| 12 | + .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true }, |
| 13 | + .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false }, |
| 14 | + }; |
| 15 | + defer if (is_debug) { |
| 16 | + _ = debug_allocator.deinit(); |
| 17 | + }; |
| 18 | + |
| 19 | + // language server typically communicate over stdio (stdin and stdout) |
| 20 | + var transport: lsp.TransportOverStdio = .init(std.io.getStdIn(), std.io.getStdOut()); |
| 21 | + |
| 22 | + var handler: Handler = .init(gpa); |
| 23 | + defer handler.deinit(); |
| 24 | + |
| 25 | + try lsp.basic_server.run( |
| 26 | + gpa, |
| 27 | + transport.any(), |
| 28 | + &handler, |
| 29 | + std.log.err, |
| 30 | + ); |
| 31 | +} |
| 32 | + |
| 33 | +// Most functions can be omitted if you are not interested them. A unhandled request will automatically return a 'null' response. |
| 34 | + |
| 35 | +pub const Handler = struct { |
| 36 | + allocator: std.mem.Allocator, |
| 37 | + files: std.StringHashMapUnmanaged([]u8), |
| 38 | + offset_encoding: lsp.offsets.Encoding, |
| 39 | + |
| 40 | + fn init(allocator: std.mem.Allocator) Handler { |
| 41 | + return .{ |
| 42 | + .allocator = allocator, |
| 43 | + .files = .{}, |
| 44 | + .offset_encoding = .@"utf-16", |
| 45 | + }; |
| 46 | + } |
| 47 | + |
| 48 | + fn deinit(handler: *Handler) void { |
| 49 | + var file_it = handler.files.iterator(); |
| 50 | + while (file_it.next()) |entry| { |
| 51 | + handler.allocator.free(entry.key_ptr.*); |
| 52 | + handler.allocator.free(entry.value_ptr.*); |
| 53 | + } |
| 54 | + |
| 55 | + handler.files.deinit(handler.allocator); |
| 56 | + |
| 57 | + handler.* = undefined; |
| 58 | + } |
| 59 | + |
| 60 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize |
| 61 | + pub fn initialize( |
| 62 | + handler: *Handler, |
| 63 | + _: std.mem.Allocator, |
| 64 | + request: lsp.types.InitializeParams, |
| 65 | + ) lsp.types.InitializeResult { |
| 66 | + std.log.debug("Received 'initialize' message", .{}); |
| 67 | + |
| 68 | + if (request.clientInfo) |client_info| { |
| 69 | + std.log.info("The client is '{s}' ({s})", .{ client_info.name, client_info.version orelse "unknown version" }); |
| 70 | + } |
| 71 | + |
| 72 | + // Specifies which features are supported by the client/editor. |
| 73 | + const client_capabilities: lsp.types.ClientCapabilities = request.capabilities; |
| 74 | + |
| 75 | + // Pick the client's favorite character offset encoding. |
| 76 | + if (client_capabilities.general) |general| { |
| 77 | + for (general.positionEncodings orelse &.{}) |encoding| { |
| 78 | + handler.offset_encoding = switch (encoding) { |
| 79 | + .@"utf-8" => .@"utf-8", |
| 80 | + .@"utf-16" => .@"utf-16", |
| 81 | + .@"utf-32" => .@"utf-32", |
| 82 | + .custom_value => continue, |
| 83 | + }; |
| 84 | + break; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + // Specifies which features are supported by the language server. |
| 89 | + const server_capabilities: lsp.types.ServerCapabilities = .{ |
| 90 | + .positionEncoding = switch (handler.offset_encoding) { |
| 91 | + .@"utf-8" => .@"utf-8", |
| 92 | + .@"utf-16" => .@"utf-16", |
| 93 | + .@"utf-32" => .@"utf-32", |
| 94 | + }, |
| 95 | + .textDocumentSync = .{ |
| 96 | + .TextDocumentSyncOptions = .{ |
| 97 | + .openClose = true, |
| 98 | + .change = .Full, |
| 99 | + }, |
| 100 | + }, |
| 101 | + .hoverProvider = .{ .bool = true }, |
| 102 | + }; |
| 103 | + |
| 104 | + // Tries to validate that our server capabilities are actually implemented. |
| 105 | + if (@import("builtin").mode == .Debug) { |
| 106 | + lsp.basic_server.validateServerCapabilities(Handler, server_capabilities); |
| 107 | + } |
| 108 | + |
| 109 | + return .{ |
| 110 | + .serverInfo = .{ |
| 111 | + .name = "My first LSP", |
| 112 | + .version = "0.1.0", |
| 113 | + }, |
| 114 | + .capabilities = server_capabilities, |
| 115 | + }; |
| 116 | + } |
| 117 | + |
| 118 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized |
| 119 | + pub fn initialized( |
| 120 | + _: *Handler, |
| 121 | + _: std.mem.Allocator, |
| 122 | + _: lsp.types.InitializedParams, |
| 123 | + ) void { |
| 124 | + std.log.debug("Received 'initialized' notification", .{}); |
| 125 | + } |
| 126 | + |
| 127 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown |
| 128 | + pub fn shutdown( |
| 129 | + _: *Handler, |
| 130 | + _: std.mem.Allocator, |
| 131 | + _: void, |
| 132 | + ) ?void { |
| 133 | + std.log.debug("Received 'shutdown' request", .{}); |
| 134 | + return null; |
| 135 | + } |
| 136 | + |
| 137 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#exit |
| 138 | + /// The `lsp.basic_server.run` function will automatically return after this function completes. |
| 139 | + pub fn exit( |
| 140 | + _: *Handler, |
| 141 | + _: std.mem.Allocator, |
| 142 | + _: void, |
| 143 | + ) void { |
| 144 | + std.log.debug("Received 'exit' notification", .{}); |
| 145 | + } |
| 146 | + |
| 147 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didOpen |
| 148 | + pub fn @"textDocument/didOpen"( |
| 149 | + self: *Handler, |
| 150 | + _: std.mem.Allocator, |
| 151 | + notification: lsp.types.DidOpenTextDocumentParams, |
| 152 | + ) !void { |
| 153 | + std.log.debug("Received 'textDocument/didOpen' notification", .{}); |
| 154 | + |
| 155 | + const new_text = try self.allocator.dupe(u8, notification.textDocument.text); |
| 156 | + errdefer self.allocator.free(new_text); |
| 157 | + |
| 158 | + const gop = try self.files.getOrPut(self.allocator, notification.textDocument.uri); |
| 159 | + |
| 160 | + if (gop.found_existing) { |
| 161 | + std.log.warn("Document opened twice: '{s}'", .{notification.textDocument.uri}); |
| 162 | + self.allocator.free(gop.value_ptr.*); |
| 163 | + } else { |
| 164 | + errdefer std.debug.assert(self.files.remove(notification.textDocument.uri)); |
| 165 | + gop.key_ptr.* = try self.allocator.dupe(u8, notification.textDocument.uri); |
| 166 | + } |
| 167 | + |
| 168 | + gop.value_ptr.* = new_text; |
| 169 | + } |
| 170 | + |
| 171 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didChange |
| 172 | + pub fn @"textDocument/didChange"( |
| 173 | + self: *Handler, |
| 174 | + _: std.mem.Allocator, |
| 175 | + notification: lsp.types.DidChangeTextDocumentParams, |
| 176 | + ) !void { |
| 177 | + std.log.debug("Received 'textDocument/didChange' notification", .{}); |
| 178 | + |
| 179 | + const current_text = self.files.getPtr(notification.textDocument.uri) orelse { |
| 180 | + std.log.warn("Modifying non existent Document: '{s}'", .{notification.textDocument.uri}); |
| 181 | + return; |
| 182 | + }; |
| 183 | + |
| 184 | + var buffer: std.ArrayListUnmanaged(u8) = .empty; |
| 185 | + errdefer buffer.deinit(self.allocator); |
| 186 | + |
| 187 | + try buffer.appendSlice(self.allocator, current_text.*); |
| 188 | + |
| 189 | + for (notification.contentChanges) |content_change| { |
| 190 | + switch (content_change) { |
| 191 | + .literal_1 => |change| { |
| 192 | + buffer.clearRetainingCapacity(); |
| 193 | + try buffer.appendSlice(self.allocator, change.text); |
| 194 | + }, |
| 195 | + .literal_0 => |change| { |
| 196 | + const loc = lsp.offsets.rangeToLoc(buffer.items, change.range, self.offset_encoding); |
| 197 | + try buffer.replaceRange(self.allocator, loc.start, loc.end - loc.start, change.text); |
| 198 | + }, |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + const new_text = try buffer.toOwnedSlice(self.allocator); |
| 203 | + self.allocator.free(current_text.*); |
| 204 | + current_text.* = new_text; |
| 205 | + } |
| 206 | + |
| 207 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didClose |
| 208 | + pub fn @"textDocument/didClose"( |
| 209 | + self: *Handler, |
| 210 | + _: std.mem.Allocator, |
| 211 | + notification: lsp.types.DidCloseTextDocumentParams, |
| 212 | + ) !void { |
| 213 | + std.log.debug("Received 'textDocument/didClose' notification", .{}); |
| 214 | + |
| 215 | + const entry = self.files.fetchRemove(notification.textDocument.uri) orelse { |
| 216 | + std.log.warn("Closing non existent Document: '{s}'", .{notification.textDocument.uri}); |
| 217 | + return; |
| 218 | + }; |
| 219 | + self.allocator.free(entry.key); |
| 220 | + self.allocator.free(entry.value); |
| 221 | + } |
| 222 | + |
| 223 | + /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover |
| 224 | + /// |
| 225 | + /// This function can be omitted if you are not interested in this request. A `null` response will be automatically send back. |
| 226 | + pub fn @"textDocument/hover"( |
| 227 | + handler: *Handler, |
| 228 | + _: std.mem.Allocator, |
| 229 | + params: lsp.types.HoverParams, |
| 230 | + ) ?lsp.types.Hover { |
| 231 | + std.log.debug("Received 'textDocument/hover' request", .{}); |
| 232 | + |
| 233 | + const source = handler.files.get(params.textDocument.uri) orelse |
| 234 | + return null; // We should actually read the document from the file system |
| 235 | + |
| 236 | + const source_index = lsp.offsets.positionToIndex(source, params.position, handler.offset_encoding); |
| 237 | + std.log.debug("Hover position: line={d}, character={d}, index={d}", .{ params.position.line, params.position.character, source_index }); |
| 238 | + |
| 239 | + return .{ |
| 240 | + .contents = .{ |
| 241 | + .MarkupContent = .{ |
| 242 | + .kind = .plaintext, |
| 243 | + .value = "I don't know what you are hovering over but I'd like to point out that you have a nice editor theme", |
| 244 | + }, |
| 245 | + }, |
| 246 | + }; |
| 247 | + } |
| 248 | + |
| 249 | + /// We received a response message from the client/editor. |
| 250 | + pub fn onResponse( |
| 251 | + _: *Handler, |
| 252 | + _: std.mem.Allocator, |
| 253 | + response: lsp.JsonRPCMessage.Response, |
| 254 | + ) void { |
| 255 | + // We didn't make any requests to the client/editor. |
| 256 | + std.log.warn("received unexpected response from client with id '{?}'!", .{response.id}); |
| 257 | + } |
| 258 | +}; |
0 commit comments