Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

textDocument/selectionRange #784

Merged
merged 1 commit into from
Nov 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 67 additions & 8 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand All @@ -1919,15 +1919,15 @@ 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;

var text_edits = try server.autofix(allocator, handle);

return try send(writer, allocator, types.Response{
.id = id,
.result = .{.TextEdits = text_edits.toOwnedSlice(allocator)},
.result = .{ .TextEdits = text_edits.toOwnedSlice(allocator) },
});
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 },
Expand All @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions src/requests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,10 @@ pub const FoldingRange = struct {
textDocument: TextDocumentIdentifier,
},
};

pub const SelectionRange = struct {
params: struct {
textDocument: TextDocumentIdentifier,
positions: []types.Position,
},
};
8 changes: 7 additions & 1 deletion src/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub const ResponseParams = union(enum) {
CodeAction: []CodeAction,
ApplyEdit: ApplyWorkspaceEditParams,
FoldingRange: []FoldingRange,
SelectionRange: []*SelectionRange,
};

pub const Response = struct {
Expand Down Expand Up @@ -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,
};
76 changes: 76 additions & 0 deletions tests/lsp_features/selection_range.zig
Original file line number Diff line number Diff line change
@@ -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]);
}
}
1 change: 1 addition & 0 deletions tests/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down