├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── Header.zig ├── Server.zig ├── Transport.zig ├── lsp.zig ├── main.zig └── offsets.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | *.zon text=auto eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 zigtools contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig-LSP-Sample 2 | 3 | An example LSP implementation in Zig. 4 | 5 | This code is a very stripped-down version of [ZLS](https://github.com/zigtools/zls). -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const exe = b.addExecutable(.{ 8 | .name = "zig-lsp-sample", 9 | .root_source_file = .{ .path = "src/main.zig" }, 10 | .target = target, 11 | .optimize = optimize, 12 | .single_threaded = true, 13 | }); 14 | b.installArtifact(exe); 15 | } 16 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zig-lsp-sample", 3 | .version = "0.11.0", 4 | .dependencies = .{}, 5 | .paths = .{""}, 6 | } 7 | -------------------------------------------------------------------------------- /src/Header.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Header = @This(); 4 | 5 | content_length: usize, 6 | 7 | /// null implies "application/vscode-jsonrpc; charset=utf-8" 8 | content_type: ?[]const u8 = null, 9 | 10 | pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { 11 | if (self.content_type) |ct| allocator.free(ct); 12 | } 13 | 14 | // Caller owns returned memory. 15 | pub fn parse(allocator: std.mem.Allocator, reader: anytype) !Header { 16 | var r = Header{ 17 | .content_length = undefined, 18 | .content_type = null, 19 | }; 20 | errdefer r.deinit(allocator); 21 | 22 | var has_content_length = false; 23 | while (true) { 24 | const header = try reader.readUntilDelimiterAlloc(allocator, '\n', 0x100); 25 | defer allocator.free(header); 26 | if (header.len == 0 or header[header.len - 1] != '\r') return error.MissingCarriageReturn; 27 | if (header.len == 1) break; 28 | 29 | const header_name = header[0 .. std.mem.indexOf(u8, header, ": ") orelse return error.MissingColon]; 30 | const header_value = header[header_name.len + 2 .. header.len - 1]; 31 | if (std.mem.eql(u8, header_name, "Content-Length")) { 32 | if (header_value.len == 0) return error.MissingHeaderValue; 33 | r.content_length = std.fmt.parseInt(usize, header_value, 10) catch return error.InvalidContentLength; 34 | has_content_length = true; 35 | } else if (std.mem.eql(u8, header_name, "Content-Type")) { 36 | r.content_type = try allocator.dupe(u8, header_value); 37 | } else { 38 | return error.UnknownHeader; 39 | } 40 | } 41 | if (!has_content_length) return error.MissingContentLength; 42 | 43 | return r; 44 | } 45 | -------------------------------------------------------------------------------- /src/Server.zig: -------------------------------------------------------------------------------- 1 | const Server = @This(); 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | const types = @import("lsp.zig"); 6 | const offsets = @import("offsets.zig"); 7 | const Transport = @import("Transport.zig"); 8 | 9 | allocator: std.mem.Allocator, 10 | transport: *Transport, 11 | offset_encoding: offsets.Encoding = .@"utf-16", 12 | status: Status = .uninitialized, 13 | client_capabilities: std.json.Parsed(types.ClientCapabilities), 14 | 15 | /// maps document URI's to the document source code 16 | documents: std.StringHashMapUnmanaged([]const u8) = .{}, 17 | 18 | pub const Error = error{ 19 | OutOfMemory, 20 | ParseError, 21 | InvalidRequest, 22 | MethodNotFound, 23 | InvalidParams, 24 | InternalError, 25 | /// Error code indicating that a server received a notification or 26 | /// request before the server has received the `initialize` request. 27 | ServerNotInitialized, 28 | /// A request failed but it was syntactically correct, e.g the 29 | /// method name was known and the parameters were valid. The error 30 | /// message should contain human readable information about why 31 | /// the request failed. 32 | /// 33 | /// @since 3.17.0 34 | RequestFailed, 35 | /// The server cancelled the request. This error code should 36 | /// only be used for requests that explicitly support being 37 | /// server cancellable. 38 | /// 39 | /// @since 3.17.0 40 | ServerCancelled, 41 | /// The server detected that the content of a document got 42 | /// modified outside normal conditions. A server should 43 | /// NOT send this error code if it detects a content change 44 | /// in it unprocessed messages. The result even computed 45 | /// on an older state might still be useful for the client. 46 | /// 47 | /// If a client decides that a result is not of any use anymore 48 | /// the client should cancel the request. 49 | ContentModified, 50 | /// The client has canceled a request and a server as detected 51 | /// the cancel. 52 | RequestCancelled, 53 | }; 54 | 55 | pub const Status = enum { 56 | /// the server has not received a `initialize` request 57 | uninitialized, 58 | /// the server has received a `initialize` request and is awaiting the `initialized` notification 59 | initializing, 60 | /// the server has been initialized and is ready to received requests 61 | initialized, 62 | /// the server has been shutdown and can't handle any more requests 63 | shutdown, 64 | /// the server is received a `exit` notification and has been shutdown 65 | exiting_success, 66 | /// the server is received a `exit` notification but has not been shutdown 67 | exiting_failure, 68 | }; 69 | 70 | fn sendToClientResponse(server: *Server, id: types.RequestId, result: anytype) error{OutOfMemory}![]u8 { 71 | // TODO validate result type is a possible response 72 | // TODO validate response is from a client to server request 73 | // TODO validate result type 74 | 75 | return try server.sendToClientInternal(id, null, null, "result", result); 76 | } 77 | 78 | fn sendToClientRequest(server: *Server, id: types.RequestId, method: []const u8, params: anytype) error{OutOfMemory}![]u8 { 79 | std.debug.assert(isRequestMethod(method)); 80 | // TODO validate method is server to client 81 | // TODO validate params type 82 | 83 | return try server.sendToClientInternal(id, method, null, "params", params); 84 | } 85 | 86 | fn sendToClientNotification(server: *Server, method: []const u8, params: anytype) error{OutOfMemory}![]u8 { 87 | std.debug.assert(isNotificationMethod(method)); 88 | // TODO validate method is server to client 89 | // TODO validate params type 90 | 91 | return try server.sendToClientInternal(null, method, null, "params", params); 92 | } 93 | 94 | fn sendToClientResponseError(server: *Server, id: types.RequestId, err: ?types.ResponseError) error{OutOfMemory}![]u8 { 95 | return try server.sendToClientInternal(id, null, err, "", null); 96 | } 97 | 98 | fn sendToClientInternal( 99 | server: *Server, 100 | maybe_id: ?types.RequestId, 101 | maybe_method: ?[]const u8, 102 | maybe_err: ?types.ResponseError, 103 | extra_name: []const u8, 104 | extra: anytype, 105 | ) error{OutOfMemory}![]u8 { 106 | var buffer = std.ArrayListUnmanaged(u8){}; 107 | errdefer buffer.deinit(server.allocator); 108 | var writer = buffer.writer(server.allocator); 109 | try writer.writeAll( 110 | \\{"jsonrpc":"2.0" 111 | ); 112 | if (maybe_id) |id| { 113 | try writer.writeAll( 114 | \\,"id": 115 | ); 116 | try std.json.stringify(id, .{}, writer); 117 | } 118 | if (maybe_method) |method| { 119 | try writer.writeAll( 120 | \\,"method": 121 | ); 122 | try std.json.stringify(method, .{}, writer); 123 | } 124 | switch (@TypeOf(extra)) { 125 | void => {}, 126 | ?void => { 127 | try writer.print( 128 | \\,"{s}":null 129 | , .{extra_name}); 130 | }, 131 | else => { 132 | try writer.print( 133 | \\,"{s}": 134 | , .{extra_name}); 135 | try std.json.stringify(extra, .{ .emit_null_optional_fields = false }, writer); 136 | }, 137 | } 138 | if (maybe_err) |err| { 139 | try writer.writeAll( 140 | \\,"error": 141 | ); 142 | try std.json.stringify(err, .{}, writer); 143 | } 144 | try writer.writeByte('}'); 145 | 146 | server.transport.writeJsonMessage(buffer.items) catch |err| { 147 | std.log.err("failed to write response: {}", .{err}); 148 | }; 149 | return buffer.toOwnedSlice(server.allocator); 150 | } 151 | 152 | fn showMessage( 153 | server: *Server, 154 | message_type: types.MessageType, 155 | comptime fmt: []const u8, 156 | args: anytype, 157 | ) void { 158 | const message = std.fmt.allocPrint(server.allocator, fmt, args) catch return; 159 | defer server.allocator.free(message); 160 | switch (message_type) { 161 | .Error => std.log.err("{s}", .{message}), 162 | .Warning => std.log.warn("{s}", .{message}), 163 | .Info => std.log.info("{s}", .{message}), 164 | .Log => std.log.debug("{s}", .{message}), 165 | } 166 | switch (server.status) { 167 | .initializing, 168 | .initialized, 169 | => {}, 170 | .uninitialized, 171 | .shutdown, 172 | .exiting_success, 173 | .exiting_failure, 174 | => return, 175 | } 176 | if (server.sendToClientNotification("window/showMessage", types.ShowMessageParams{ 177 | .type = message_type, 178 | .message = message, 179 | })) |json_message| { 180 | server.allocator.free(json_message); 181 | } else |err| { 182 | std.log.warn("failed to show message: {}", .{err}); 183 | } 184 | } 185 | 186 | fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.InitializeParams) Error!types.InitializeResult { 187 | if (request.clientInfo) |clientInfo| { 188 | std.log.info("client is '{s}-{s}'", .{ clientInfo.name, clientInfo.version orelse "" }); 189 | } 190 | 191 | // I know... 192 | const capabilities_string = try std.json.stringifyAlloc(server.allocator, request.capabilities, .{}); 193 | defer server.allocator.free(capabilities_string); 194 | server.client_capabilities.deinit(); 195 | server.client_capabilities = std.json.parseFromSlice( 196 | types.ClientCapabilities, 197 | server.allocator, 198 | capabilities_string, 199 | .{ .allocate = .alloc_always }, 200 | ) catch return error.InternalError; 201 | 202 | server.status = .initializing; 203 | 204 | return .{ 205 | .serverInfo = .{ 206 | .name = "zig-lsp-sample", 207 | .version = null, 208 | }, 209 | .capabilities = .{ 210 | .positionEncoding = switch (server.offset_encoding) { 211 | .@"utf-8" => .@"utf-8", 212 | .@"utf-16" => .@"utf-16", 213 | .@"utf-32" => .@"utf-32", 214 | }, 215 | .textDocumentSync = .{ 216 | .TextDocumentSyncOptions = .{ 217 | .openClose = true, 218 | .change = .Full, 219 | .save = .{ .bool = true }, 220 | }, 221 | }, 222 | .completionProvider = .{ 223 | .triggerCharacters = &[_][]const u8{ ".", ":", "@", "]", "/" }, 224 | }, 225 | .hoverProvider = .{ .bool = true }, 226 | .definitionProvider = .{ .bool = true }, 227 | .referencesProvider = .{ .bool = true }, 228 | .documentFormattingProvider = .{ .bool = true }, 229 | .semanticTokensProvider = .{ 230 | .SemanticTokensOptions = .{ 231 | .full = .{ .bool = true }, 232 | .legend = .{ 233 | .tokenTypes = std.meta.fieldNames(types.SemanticTokenTypes), 234 | .tokenModifiers = std.meta.fieldNames(types.SemanticTokenModifiers), 235 | }, 236 | }, 237 | }, 238 | .inlayHintProvider = .{ .bool = true }, 239 | }, 240 | }; 241 | } 242 | 243 | fn initializedHandler(server: *Server, _: std.mem.Allocator, notification: types.InitializedParams) Error!void { 244 | _ = notification; 245 | 246 | if (server.status != .initializing) { 247 | std.log.warn("received a initialized notification but the server has not send a initialize request!", .{}); 248 | } 249 | 250 | server.status = .initialized; 251 | } 252 | 253 | fn shutdownHandler(server: *Server, _: std.mem.Allocator, _: void) Error!?void { 254 | defer server.status = .shutdown; 255 | if (server.status != .initialized) return error.InvalidRequest; // received a shutdown request but the server is not initialized! 256 | } 257 | 258 | fn exitHandler(server: *Server, _: std.mem.Allocator, _: void) Error!void { 259 | server.status = switch (server.status) { 260 | .initialized => .exiting_failure, 261 | .shutdown => .exiting_success, 262 | else => unreachable, 263 | }; 264 | } 265 | 266 | fn openDocumentHandler(server: *Server, _: std.mem.Allocator, notification: types.DidOpenTextDocumentParams) Error!void { 267 | const new_text = try server.allocator.dupe(u8, notification.textDocument.text); // We informed the client that we only do full document syncs 268 | errdefer server.allocator.free(new_text); 269 | 270 | const gop = try server.documents.getOrPut(server.allocator, notification.textDocument.uri); 271 | if (gop.found_existing) { 272 | server.allocator.free(gop.value_ptr.*); // free old text even though this shoudnt be necessary 273 | } else { 274 | errdefer std.debug.assert(server.documents.remove(notification.textDocument.uri)); 275 | gop.key_ptr.* = try server.allocator.dupe(u8, notification.textDocument.uri); 276 | } 277 | gop.value_ptr.* = new_text; 278 | } 279 | 280 | fn changeDocumentHandler(server: *Server, _: std.mem.Allocator, notification: types.DidChangeTextDocumentParams) Error!void { 281 | if (notification.contentChanges.len == 0) return; 282 | const new_text = try server.allocator.dupe(u8, notification.contentChanges[notification.contentChanges.len - 1].literal_1.text); // We informed the client that we only do full document syncs 283 | errdefer server.allocator.free(new_text); 284 | 285 | const gop = try server.documents.getOrPut(server.allocator, notification.textDocument.uri); 286 | if (gop.found_existing) { 287 | server.allocator.free(gop.value_ptr.*); // free old text even though this shoudnt be necessary 288 | } else { 289 | errdefer std.debug.assert(server.documents.remove(notification.textDocument.uri)); 290 | gop.key_ptr.* = try server.allocator.dupe(u8, notification.textDocument.uri); 291 | } 292 | gop.value_ptr.* = new_text; 293 | } 294 | 295 | fn saveDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: types.DidSaveTextDocumentParams) Error!void { 296 | _ = server; 297 | _ = arena; 298 | _ = notification; 299 | } 300 | 301 | fn closeDocumentHandler(server: *Server, _: std.mem.Allocator, notification: types.DidCloseTextDocumentParams) error{}!void { 302 | const kv = server.documents.fetchRemove(notification.textDocument.uri) orelse return; 303 | server.allocator.free(kv.key); 304 | server.allocator.free(kv.value); 305 | } 306 | 307 | fn completionHandler(server: *Server, arena: std.mem.Allocator, request: types.CompletionParams) Error!ResultType("textDocument/completion") { 308 | _ = server; 309 | _ = request; 310 | var completions: std.ArrayListUnmanaged(types.CompletionItem) = .{}; 311 | 312 | try completions.append(arena, types.CompletionItem{ 313 | .label = "ziggy", 314 | .kind = .Text, 315 | .documentation = .{ .string = "Is a Zig-flavored data format" }, 316 | }); 317 | 318 | try completions.append(arena, types.CompletionItem{ 319 | .label = "zls", 320 | .kind = .Function, 321 | .documentation = .{ .string = "is a Zig LSP" }, 322 | }); 323 | 324 | return .{ 325 | .CompletionList = types.CompletionList{ 326 | .isIncomplete = false, 327 | .items = completions.items, 328 | }, 329 | }; 330 | } 331 | 332 | fn gotoDefinitionHandler(server: *Server, arena: std.mem.Allocator, request: types.DefinitionParams) Error!ResultType("textDocument/definition") { 333 | _ = server; 334 | _ = arena; 335 | _ = request; 336 | return null; 337 | } 338 | 339 | fn hoverHandler(server: *Server, arena: std.mem.Allocator, request: types.HoverParams) Error!?types.Hover { 340 | _ = arena; 341 | 342 | const text = server.documents.get(request.textDocument.uri) orelse return null; 343 | const line = offsets.lineSliceAtPosition(text, request.position, server.offset_encoding); 344 | 345 | return types.Hover{ 346 | .contents = .{ 347 | .MarkupContent = .{ 348 | .kind = .plaintext, 349 | .value = line, 350 | }, 351 | }, 352 | }; 353 | } 354 | 355 | fn referencesHandler(server: *Server, arena: std.mem.Allocator, request: types.ReferenceParams) Error!?[]types.Location { 356 | _ = server; 357 | _ = arena; 358 | _ = request; 359 | return null; 360 | } 361 | 362 | fn formattingHandler(server: *Server, arena: std.mem.Allocator, request: types.DocumentFormattingParams) Error!?[]types.TextEdit { 363 | _ = server; 364 | _ = arena; 365 | _ = request; 366 | return null; 367 | } 368 | 369 | fn semanticTokensFullHandler(server: *Server, arena: std.mem.Allocator, request: types.SemanticTokensParams) Error!?types.SemanticTokens { 370 | _ = server; 371 | _ = arena; 372 | _ = request; 373 | return null; 374 | } 375 | 376 | fn inlayHintHandler(server: *Server, arena: std.mem.Allocator, request: types.InlayHintParams) Error!?[]types.InlayHint { 377 | _ = server; 378 | _ = arena; 379 | _ = request; 380 | return null; 381 | } 382 | 383 | /// workaround for https://github.com/ziglang/zig/issues/16392 384 | /// ```zig 385 | /// union(enum) { 386 | /// request: Request, 387 | /// notification: Notification, 388 | /// response: Response, 389 | /// } 390 | /// ```zig 391 | pub const Message = struct { 392 | tag: enum(u32) { 393 | request, 394 | notification, 395 | response, 396 | }, 397 | request: ?Request = null, 398 | notification: ?Notification = null, 399 | response: ?Response = null, 400 | 401 | pub const Request = struct { 402 | id: types.RequestId, 403 | params: Params, 404 | 405 | pub const Params = union(enum) { 406 | initialize: types.InitializeParams, 407 | shutdown: void, 408 | @"textDocument/completion": types.CompletionParams, 409 | @"textDocument/hover": types.HoverParams, 410 | @"textDocument/definition": types.DefinitionParams, 411 | @"textDocument/references": types.ReferenceParams, 412 | @"textDocument/formatting": types.DocumentFormattingParams, 413 | @"textDocument/semanticTokens/full": types.SemanticTokensParams, 414 | @"textDocument/inlayHint": types.InlayHintParams, 415 | // Not every request is included here so that the we reduce the amount of parsing code we have generate 416 | unknown: []const u8, 417 | }; 418 | }; 419 | 420 | pub const Notification = union(enum) { 421 | initialized: types.InitializedParams, 422 | exit: void, 423 | @"textDocument/didOpen": types.DidOpenTextDocumentParams, 424 | @"textDocument/didChange": types.DidChangeTextDocumentParams, 425 | @"textDocument/didSave": types.DidSaveTextDocumentParams, 426 | @"textDocument/didClose": types.DidCloseTextDocumentParams, 427 | // Not every notification is included here so that the we reduce the amount of parsing code we have generate 428 | unknown: []const u8, 429 | }; 430 | 431 | pub const Response = struct { 432 | id: types.RequestId, 433 | data: Data, 434 | 435 | pub const Data = union(enum) { 436 | result: types.LSPAny, 437 | @"error": types.ResponseError, 438 | }; 439 | }; 440 | 441 | pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) std.json.ParseError(@TypeOf(source.*))!Message { 442 | const json_value = try std.json.parseFromTokenSourceLeaky(std.json.Value, allocator, source, options); 443 | return try jsonParseFromValue(allocator, json_value, options); 444 | } 445 | 446 | pub fn jsonParseFromValue( 447 | allocator: std.mem.Allocator, 448 | source: std.json.Value, 449 | options: std.json.ParseOptions, 450 | ) !Message { 451 | if (source != .object) return error.UnexpectedToken; 452 | const object = source.object; 453 | 454 | @setEvalBranchQuota(10_000); 455 | if (object.get("id")) |id_obj| { 456 | const msg_id = try std.json.parseFromValueLeaky(types.RequestId, allocator, id_obj, options); 457 | 458 | if (object.get("method")) |method_obj| { 459 | const msg_method = try std.json.parseFromValueLeaky([]const u8, allocator, method_obj, options); 460 | 461 | const msg_params = object.get("params") orelse .null; 462 | 463 | const fields = @typeInfo(Request.Params).Union.fields; 464 | 465 | inline for (fields) |field| { 466 | if (std.mem.eql(u8, msg_method, field.name)) { 467 | const params = if (field.type == void) 468 | void{} 469 | else 470 | try std.json.parseFromValueLeaky(field.type, allocator, msg_params, options); 471 | 472 | return .{ 473 | .tag = .request, 474 | .request = .{ 475 | .id = msg_id, 476 | .params = @unionInit(Request.Params, field.name, params), 477 | }, 478 | }; 479 | } 480 | } 481 | return .{ 482 | .tag = .request, 483 | .request = .{ 484 | .id = msg_id, 485 | .params = .{ .unknown = msg_method }, 486 | }, 487 | }; 488 | } else { 489 | const result = object.get("result") orelse .null; 490 | const error_obj = object.get("error") orelse .null; 491 | 492 | const err = try std.json.parseFromValueLeaky(?types.ResponseError, allocator, error_obj, options); 493 | 494 | if (result != .null and err != null) return error.UnexpectedToken; 495 | 496 | if (err) |e| { 497 | return .{ 498 | .tag = .response, 499 | .response = .{ 500 | .id = msg_id, 501 | .data = .{ .@"error" = e }, 502 | }, 503 | }; 504 | } else { 505 | return .{ 506 | .tag = .response, 507 | .response = .{ 508 | .id = msg_id, 509 | .data = .{ .result = result }, 510 | }, 511 | }; 512 | } 513 | } 514 | } else { 515 | const method_obj = object.get("method") orelse return error.UnexpectedToken; 516 | const msg_method = try std.json.parseFromValueLeaky([]const u8, allocator, method_obj, options); 517 | 518 | const msg_params = object.get("params") orelse .null; 519 | 520 | const fields = @typeInfo(Notification).Union.fields; 521 | 522 | inline for (fields) |field| { 523 | if (std.mem.eql(u8, msg_method, field.name)) { 524 | const params = if (field.type == void) 525 | void{} 526 | else 527 | try std.json.parseFromValueLeaky(field.type, allocator, msg_params, options); 528 | 529 | return .{ 530 | .tag = .notification, 531 | .notification = @unionInit(Notification, field.name, params), 532 | }; 533 | } 534 | } 535 | return .{ 536 | .tag = .notification, 537 | .notification = .{ .unknown = msg_method }, 538 | }; 539 | } 540 | } 541 | 542 | pub fn format(message: Message, comptime fmt_str: []const u8, options: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { 543 | _ = options; 544 | if (fmt_str.len != 0) std.fmt.invalidFmtError(fmt_str, message); 545 | switch (message.tag) { 546 | .request => try writer.print("request-{}-{s}", .{ message.request.?.id, switch (message.request.?.params) { 547 | .unknown => |method| method, 548 | else => @tagName(message.request.?.params), 549 | } }), 550 | .notification => try writer.print("notification-{s}", .{switch (message.notification.?) { 551 | .unknown => |method| method, 552 | else => @tagName(message.notification.?), 553 | }}), 554 | .response => try writer.print("response-{}", .{message.response.?.id}), 555 | } 556 | } 557 | }; 558 | 559 | pub fn create(allocator: std.mem.Allocator, transport: *Transport) !*Server { 560 | const server = try allocator.create(Server); 561 | errdefer server.destroy(); 562 | server.* = Server{ 563 | .allocator = allocator, 564 | .transport = transport, 565 | .client_capabilities = try std.json.parseFromSlice(types.ClientCapabilities, allocator, "{}", .{}), 566 | }; 567 | 568 | return server; 569 | } 570 | 571 | pub fn destroy(server: *Server) void { 572 | server.client_capabilities.deinit(); 573 | server.allocator.destroy(server); 574 | } 575 | 576 | pub fn keepRunning(server: Server) bool { 577 | switch (server.status) { 578 | .exiting_success, .exiting_failure => return false, 579 | else => return true, 580 | } 581 | } 582 | 583 | pub fn loop(server: *Server) !void { 584 | while (server.keepRunning()) { 585 | // `json_message` is the message that is send from the client to the server (request or notification or response) 586 | const json_message = try server.transport.readJsonMessage(server.allocator); 587 | defer server.allocator.free(json_message); 588 | 589 | // `send_message` is the message that is send from the server to the client (response) 590 | const send_message = try server.sendJsonMessageSync(json_message) orelse continue; // no response message on notifications 591 | server.allocator.free(send_message); 592 | } 593 | } 594 | 595 | pub fn sendJsonMessageSync(server: *Server, json_message: []const u8) Error!?[]u8 { 596 | const parsed_message = std.json.parseFromSlice( 597 | Message, 598 | server.allocator, 599 | json_message, 600 | .{ .ignore_unknown_fields = true, .max_value_len = null }, 601 | ) catch return error.ParseError; 602 | defer parsed_message.deinit(); 603 | return try server.processMessage(parsed_message.value); 604 | } 605 | 606 | pub fn sendRequestSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: ParamsType(method)) Error!ResultType(method) { 607 | comptime std.debug.assert(isRequestMethod(method)); 608 | const RequestMethods = std.meta.Tag(Message.Request.Params); 609 | 610 | return switch (comptime std.meta.stringToEnum(RequestMethods, method).?) { 611 | .initialize => try server.initializeHandler(arena, params), 612 | .shutdown => try server.shutdownHandler(arena, params), 613 | .@"textDocument/completion" => try server.completionHandler(arena, params), 614 | .@"textDocument/hover" => try server.hoverHandler(arena, params), 615 | .@"textDocument/definition" => try server.gotoDefinitionHandler(arena, params), 616 | .@"textDocument/references" => try server.referencesHandler(arena, params), 617 | .@"textDocument/formatting" => try server.formattingHandler(arena, params), 618 | .@"textDocument/semanticTokens/full" => try server.semanticTokensFullHandler(arena, params), 619 | .@"textDocument/inlayHint" => try server.inlayHintHandler(arena, params), 620 | .unknown => return null, 621 | }; 622 | } 623 | 624 | pub fn sendNotificationSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: ParamsType(method)) Error!void { 625 | comptime std.debug.assert(isNotificationMethod(method)); 626 | const NotificationMethods = std.meta.Tag(Message.Notification); 627 | 628 | return switch (comptime std.meta.stringToEnum(NotificationMethods, method).?) { 629 | .initialized => try server.initializedHandler(arena, params), 630 | .exit => try server.exitHandler(arena, params), 631 | .@"textDocument/didOpen" => try server.openDocumentHandler(arena, params), 632 | .@"textDocument/didChange" => try server.changeDocumentHandler(arena, params), 633 | .@"textDocument/didSave" => try server.saveDocumentHandler(arena, params), 634 | .@"textDocument/didClose" => try server.closeDocumentHandler(arena, params), 635 | .unknown => return, 636 | }; 637 | } 638 | 639 | pub fn sendMessageSync(server: *Server, arena: std.mem.Allocator, comptime method: []const u8, params: ParamsType(method)) Error!ResultType(method) { 640 | comptime std.debug.assert(isRequestMethod(method) or isNotificationMethod(method)); 641 | 642 | if (comptime isRequestMethod(method)) { 643 | return try server.sendRequestSync(arena, method, params); 644 | } else if (comptime isNotificationMethod(method)) { 645 | return try server.sendNotificationSync(arena, method, params); 646 | } else unreachable; 647 | } 648 | 649 | fn processMessage(server: *Server, message: Message) Error!?[]u8 { 650 | var timer = std.time.Timer.start() catch null; 651 | defer if (timer) |*t| { 652 | const total_time = @divFloor(t.read(), std.time.ns_per_ms); 653 | std.log.debug("Took {d}ms to process {}", .{ total_time, message }); 654 | }; 655 | 656 | try server.validateMessage(message); 657 | 658 | // Set up an ArenaAllocator that can be used any allocations that are only needed while handling a single request. 659 | var arena_allocator = std.heap.ArenaAllocator.init(server.allocator); 660 | defer arena_allocator.deinit(); 661 | 662 | @setEvalBranchQuota(5_000); 663 | switch (message.tag) { 664 | .request => switch (message.request.?.params) { 665 | inline else => |params, method| { 666 | const result = try server.sendRequestSync(arena_allocator.allocator(), @tagName(method), params); 667 | return try server.sendToClientResponse(message.request.?.id, result); 668 | }, 669 | .unknown => return try server.sendToClientResponse(message.request.?.id, null), 670 | }, 671 | .notification => switch (message.notification.?) { 672 | inline else => |params, method| { 673 | try server.sendNotificationSync(arena_allocator.allocator(), @tagName(method), params); 674 | }, 675 | .unknown => {}, 676 | }, 677 | .response => try server.handleResponse(message.response.?), 678 | } 679 | return null; 680 | } 681 | 682 | fn validateMessage(server: *const Server, message: Message) Error!void { 683 | const method = switch (message.tag) { 684 | .request => switch (message.request.?.params) { 685 | .unknown => |method| blk: { 686 | if (!isRequestMethod(method)) return error.MethodNotFound; 687 | break :blk method; 688 | }, 689 | else => @tagName(message.request.?.params), 690 | }, 691 | .notification => switch (message.notification.?) { 692 | .unknown => |method| blk: { 693 | if (!isNotificationMethod(method)) return error.MethodNotFound; 694 | break :blk method; 695 | }, 696 | else => @tagName(message.notification.?), 697 | }, 698 | .response => return, // validation happens in `handleResponse` 699 | }; 700 | 701 | switch (server.status) { 702 | .uninitialized => blk: { 703 | if (std.mem.eql(u8, method, "initialize")) break :blk; 704 | if (std.mem.eql(u8, method, "exit")) break :blk; 705 | 706 | return error.ServerNotInitialized; // server received a request before being initialized! 707 | }, 708 | .initializing => blk: { 709 | if (std.mem.eql(u8, method, "initialized")) break :blk; 710 | if (std.mem.eql(u8, method, "$/progress")) break :blk; 711 | 712 | return error.InvalidRequest; // server received a request during initialization! 713 | }, 714 | .initialized => {}, 715 | .shutdown => blk: { 716 | if (std.mem.eql(u8, method, "exit")) break :blk; 717 | 718 | return error.InvalidRequest; // server received a request after shutdown! 719 | }, 720 | .exiting_success, 721 | .exiting_failure, 722 | => unreachable, 723 | } 724 | } 725 | 726 | /// Handle a reponse that we have received from the client. 727 | /// Doesn't usually happen unless we explicitly send a request to the client. 728 | fn handleResponse(server: *Server, response: Message.Response) Error!void { 729 | _ = server; 730 | 731 | const id: []const u8 = switch (response.id) { 732 | .string => |id| id, 733 | .integer => |id| { 734 | std.log.warn("received response from client with id '{d}' that has no handler!", .{id}); 735 | return; 736 | }, 737 | }; 738 | 739 | if (response.data == .@"error") { 740 | const err = response.data.@"error"; 741 | std.log.err("Error response for '{s}': {}, {s}", .{ id, err.code, err.message }); 742 | return; 743 | } 744 | 745 | std.log.warn("received response from client with id '{s}' that has no handler!", .{id}); 746 | } 747 | 748 | // 749 | // LSP helper functions 750 | // 751 | 752 | pub fn ResultType(comptime method: []const u8) type { 753 | if (getRequestMetadata(method)) |meta| return meta.Result; 754 | if (isNotificationMethod(method)) return void; 755 | @compileError("unknown method '" ++ method ++ "'"); 756 | } 757 | 758 | pub fn ParamsType(comptime method: []const u8) type { 759 | if (getRequestMetadata(method)) |meta| return meta.Params orelse void; 760 | if (getNotificationMetadata(method)) |meta| return meta.Params orelse void; 761 | @compileError("unknown method '" ++ method ++ "'"); 762 | } 763 | 764 | fn getRequestMetadata(comptime method: []const u8) ?types.RequestMetadata { 765 | for (types.request_metadata) |meta| { 766 | if (std.mem.eql(u8, method, meta.method)) { 767 | return meta; 768 | } 769 | } 770 | return null; 771 | } 772 | 773 | fn getNotificationMetadata(comptime method: []const u8) ?types.NotificationMetadata { 774 | for (types.notification_metadata) |meta| { 775 | if (std.mem.eql(u8, method, meta.method)) { 776 | return meta; 777 | } 778 | } 779 | return null; 780 | } 781 | 782 | const RequestMethodSet = blk: { 783 | @setEvalBranchQuota(5000); 784 | var kvs_list: [types.request_metadata.len]struct { []const u8 } = undefined; 785 | for (types.request_metadata, &kvs_list) |meta, *kv| { 786 | kv.* = .{meta.method}; 787 | } 788 | break :blk std.ComptimeStringMap(void, &kvs_list); 789 | }; 790 | 791 | const NotificationMethodSet = blk: { 792 | @setEvalBranchQuota(5000); 793 | var kvs_list: [types.notification_metadata.len]struct { []const u8 } = undefined; 794 | for (types.notification_metadata, &kvs_list) |meta, *kv| { 795 | kv.* = .{meta.method}; 796 | } 797 | break :blk std.ComptimeStringMap(void, &kvs_list); 798 | }; 799 | 800 | /// return true if there is a request with the given method name 801 | pub fn isRequestMethod(method: []const u8) bool { 802 | return RequestMethodSet.has(method); 803 | } 804 | 805 | /// return true if there is a notification with the given method name 806 | pub fn isNotificationMethod(method: []const u8) bool { 807 | return NotificationMethodSet.has(method); 808 | } 809 | -------------------------------------------------------------------------------- /src/Transport.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Header = @import("Header.zig"); 3 | 4 | in: std.io.BufferedReader(4096, std.fs.File.Reader), 5 | out: std.fs.File.Writer, 6 | in_lock: std.Thread.Mutex = .{}, 7 | out_lock: std.Thread.Mutex = .{}, 8 | message_tracing: bool = false, 9 | 10 | const message_logger = std.log.scoped(.message); 11 | 12 | const Transport = @This(); 13 | 14 | pub fn init(in: std.fs.File.Reader, out: std.fs.File.Writer) Transport { 15 | return .{ 16 | .in = std.io.bufferedReader(in), 17 | .out = out, 18 | }; 19 | } 20 | 21 | pub fn readJsonMessage(self: *Transport, allocator: std.mem.Allocator) ![]u8 { 22 | const json_message = blk: { 23 | self.in_lock.lock(); 24 | defer self.in_lock.unlock(); 25 | 26 | const reader = self.in.reader(); 27 | const header = try Header.parse(allocator, reader); 28 | defer header.deinit(allocator); 29 | 30 | const json_message = try allocator.alloc(u8, header.content_length); 31 | errdefer allocator.free(json_message); 32 | try reader.readNoEof(json_message); 33 | 34 | break :blk json_message; 35 | }; 36 | 37 | if (self.message_tracing) message_logger.debug("received: {s}", .{json_message}); 38 | return json_message; 39 | } 40 | 41 | pub fn writeJsonMessage(self: *Transport, json_message: []const u8) !void { 42 | var buffer: [64]u8 = undefined; 43 | const prefix = std.fmt.bufPrint(&buffer, "Content-Length: {d}\r\n\r\n", .{json_message.len}) catch unreachable; 44 | 45 | { 46 | self.out_lock.lock(); 47 | defer self.out_lock.unlock(); 48 | 49 | try self.out.writeAll(prefix); 50 | try self.out.writeAll(json_message); 51 | } 52 | if (self.message_tracing) message_logger.debug("sent: {s}", .{json_message}); 53 | } 54 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Server = @import("Server.zig"); 5 | const Transport = @import("Transport.zig"); 6 | 7 | pub fn main() !void { 8 | var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; 9 | defer _ = general_purpose_allocator.deinit(); 10 | 11 | const allocator: std.mem.Allocator = general_purpose_allocator.allocator(); 12 | 13 | var transport = Transport.init( 14 | std.io.getStdIn().reader(), 15 | std.io.getStdOut().writer(), 16 | ); 17 | transport.message_tracing = false; 18 | 19 | const server = try Server.create(allocator, &transport); 20 | defer server.destroy(); 21 | 22 | try server.loop(); 23 | 24 | if (server.status == .exiting_failure) { 25 | if (builtin.mode == .Debug) { 26 | // make sure that GeneralPurposeAllocator.deinit gets run to detect leaks 27 | return; 28 | } else { 29 | std.process.exit(1); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/offsets.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("lsp.zig"); 3 | 4 | pub const Encoding = enum { 5 | /// Character offsets count UTF-8 code units (e.g. bytes). 6 | @"utf-8", 7 | /// Character offsets count UTF-16 code units. 8 | /// 9 | /// This is the default and must always be supported 10 | /// by servers 11 | @"utf-16", 12 | /// Character offsets count UTF-32 code units. 13 | /// 14 | /// Implementation note: these are the same as Unicode codepoints, 15 | /// so this `PositionEncodingKind` may also be used for an 16 | /// encoding-agnostic representation of character offsets. 17 | @"utf-32", 18 | }; 19 | 20 | pub const Loc = std.zig.Token.Loc; 21 | 22 | pub fn indexToPosition(text: []const u8, index: usize, encoding: Encoding) types.Position { 23 | const last_line_start = if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |line| line + 1 else 0; 24 | const line_count = std.mem.count(u8, text[0..last_line_start], "\n"); 25 | 26 | return .{ 27 | .line = @intCast(line_count), 28 | .character = @intCast(countCodeUnits(text[last_line_start..index], encoding)), 29 | }; 30 | } 31 | 32 | pub fn maybePositionToIndex(text: []const u8, position: types.Position, encoding: Encoding) ?usize { 33 | var line: u32 = 0; 34 | var line_start_index: usize = 0; 35 | for (text, 0..) |c, i| { 36 | if (line == position.line) break; 37 | if (c == '\n') { 38 | line += 1; 39 | line_start_index = i + 1; 40 | } 41 | } 42 | 43 | if (line != position.line) return null; 44 | 45 | const line_text = std.mem.sliceTo(text[line_start_index..], '\n'); 46 | const line_byte_length = getNCodeUnitByteCount(line_text, position.character, encoding); 47 | 48 | return line_start_index + line_byte_length; 49 | } 50 | 51 | pub fn positionToIndex(text: []const u8, position: types.Position, encoding: Encoding) usize { 52 | var line: u32 = 0; 53 | var line_start_index: usize = 0; 54 | for (text, 0..) |c, i| { 55 | if (line == position.line) break; 56 | if (c == '\n') { 57 | line += 1; 58 | line_start_index = i + 1; 59 | } 60 | } 61 | std.debug.assert(line == position.line); 62 | 63 | const line_text = std.mem.sliceTo(text[line_start_index..], '\n'); 64 | const line_byte_length = getNCodeUnitByteCount(line_text, position.character, encoding); 65 | 66 | return line_start_index + line_byte_length; 67 | } 68 | 69 | pub fn locLength(text: []const u8, loc: Loc, encoding: Encoding) usize { 70 | return countCodeUnits(text[loc.start..loc.end], encoding); 71 | } 72 | 73 | pub fn rangeLength(text: []const u8, range: types.Range, encoding: Encoding) usize { 74 | const loc = rangeToLoc(text, range, encoding); 75 | return locLength(text, loc, encoding); 76 | } 77 | 78 | pub fn locToSlice(text: []const u8, loc: Loc) []const u8 { 79 | return text[loc.start..loc.end]; 80 | } 81 | 82 | pub fn locToRange(text: []const u8, loc: Loc, encoding: Encoding) types.Range { 83 | std.debug.assert(loc.start <= loc.end and loc.end <= text.len); 84 | const start = indexToPosition(text, loc.start, encoding); 85 | return .{ 86 | .start = start, 87 | .end = advancePosition(text, start, loc.start, loc.end, encoding), 88 | }; 89 | } 90 | 91 | pub fn rangeToSlice(text: []const u8, range: types.Range, encoding: Encoding) []const u8 { 92 | return locToSlice(text, rangeToLoc(text, range, encoding)); 93 | } 94 | 95 | pub fn rangeToLoc(text: []const u8, range: types.Range, encoding: Encoding) Loc { 96 | return .{ 97 | .start = positionToIndex(text, range.start, encoding), 98 | .end = positionToIndex(text, range.end, encoding), 99 | }; 100 | } 101 | 102 | pub fn lineLocAtIndex(text: []const u8, index: usize) Loc { 103 | return .{ 104 | .start = if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |idx| idx + 1 else 0, 105 | .end = std.mem.indexOfScalarPos(u8, text, index, '\n') orelse text.len, 106 | }; 107 | } 108 | 109 | pub fn lineSliceAtIndex(text: []const u8, index: usize) []const u8 { 110 | return locToSlice(text, lineLocAtIndex(text, index)); 111 | } 112 | 113 | pub fn lineLocAtPosition(text: []const u8, position: types.Position, encoding: Encoding) Loc { 114 | return lineLocAtIndex(text, positionToIndex(text, position, encoding)); 115 | } 116 | 117 | pub fn lineSliceAtPosition(text: []const u8, position: types.Position, encoding: Encoding) []const u8 { 118 | return locToSlice(text, lineLocAtPosition(text, position, encoding)); 119 | } 120 | 121 | pub fn lineLocUntilIndex(text: []const u8, index: usize) Loc { 122 | return .{ 123 | .start = if (std.mem.lastIndexOfScalar(u8, text[0..index], '\n')) |idx| idx + 1 else 0, 124 | .end = index, 125 | }; 126 | } 127 | 128 | pub fn lineSliceUntilIndex(text: []const u8, index: usize) []const u8 { 129 | return locToSlice(text, lineLocUntilIndex(text, index)); 130 | } 131 | 132 | pub fn lineLocUntilPosition(text: []const u8, position: types.Position, encoding: Encoding) Loc { 133 | return lineLocUntilIndex(text, positionToIndex(text, position, encoding)); 134 | } 135 | 136 | pub fn lineSliceUntilPosition(text: []const u8, position: types.Position, encoding: Encoding) []const u8 { 137 | return locToSlice(text, lineLocUntilPosition(text, position, encoding)); 138 | } 139 | 140 | pub fn convertPositionEncoding(text: []const u8, position: types.Position, from_encoding: Encoding, to_encoding: Encoding) types.Position { 141 | if (from_encoding == to_encoding) return position; 142 | 143 | const line_loc = lineLocUntilPosition(text, position, from_encoding); 144 | 145 | return .{ 146 | .line = position.line, 147 | .character = @intCast(locLength(text, line_loc, to_encoding)), 148 | }; 149 | } 150 | 151 | pub fn convertRangeEncoding(text: []const u8, range: types.Range, from_encoding: Encoding, to_encoding: Encoding) types.Range { 152 | if (from_encoding == to_encoding) return range; 153 | return .{ 154 | .start = convertPositionEncoding(text, range.start, from_encoding, to_encoding), 155 | .end = convertPositionEncoding(text, range.end, from_encoding, to_encoding), 156 | }; 157 | } 158 | 159 | /// returns true if a and b intersect 160 | pub fn locIntersect(a: Loc, b: Loc) bool { 161 | std.debug.assert(a.start <= a.end and b.start <= b.end); 162 | return a.start < b.end and a.end > b.start; 163 | } 164 | 165 | /// returns true if a is inside b 166 | pub fn locInside(inner: Loc, outer: Loc) bool { 167 | std.debug.assert(inner.start <= inner.end and outer.start <= outer.end); 168 | return outer.start <= inner.start and inner.end <= outer.end; 169 | } 170 | 171 | /// returns the union of a and b 172 | pub fn locMerge(a: Loc, b: Loc) Loc { 173 | std.debug.assert(a.start <= a.end and b.start <= b.end); 174 | return .{ 175 | .start = @min(a.start, b.start), 176 | .end = @max(a.end, b.end), 177 | }; 178 | } 179 | 180 | // Helper functions 181 | 182 | /// advance `position` which starts at `from_index` to `to_index` accounting for line breaks 183 | pub fn advancePosition(text: []const u8, position: types.Position, from_index: usize, to_index: usize, encoding: Encoding) types.Position { 184 | var line = position.line; 185 | 186 | for (text[from_index..to_index]) |c| { 187 | if (c == '\n') { 188 | line += 1; 189 | } 190 | } 191 | 192 | const line_loc = lineLocUntilIndex(text, to_index); 193 | 194 | return .{ 195 | .line = line, 196 | .character = @intCast(locLength(text, line_loc, encoding)), 197 | }; 198 | } 199 | 200 | /// returns the number of code units in `text` 201 | pub fn countCodeUnits(text: []const u8, encoding: Encoding) usize { 202 | switch (encoding) { 203 | .@"utf-8" => return text.len, 204 | .@"utf-16" => { 205 | var iter: std.unicode.Utf8Iterator = .{ .bytes = text, .i = 0 }; 206 | 207 | var utf16_len: usize = 0; 208 | while (iter.nextCodepoint()) |codepoint| { 209 | if (codepoint < 0x10000) { 210 | utf16_len += 1; 211 | } else { 212 | utf16_len += 2; 213 | } 214 | } 215 | return utf16_len; 216 | }, 217 | .@"utf-32" => return std.unicode.utf8CountCodepoints(text) catch unreachable, 218 | } 219 | } 220 | 221 | /// returns the number of (utf-8 code units / bytes) that represent `n` code units in `text` 222 | /// if `text` has less than `n` code units then the number of code units in 223 | /// `text` are returned, i.e. the result is being clamped. 224 | pub fn getNCodeUnitByteCount(text: []const u8, n: usize, encoding: Encoding) usize { 225 | switch (encoding) { 226 | .@"utf-8" => return @min(text.len, n), 227 | .@"utf-16" => { 228 | if (n == 0) return 0; 229 | var iter: std.unicode.Utf8Iterator = .{ .bytes = text, .i = 0 }; 230 | 231 | var utf16_len: usize = 0; 232 | while (iter.nextCodepoint()) |codepoint| { 233 | if (codepoint < 0x10000) { 234 | utf16_len += 1; 235 | } else { 236 | utf16_len += 2; 237 | } 238 | if (utf16_len >= n) break; 239 | } 240 | return iter.i; 241 | }, 242 | .@"utf-32" => { 243 | var i: usize = 0; 244 | var count: usize = 0; 245 | while (count != n) : (count += 1) { 246 | if (i >= text.len) break; 247 | i += std.unicode.utf8ByteSequenceLength(text[i]) catch unreachable; 248 | } 249 | return i; 250 | }, 251 | } 252 | } 253 | 254 | pub fn rangeLessThan(a: types.Range, b: types.Range) bool { 255 | return positionLessThan(a.start, b.start) or positionLessThan(a.end, b.end); 256 | } 257 | 258 | pub fn positionLessThan(a: types.Position, b: types.Position) bool { 259 | if (a.line < b.line) { 260 | return true; 261 | } 262 | if (a.line > b.line) { 263 | return false; 264 | } 265 | 266 | if (a.character < b.character) { 267 | return true; 268 | } 269 | 270 | return false; 271 | } 272 | --------------------------------------------------------------------------------