From 8731a37d1f4683c795ddbb5c16e69e4c1bba33d0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 26 Nov 2022 01:14:33 +0000 Subject: [PATCH] textDocument/selectionRange closes #777 --- src/Server.zig | 75 ++++++++++++++++++++++--- src/requests.zig | 7 +++ src/types.zig | 8 ++- tests/lsp_features/selection_range.zig | 76 ++++++++++++++++++++++++++ tests/tests.zig | 1 + 5 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 tests/lsp_features/selection_range.zig diff --git a/src/Server.zig b/src/Server.zig index 9d895a0..d39e86b 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -451,7 +451,7 @@ fn getAstCheckDiagnostics( fn autofix(server: *Server, allocator: std.mem.Allocator, handle: *const DocumentStore.Handle) !std.ArrayListUnmanaged(types.TextEdit) { var diagnostics = std.ArrayListUnmanaged(types.Diagnostic){}; try getAstCheckDiagnostics(server, handle.*, &diagnostics); - + var builder = code_actions.Builder{ .arena = &server.arena, .document_store = &server.document_store, @@ -1612,7 +1612,7 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: } } } - if(textDocument.synchronization) |synchronization| { + if (textDocument.synchronization) |synchronization| { server.client_capabilities.supports_will_save = synchronization.willSave.value; server.client_capabilities.supports_will_save_wait_until = synchronization.willSaveWaitUntil.value; } @@ -1662,7 +1662,7 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: .documentFormattingProvider = true, .documentRangeFormattingProvider = false, .foldingRangeProvider = true, - .selectionRangeProvider = false, + .selectionRangeProvider = true, .workspaceSymbolProvider = false, .rangeProvider = false, .documentProvider = true, @@ -1906,7 +1906,7 @@ fn willSaveHandler(server: *Server, writer: anytype, id: types.RequestId, req: r const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); - if(server.client_capabilities.supports_will_save_wait_until) return; + if (server.client_capabilities.supports_will_save_wait_until) return; try willSaveWaitUntilHandler(server, writer, id, req); } @@ -1919,7 +1919,7 @@ fn willSaveWaitUntilHandler(server: *Server, writer: anytype, id: types.RequestI const allocator = server.arena.allocator(); const uri = req.params.textDocument.uri; - + const handle = server.document_store.getHandle(uri) orelse return; if (handle.tree.errors.len != 0) return; @@ -1927,7 +1927,7 @@ fn willSaveWaitUntilHandler(server: *Server, writer: anytype, id: types.RequestI return try send(writer, allocator, types.Response{ .id = id, - .result = .{.TextEdits = text_edits.toOwnedSlice(allocator)}, + .result = .{ .TextEdits = text_edits.toOwnedSlice(allocator) }, }); } @@ -2690,6 +2690,64 @@ fn foldingRangeHandler(server: *Server, writer: anytype, id: types.RequestId, re }); } +fn selectionRangeHandler(server: *Server, writer: anytype, id: types.RequestId, req: requests.SelectionRange) !void { + const allocator = server.arena.allocator(); + const handle = server.document_store.getHandle(req.params.textDocument.uri) orelse { + log.warn("Trying to get selection range of non existent document {s}", .{req.params.textDocument.uri}); + return try respondGeneric(writer, id, null_result_response); + }; + + // For each of the input positons, we need to compute the stack of AST + // nodes/ranges which contain the position. At the moment, we do this in a + // super inefficient way, by iterationg _all_ nodes, selecting the ones that + // contain position, and then sorting. + // + // A faster algorithm would be to walk the tree starting from the root, + // descending into the child containing the position at every step. + var result = try allocator.alloc(*types.SelectionRange, req.params.positions.len); + var locs = try std.ArrayListUnmanaged(offsets.Loc).initCapacity(allocator, 32); + for (req.params.positions) |position, position_index| { + const index = offsets.positionToIndex(handle.text, position, server.offset_encoding); + + locs.clearRetainingCapacity(); + for (handle.tree.nodes.items(.data)) |_, i| { + const node = @intCast(u32, i); + const loc = offsets.nodeToLoc(handle.tree, node); + if (loc.start <= index and index <= loc.end) { + (try locs.addOne(allocator)).* = loc; + } + } + + std.sort.sort(offsets.Loc, locs.items, {}, shorterLocsFirst); + { + var i: usize = 0; + while (i + 1 < locs.items.len) { + if (std.meta.eql(locs.items[i], locs.items[i + 1])) { + _ = locs.orderedRemove(i); + } else { + i += 1; + } + } + } + + var selection_ranges = try allocator.alloc(types.SelectionRange, locs.items.len); + for (selection_ranges) |*range, i| { + range.range = offsets.locToRange(handle.text, locs.items[i], server.offset_encoding); + range.parent = if (i + 1 < selection_ranges.len) &selection_ranges[i + 1] else null; + } + result[position_index] = &selection_ranges[0]; + } + + try send(writer, allocator, types.Response{ + .id = id, + .result = .{ .SelectionRange = result }, + }); +} + +fn shorterLocsFirst(_: void, lhs: offsets.Loc, rhs: offsets.Loc) bool { + return (lhs.end - lhs.start) < (rhs.end - rhs.start); +} + // Needed for the hack seen below. fn extractErr(val: anytype) anyerror { val catch |e| return e; @@ -2817,8 +2875,8 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void .{ "textDocument/didChange", requests.ChangeDocument, changeDocumentHandler }, .{ "textDocument/didSave", requests.SaveDocument, saveDocumentHandler }, .{ "textDocument/didClose", requests.CloseDocument, closeDocumentHandler }, - .{"textDocument/willSave", requests.WillSave, willSaveHandler}, - .{"textDocument/willSaveWaitUntil", requests.WillSave, willSaveWaitUntilHandler}, + .{ "textDocument/willSave", requests.WillSave, willSaveHandler }, + .{ "textDocument/willSaveWaitUntil", requests.WillSave, willSaveWaitUntilHandler }, .{ "textDocument/semanticTokens/full", requests.SemanticTokensFull, semanticTokensFullHandler }, .{ "textDocument/inlayHint", requests.InlayHint, inlayHintHandler }, .{ "textDocument/completion", requests.Completion, completionHandler }, @@ -2836,6 +2894,7 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void .{ "textDocument/codeAction", requests.CodeAction, codeActionHandler }, .{ "workspace/didChangeConfiguration", Config.DidChangeConfigurationParams, didChangeConfigurationHandler }, .{ "textDocument/foldingRange", requests.FoldingRange, foldingRangeHandler }, + .{ "textDocument/selectionRange", requests.SelectionRange, selectionRangeHandler }, }; if (zig_builtin.zig_backend == .stage1) { diff --git a/src/requests.zig b/src/requests.zig index 54ca772..466a7bd 100644 --- a/src/requests.zig +++ b/src/requests.zig @@ -312,3 +312,10 @@ pub const FoldingRange = struct { textDocument: TextDocumentIdentifier, }, }; + +pub const SelectionRange = struct { + params: struct { + textDocument: TextDocumentIdentifier, + positions: []types.Position, + }, +}; diff --git a/src/types.zig b/src/types.zig index 67100e6..5ad7e62 100644 --- a/src/types.zig +++ b/src/types.zig @@ -44,6 +44,7 @@ pub const ResponseParams = union(enum) { CodeAction: []CodeAction, ApplyEdit: ApplyWorkspaceEditParams, FoldingRange: []FoldingRange, + SelectionRange: []*SelectionRange, }; pub const Response = struct { @@ -525,6 +526,11 @@ pub const DocumentHighlight = struct { }; pub const FoldingRange = struct { - startLine: usize, + startLine: usize, endLine: usize, }; + +pub const SelectionRange = struct { + range: Range, + parent: ?*SelectionRange, +}; diff --git a/tests/lsp_features/selection_range.zig b/tests/lsp_features/selection_range.zig new file mode 100644 index 0000000..9eb7c5c --- /dev/null +++ b/tests/lsp_features/selection_range.zig @@ -0,0 +1,76 @@ +const std = @import("std"); +const zls = @import("zls"); +const builtin = @import("builtin"); + +const helper = @import("../helper.zig"); +const Context = @import("../context.zig").Context; +const ErrorBuilder = @import("../ErrorBuilder.zig"); + +const types = zls.types; +const offsets = zls.offsets; +const requests = zls.requests; + +const allocator: std.mem.Allocator = std.testing.allocator; + +test "selectionRange - empty" { + try testSelectionRange("<>", &.{}); +} + +test "seletionRange - smoke" { + try testSelectionRange( + \\fn main() void { + \\ const x = 1 <>+ 1; + \\} + , &.{ "1 + 1", "const x = 1 + 1", "{\n const x = 1 + 1;\n}" }); +} + +fn testSelectionRange(source: []const u8, want: []const []const u8) !void { + var phr = try helper.collectClearPlaceholders(allocator, source); + defer phr.deinit(allocator); + + var ctx = try Context.init(); + defer ctx.deinit(); + + const test_uri: []const u8 = switch (builtin.os.tag) { + .windows => "file:///C:\\test.zig", + else => "file:///test.zig", + }; + + try ctx.requestDidOpen(test_uri, phr.new_source); + + const position = offsets.locToRange(phr.new_source, phr.locations.items(.new)[0], .utf16).start; + + const SelectionRange = struct { + range: types.Range, + parent: ?*@This(), + }; + + const request = requests.SelectionRange{ .params = .{ + .textDocument = .{ .uri = test_uri }, + .positions = &.{position}, + } }; + + const response = try ctx.requestGetResponse(?[]SelectionRange, "textDocument/selectionRange", request); + defer response.deinit(); + + const selectionRanges: []SelectionRange = response.result orelse { + std.debug.print("Server returned `null` as the result\n", .{}); + return error.InvalidResponse; + }; + + var got = std.ArrayList([]const u8).init(allocator); + defer got.deinit(); + + var it: ?*SelectionRange = &selectionRanges[0]; + while (it) |r| { + const slice = offsets.rangeToSlice(phr.new_source, r.range, .utf16); + (try got.addOne()).* = slice; + it = r.parent; + } + const last = got.pop(); + try std.testing.expectEqualStrings(phr.new_source, last); + try std.testing.expectEqual(want.len, got.items.len); + for (want) |w, i| { + try std.testing.expectEqualStrings(w, got.items[i]); + } +} diff --git a/tests/tests.zig b/tests/tests.zig index f6a97a9..1951513 100644 --- a/tests/tests.zig +++ b/tests/tests.zig @@ -14,6 +14,7 @@ comptime { _ = @import("lsp_features/inlay_hints.zig"); _ = @import("lsp_features/references.zig"); _ = @import("lsp_features/completion.zig"); + _ = @import("lsp_features/selection_range.zig"); // Language features _ = @import("language_features/cimport.zig");