Skip to content

Commit 7954708

Browse files
committed
implement server side code action kind filtering
Zed's `code_actions_on_format` setting relies on the server to filter code actions with `CodeActionContext.only` even though the filtering should be performed by the client.
1 parent d120457 commit 7954708

File tree

3 files changed

+106
-53
lines changed

3 files changed

+106
-53
lines changed

src/Server.zig

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ fn autofix(server: *Server, arena: std.mem.Allocator, handle: *DocumentStore.Han
369369
.analyser = &analyser,
370370
.handle = handle,
371371
.offset_encoding = server.offset_encoding,
372+
.only_kinds = std.EnumSet(std.meta.Tag(types.CodeActionKind)).init(.{
373+
.@"source.fixAll" = true,
374+
}),
372375
};
373376

374377
var actions: std.ArrayListUnmanaged(types.CodeAction) = .{};
@@ -377,11 +380,10 @@ fn autofix(server: *Server, arena: std.mem.Allocator, handle: *DocumentStore.Han
377380
var text_edits: std.ArrayListUnmanaged(types.TextEdit) = .{};
378381
for (actions.items) |action| {
379382
std.debug.assert(action.kind != null);
383+
std.debug.assert(action.kind.? == .@"source.fixAll");
380384
std.debug.assert(action.edit != null);
381385
std.debug.assert(action.edit.?.changes != null);
382386

383-
if (action.kind.? != .@"source.fixAll") continue;
384-
385387
const changes = action.edit.?.changes.?.map;
386388
if (changes.count() != 1) continue;
387389

@@ -1632,19 +1634,25 @@ fn codeActionHandler(server: *Server, arena: std.mem.Allocator, request: types.C
16321634
var analyser = server.initAnalyser(handle);
16331635
defer analyser.deinit();
16341636

1637+
const only_kinds = if (request.context.only) |kinds| blk: {
1638+
var set = std.EnumSet(std.meta.Tag(types.CodeActionKind)).initEmpty();
1639+
for (kinds) |kind| {
1640+
set.setPresent(kind, true);
1641+
}
1642+
break :blk set;
1643+
} else null;
1644+
16351645
var builder: code_actions.Builder = .{
16361646
.arena = arena,
16371647
.analyser = &analyser,
16381648
.handle = handle,
16391649
.offset_encoding = server.offset_encoding,
1650+
.only_kinds = only_kinds,
16401651
};
16411652

16421653
var actions: std.ArrayListUnmanaged(types.CodeAction) = .{};
16431654
try builder.generateCodeAction(error_bundle, &actions);
16441655

1645-
// Always generate code action organizeImports
1646-
try builder.generateOrganizeImportsAction(&actions);
1647-
16481656
const Result = lsp.types.getRequestMetadata("textDocument/codeAction").?.Result;
16491657
const result = try arena.alloc(std.meta.Child(std.meta.Child(Result)), actions.items.len);
16501658
for (actions.items, result) |action, *out| {

src/features/code_actions.zig

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub const Builder = struct {
1616
analyser: *Analyser,
1717
handle: *DocumentStore.Handle,
1818
offset_encoding: offsets.Encoding,
19+
only_kinds: ?std.EnumSet(std.meta.Tag(types.CodeActionKind)),
1920

2021
pub fn generateCodeAction(
2122
builder: *Builder,
@@ -27,6 +28,8 @@ pub const Builder = struct {
2728

2829
var remove_capture_actions: std.AutoHashMapUnmanaged(types.Range, void) = .{};
2930

31+
try handleUnorganizedImport(builder, actions);
32+
3033
if (error_bundle.errorMessageCount() == 0) return; // `getMessages` can't be called on an empty ErrorBundle
3134
for (error_bundle.getMessages()) |msg_index| {
3235
const err = error_bundle.getErrorMessage(msg_index);
@@ -66,11 +69,10 @@ pub const Builder = struct {
6669
}
6770
}
6871

69-
pub fn generateOrganizeImportsAction(
70-
builder: *Builder,
71-
actions: *std.ArrayListUnmanaged(types.CodeAction),
72-
) error{OutOfMemory}!void {
73-
try handleUnorganizedImport(builder, actions);
72+
/// Returns `false` if the client explicitly specified that they are not interested in this code action kind.
73+
fn wantKind(builder: *Builder, kind: std.meta.Tag(types.CodeActionKind)) bool {
74+
const only_kinds = builder.only_kinds orelse return true;
75+
return only_kinds.contains(kind);
7476
}
7577

7678
pub fn createTextEditLoc(self: *Builder, loc: offsets.Loc, new_text: []const u8) types.TextEdit {
@@ -106,6 +108,9 @@ pub fn collectAutoDiscardDiagnostics(
106108
diagnostics: *std.ArrayListUnmanaged(types.Diagnostic),
107109
offset_encoding: offsets.Encoding,
108110
) error{OutOfMemory}!void {
111+
const tracy_zone = tracy.trace(@src());
112+
defer tracy_zone.end();
113+
109114
const token_tags = tree.tokens.items(.tag);
110115
const token_starts = tree.tokens.items(.start);
111116

@@ -145,6 +150,11 @@ pub fn collectAutoDiscardDiagnostics(
145150
}
146151

147152
fn handleNonCamelcaseFunction(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
153+
const tracy_zone = tracy.trace(@src());
154+
defer tracy_zone.end();
155+
156+
if (!builder.wantKind(.quickfix)) return;
157+
148158
const identifier_name = offsets.locToSlice(builder.handle.tree.source, loc);
149159

150160
if (std.mem.allEqual(u8, identifier_name, '_')) return;
@@ -165,6 +175,8 @@ fn handleUnusedFunctionParameter(builder: *Builder, actions: *std.ArrayListUnman
165175
const tracy_zone = tracy.trace(@src());
166176
defer tracy_zone.end();
167177

178+
if (!builder.wantKind(.@"source.fixAll") and !builder.wantKind(.quickfix)) return;
179+
168180
const identifier_name = offsets.locToSlice(builder.handle.tree.source, loc);
169181

170182
const tree = builder.handle.tree;
@@ -213,28 +225,34 @@ fn handleUnusedFunctionParameter(builder: *Builder, actions: *std.ArrayListUnman
213225
const add_suffix_newline = is_last_param and token_tags[insert_token + 1] == .r_brace and tree.tokensOnSameLine(insert_token, insert_token + 1);
214226
const insert_index, const new_text = try createDiscardText(builder, identifier_name, insert_token, true, add_suffix_newline);
215227

216-
const action1 = types.CodeAction{
217-
.title = "discard function parameter",
218-
.kind = .@"source.fixAll",
219-
.isPreferred = true,
220-
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(insert_index, new_text)}),
221-
};
228+
try actions.ensureUnusedCapacity(builder.arena, 2);
222229

223-
// TODO fix formatting
224-
const action2 = types.CodeAction{
225-
.title = "remove function parameter",
226-
.kind = .quickfix,
227-
.isPreferred = false,
228-
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(getParamRemovalRange(tree, fn_proto_param), "")}),
229-
};
230+
if (builder.wantKind(.@"source.fixAll")) {
231+
actions.insertAssumeCapacity(0, .{
232+
.title = "discard function parameter",
233+
.kind = .@"source.fixAll",
234+
.isPreferred = true,
235+
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(insert_index, new_text)}),
236+
});
237+
}
230238

231-
try actions.insertSlice(builder.arena, 0, &.{ action1, action2 });
239+
if (builder.wantKind(.quickfix)) {
240+
// TODO fix formatting
241+
actions.appendAssumeCapacity(.{
242+
.title = "remove function parameter",
243+
.kind = .quickfix,
244+
.isPreferred = false,
245+
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(getParamRemovalRange(tree, fn_proto_param), "")}),
246+
});
247+
}
232248
}
233249

234250
fn handleUnusedVariableOrConstant(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
235251
const tracy_zone = tracy.trace(@src());
236252
defer tracy_zone.end();
237253

254+
if (!builder.wantKind(.@"source.fixAll")) return;
255+
238256
const identifier_name = offsets.locToSlice(builder.handle.tree.source, loc);
239257

240258
const tree = builder.handle.tree;
@@ -276,11 +294,40 @@ fn handleUnusedCapture(
276294
const tracy_zone = tracy.trace(@src());
277295
defer tracy_zone.end();
278296

297+
if (!builder.wantKind(.@"source.fixAll") and !builder.wantKind(.quickfix)) return;
298+
279299
const tree = builder.handle.tree;
280300
const token_tags = tree.tokens.items(.tag);
281301

282302
const source = tree.source;
283-
const capture_loc = getCaptureLoc(source, loc) orelse return;
303+
304+
try actions.ensureUnusedCapacity(builder.arena, 3);
305+
306+
if (builder.wantKind(.quickfix)) {
307+
const capture_loc = getCaptureLoc(source, loc) orelse return;
308+
309+
const remove_cap_loc = builder.createTextEditLoc(capture_loc, "");
310+
actions.appendAssumeCapacity(.{
311+
.title = "discard capture name",
312+
.kind = .quickfix,
313+
.isPreferred = false,
314+
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(loc, "_")}),
315+
});
316+
317+
// prevent adding duplicate 'remove capture' action.
318+
// search for a matching action by comparing ranges.
319+
const gop = try remove_capture_actions.getOrPut(builder.arena, remove_cap_loc.range);
320+
if (!gop.found_existing) {
321+
actions.appendAssumeCapacity(.{
322+
.title = "remove capture",
323+
.kind = .quickfix,
324+
.isPreferred = false,
325+
.edit = try builder.createWorkspaceEdit(&.{remove_cap_loc}),
326+
});
327+
}
328+
}
329+
330+
if (!builder.wantKind(.@"source.fixAll")) return;
284331

285332
const identifier_token = offsets.sourceIndexToTokenIndex(tree, loc.start);
286333
if (token_tags[identifier_token] != .identifier) return;
@@ -328,42 +375,22 @@ fn handleUnusedCapture(
328375
// if we are on the last capture of the block, we need to add an additional newline
329376
// i.e |a, b| { ... } -> |a, b| { ... \n_ = a; \n_ = b;\n }
330377
const add_suffix_newline = is_last_capture and token_tags[insert_token + 1] == .r_brace and tree.tokensOnSameLine(insert_token, insert_token + 1);
331-
332378
const insert_index, const new_text = try createDiscardText(builder, identifier_name, insert_token, true, add_suffix_newline);
333-
const action1: types.CodeAction = .{
379+
380+
actions.insertAssumeCapacity(0, .{
334381
.title = "discard capture",
335382
.kind = .@"source.fixAll",
336383
.isPreferred = true,
337384
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(insert_index, new_text)}),
338-
};
339-
const action2: types.CodeAction = .{
340-
.title = "discard capture name",
341-
.kind = .quickfix,
342-
.isPreferred = false,
343-
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(loc, "_")}),
344-
};
345-
346-
// prevent adding duplicate 'remove capture' action.
347-
// search for a matching action by comparing ranges.
348-
const remove_cap_loc = builder.createTextEditLoc(capture_loc, "");
349-
const gop = try remove_capture_actions.getOrPut(builder.arena, remove_cap_loc.range);
350-
if (gop.found_existing)
351-
try actions.insertSlice(builder.arena, 0, &.{ action1, action2 })
352-
else {
353-
const action0 = types.CodeAction{
354-
.title = "remove capture",
355-
.kind = .quickfix,
356-
.isPreferred = false,
357-
.edit = try builder.createWorkspaceEdit(&.{remove_cap_loc}),
358-
};
359-
try actions.insertSlice(builder.arena, 0, &.{ action0, action1, action2 });
360-
}
385+
});
361386
}
362387

363388
fn handlePointlessDiscard(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
364389
const tracy_zone = tracy.trace(@src());
365390
defer tracy_zone.end();
366391

392+
if (!builder.wantKind(.@"source.fixAll")) return;
393+
367394
const edit_loc = getDiscardLoc(builder.handle.tree.source, loc) orelse return;
368395

369396
try actions.append(builder.arena, .{
@@ -377,6 +404,11 @@ fn handlePointlessDiscard(builder: *Builder, actions: *std.ArrayListUnmanaged(ty
377404
}
378405

379406
fn handleVariableNeverMutated(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
407+
const tracy_zone = tracy.trace(@src());
408+
defer tracy_zone.end();
409+
410+
if (!builder.wantKind(.quickfix)) return;
411+
380412
const source = builder.handle.tree.source;
381413

382414
const var_keyword_end = 1 + (std.mem.lastIndexOfNone(u8, source[0..loc.start], &std.ascii.whitespace) orelse return);
@@ -399,6 +431,11 @@ fn handleVariableNeverMutated(builder: *Builder, actions: *std.ArrayListUnmanage
399431
}
400432

401433
fn handleUnorganizedImport(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction)) !void {
434+
const tracy_zone = tracy.trace(@src());
435+
defer tracy_zone.end();
436+
437+
if (!builder.wantKind(.@"source.organizeImports")) return;
438+
402439
const tree = builder.handle.tree;
403440
if (tree.errors.len != 0) return;
404441

tests/lsp_features/code_actions.zig

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,10 @@ fn testDiagnostic(
685685
.start = .{ .line = 0, .character = 0 },
686686
.end = offsets.indexToPosition(before, before.len, ctx.server.offset_encoding),
687687
},
688-
.context = .{ .diagnostics = diagnostics },
688+
.context = .{
689+
.diagnostics = diagnostics,
690+
.only = if (options.filter_kind) |kind| &.{kind} else null,
691+
},
689692
};
690693

691694
@setEvalBranchQuota(5000);
@@ -700,8 +703,13 @@ fn testDiagnostic(
700703
for (response) |action| {
701704
const code_action: types.CodeAction = action.CodeAction;
702705

703-
if (options.filter_kind) |kind| if (!code_action.kind.?.eql(kind)) continue;
704-
if (options.filter_title) |title| if (!std.mem.eql(u8, title, code_action.title)) continue;
706+
if (options.filter_kind) |kind| {
707+
// check that `types.CodeActionContext.only` is being respected
708+
try std.testing.expectEqual(code_action.kind.?, kind);
709+
}
710+
if (options.filter_title) |title| {
711+
if (!std.mem.eql(u8, title, code_action.title)) continue;
712+
}
705713

706714
const workspace_edit = code_action.edit.?;
707715
const changes = workspace_edit.changes.?.map;

0 commit comments

Comments
 (0)