implement textDocument/codeAction

This commit is contained in:
Techatrix 2022-09-25 01:04:29 +02:00
parent ca58cbb3f3
commit 1b64db8a4c
2 changed files with 520 additions and 2 deletions

View File

@ -12,6 +12,7 @@ const references = @import("references.zig");
const offsets = @import("offsets.zig"); const offsets = @import("offsets.zig");
const semantic_tokens = @import("semantic_tokens.zig"); const semantic_tokens = @import("semantic_tokens.zig");
const inlay_hints = @import("inlay_hints.zig"); const inlay_hints = @import("inlay_hints.zig");
const code_actions = @import("code_actions.zig");
const shared = @import("shared.zig"); const shared = @import("shared.zig");
const Ast = std.zig.Ast; const Ast = std.zig.Ast;
const tracy = @import("tracy.zig"); const tracy = @import("tracy.zig");
@ -1569,7 +1570,7 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req:
.completionProvider = .{ .resolveProvider = false, .triggerCharacters = &[_][]const u8{ ".", ":", "@", "]" }, .completionItem = .{ .labelDetailsSupport = true } }, .completionProvider = .{ .resolveProvider = false, .triggerCharacters = &[_][]const u8{ ".", ":", "@", "]" }, .completionItem = .{ .labelDetailsSupport = true } },
.documentHighlightProvider = true, .documentHighlightProvider = true,
.hoverProvider = true, .hoverProvider = true,
.codeActionProvider = false, .codeActionProvider = true,
.declarationProvider = true, .declarationProvider = true,
.definitionProvider = true, .definitionProvider = true,
.typeDefinitionProvider = true, .typeDefinitionProvider = true,
@ -2257,6 +2258,38 @@ fn inlayHintHandler(server: *Server, writer: anytype, id: types.RequestId, req:
return try respondGeneric(writer, id, null_result_response); return try respondGeneric(writer, id, null_result_response);
} }
fn codeActionHandler(server: *Server, writer: anytype, id: types.RequestId, req: requests.CodeAction) !void {
const handle = server.document_store.getHandle(req.params.textDocument.uri) orelse {
log.warn("Trying to get code actions of non existent document {s}", .{req.params.textDocument.uri});
return try respondGeneric(writer, id, null_result_response);
};
const allocator = server.arena.allocator();
var builder = code_actions.Builder{
.arena = &server.arena,
.document_store = &server.document_store,
.handle = handle,
.offset_encoding = server.offset_encoding,
};
var actions = std.ArrayListUnmanaged(types.CodeAction){};
for (req.params.context.diagnostics) |diagnostic| {
try builder.generateCodeAction(diagnostic, &actions);
}
for (actions.items) |*action| {
// TODO query whether SourceFixAll is supported by the server
if (action.kind == .SourceFixAll) action.kind = .QuickFix;
}
return try send(writer, allocator, types.Response{
.id = id,
.result = .{ .CodeAction = actions.items },
});
}
// Needed for the hack seen below. // Needed for the hack seen below.
fn extractErr(val: anytype) anyerror { fn extractErr(val: anytype) anyerror {
val catch |e| return e; val catch |e| return e;
@ -2368,6 +2401,7 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void
.{ "textDocument/rename", requests.Rename, renameHandler }, .{ "textDocument/rename", requests.Rename, renameHandler },
.{ "textDocument/references", requests.References, referencesHandler }, .{ "textDocument/references", requests.References, referencesHandler },
.{ "textDocument/documentHighlight", requests.DocumentHighlight, documentHighlightHandler }, .{ "textDocument/documentHighlight", requests.DocumentHighlight, documentHighlightHandler },
.{ "textDocument/codeAction", requests.CodeAction, codeActionHandler },
.{ "workspace/didChangeConfiguration", std.json.Value, didChangeConfigurationHandler }, .{ "workspace/didChangeConfiguration", std.json.Value, didChangeConfigurationHandler },
}; };
@ -2408,7 +2442,6 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void
// needs a response) or false if the method is a notification (in which // needs a response) or false if the method is a notification (in which
// case it should be silently ignored) // case it should be silently ignored)
const unimplemented_map = std.ComptimeStringMap(bool, .{ const unimplemented_map = std.ComptimeStringMap(bool, .{
.{ "textDocument/codeAction", true },
.{ "textDocument/codeLens", true }, .{ "textDocument/codeLens", true },
.{ "textDocument/documentLink", true }, .{ "textDocument/documentLink", true },
.{ "textDocument/rangeFormatting", true }, .{ "textDocument/rangeFormatting", true },

485
src/code_actions.zig Normal file
View File

@ -0,0 +1,485 @@
const std = @import("std");
const Ast = std.zig.Ast;
const DocumentStore = @import("DocumentStore.zig");
const analysis = @import("analysis.zig");
const ast = @import("ast.zig");
const types = @import("types.zig");
const requests = @import("requests.zig");
const offsets = @import("offsets.zig");
pub const Builder = struct {
arena: *std.heap.ArenaAllocator,
document_store: *DocumentStore,
handle: *DocumentStore.Handle,
offset_encoding: offsets.Encoding,
pub fn generateCodeAction(
builder: *Builder,
diagnostic: types.Diagnostic,
actions: *std.ArrayListUnmanaged(types.CodeAction),
) error{OutOfMemory}!void {
const kind = DiagnosticKind.parse(diagnostic.message) orelse return;
const loc = offsets.rangeToLoc(builder.text(), diagnostic.range, builder.offset_encoding);
switch (kind) {
.unused => |id| switch (id) {
.@"function parameter" => try handleUnusedFunctionParameter(builder, actions, loc),
.@"local constant" => try handleUnusedVariableOrConstant(builder, actions, loc),
.@"local variable" => try handleUnusedVariableOrConstant(builder, actions, loc),
.@"loop index capture" => try handleUnusedIndexCapture(builder, actions, loc),
.@"capture" => try handleUnusedCapture(builder, actions, loc),
},
.pointless_discard => try handlePointlessDiscard(builder, actions, loc),
.omit_discard => |id| switch (id) {
.@"index capture" => try handleUnusedIndexCapture(builder, actions, loc),
.@"error capture" => try handleUnusedCapture(builder, actions, loc),
},
.unreachable_code => {
// TODO
// autofix: comment out code
// fix: remove code
},
}
}
pub fn createTextEditLoc(self: *Builder, loc: offsets.Loc, new_text: []const u8) types.TextEdit {
const range = offsets.locToRange(self.text(), loc, self.offset_encoding);
return types.TextEdit{ .range = range, .newText = new_text };
}
pub fn createTextEditPos(self: *Builder, index: usize, new_text: []const u8) types.TextEdit {
const position = offsets.indexToPosition(self.text(), index, self.offset_encoding);
return types.TextEdit{ .range = .{ .start = position, .end = position }, .newText = new_text };
}
pub fn createWorkspaceEdit(self: *Builder, edits: []const types.TextEdit) error{OutOfMemory}!types.WorkspaceEdit {
var text_edits = std.ArrayListUnmanaged(types.TextEdit){};
try text_edits.appendSlice(self.arena.allocator(), edits);
var workspace_edit = types.WorkspaceEdit{ .changes = .{} };
try workspace_edit.changes.putNoClobber(self.arena.allocator(), self.handle.uri(), text_edits);
return workspace_edit;
}
fn text(self: *Builder) []const u8 {
return self.handle.document.text;
}
};
fn handleUnusedFunctionParameter(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
const identifier_name = offsets.locToSlice(builder.text(), loc);
const tree = builder.handle.tree;
const node_tags = tree.nodes.items(.tag);
const node_datas = tree.nodes.items(.data);
const node_tokens = tree.nodes.items(.main_token);
const token_starts = tree.tokens.items(.start);
const decl = (try analysis.lookupSymbolGlobal(
builder.document_store,
builder.arena,
builder.handle,
identifier_name,
loc.start,
)) orelse return;
const payload = switch (decl.decl.*) {
.param_payload => |pay| pay,
else => return,
};
std.debug.assert(node_tags[payload.func] == .fn_decl);
const block = node_datas[payload.func].rhs;
const indent = offsets.lineSliceUntilIndex(builder.text(), token_starts[node_tokens[payload.func]]).len;
const new_text = try createDiscardText(builder.arena.allocator(), identifier_name, indent + 4);
const index = token_starts[node_tokens[block]] + 1;
const action1 = types.CodeAction{
.title = "discard function parameter",
.kind = .SourceFixAll,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(index, new_text)}),
};
const param_loc = .{
.start = offsets.tokenToIndex(tree, ast.paramFirstToken(tree, payload.param)),
.end = offsets.tokenToLoc(tree, ast.paramLastToken(tree, payload.param)).end,
};
// TODO fix formatting
// TODO remove trailing comma on last parameter
const action2 = types.CodeAction{
.title = "remove function parameter",
.kind = .QuickFix,
.isPreferred = false,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(param_loc, "")}),
};
try actions.appendSlice(builder.arena.allocator(), &.{ action1, action2 });
}
fn handleUnusedVariableOrConstant(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
const identifier_name = offsets.locToSlice(builder.text(), loc);
const tree = builder.handle.tree;
const token_tags = tree.tokens.items(.tag);
const token_starts = tree.tokens.items(.start);
const decl = (try analysis.lookupSymbolGlobal(
builder.document_store,
builder.arena,
builder.handle,
identifier_name,
loc.start,
)) orelse return;
const node = switch (decl.decl.*) {
.ast_node => |node| node,
else => return,
};
const first_token = tree.firstToken(node);
const last_token = ast.lastToken(tree, node) + 1;
const indent = offsets.lineSliceUntilIndex(builder.text(), token_starts[first_token]).len;
if (token_tags[last_token] != .semicolon) return;
const new_text = try createDiscardText(builder.arena.allocator(), identifier_name, indent);
const index = token_starts[last_token] + 1;
try actions.append(builder.arena.allocator(), .{
.title = "discard value",
.kind = .SourceFixAll,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(index, new_text)}),
});
}
fn handleUnusedIndexCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
const capture_locs = getCaptureLoc(builder.text(), loc, true) orelse return;
// TODO support discarding without modifying the capture
// by adding a discard in the block scope
const is_value_discarded = std.mem.eql(u8, offsets.locToSlice(builder.text(), capture_locs.value), "_");
if (is_value_discarded) {
// |_, i| ->
// TODO fix formatting
try actions.append(builder.arena.allocator(), .{
.title = "remove capture",
.kind = .QuickFix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.loc, "")}),
});
} else {
// |v, i| -> |v|
// |v, _| -> |v|
try actions.append(builder.arena.allocator(), .{
.title = "remove index capture",
.kind = .QuickFix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(
.{ .start = capture_locs.value.end, .end = capture_locs.loc.end - 1 },
"",
)}),
});
}
}
fn handleUnusedCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
const capture_locs = getCaptureLoc(builder.text(), loc, false) orelse return;
// TODO support discarding without modifying the capture
// by adding a discard in the block scope
if (capture_locs.index != null) {
// |v, i| -> |_, i|
try actions.append(builder.arena.allocator(), .{
.title = "discard capture",
.kind = .QuickFix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.value, "_")}),
});
} else {
// |v| ->
// TODO fix formatting
try actions.append(builder.arena.allocator(), .{
.title = "remove capture",
.kind = .QuickFix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.loc, "")}),
});
}
}
fn handlePointlessDiscard(builder: *Builder, actions: *std.ArrayListUnmanaged(types.CodeAction), loc: offsets.Loc) !void {
const edit_loc = getDiscardLoc(builder.text(), loc) orelse return;
try actions.append(builder.arena.allocator(), .{
.title = "remove pointless discard",
.kind = .SourceFixAll,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{
builder.createTextEditLoc(edit_loc, ""),
}),
});
}
// returns a discard string `\n{indent}_ = identifier_name;`
fn createDiscardText(allocator: std.mem.Allocator, identifier_name: []const u8, indent: usize) ![]const u8 {
const new_text_len = 1 + indent + "_ = ;".len + identifier_name.len;
var new_text = try std.ArrayListUnmanaged(u8).initCapacity(allocator, new_text_len);
errdefer new_text.deinit(allocator);
new_text.appendAssumeCapacity('\n');
new_text.appendNTimesAssumeCapacity(' ', indent);
new_text.appendSliceAssumeCapacity("_ = ");
new_text.appendSliceAssumeCapacity(identifier_name);
new_text.appendAssumeCapacity(';');
return new_text.toOwnedSlice(allocator);
}
const DiagnosticKind = union(enum) {
unused: IdCat,
pointless_discard: IdCat,
omit_discard: DiscardCat,
unreachable_code,
const IdCat = enum {
@"function parameter",
@"local constant",
@"local variable",
@"loop index capture",
@"capture",
};
const DiscardCat = enum {
// "discard of index capture; omit it instead"
@"index capture",
// "discard of error capture; omit it instead"
@"error capture",
};
pub fn parse(diagnostic_message: []const u8) ?DiagnosticKind {
const msg = diagnostic_message;
if (std.mem.startsWith(u8, msg, "unused ")) {
return DiagnosticKind{
.unused = parseEnum(IdCat, msg["unused ".len..]) orelse return null,
};
} else if (std.mem.startsWith(u8, msg, "pointless discard of ")) {
return DiagnosticKind{
.pointless_discard = parseEnum(IdCat, msg["pointless discard of ".len..]) orelse return null,
};
} else if (std.mem.startsWith(u8, msg, "discard of ")) {
return DiagnosticKind{
.omit_discard = parseEnum(DiscardCat, msg["discard of ".len..]) orelse return null,
};
}
return null;
}
fn parseEnum(comptime T: type, message: []const u8) ?T {
inline for (std.meta.fields(T)) |field| {
if (std.mem.startsWith(u8, message, field.name)) {
// is there a better way to achieve this?
return @intToEnum(T, field.value);
}
}
return null;
}
};
/// takes the location of an identifier which is part of a discard `_ = location_here;`
/// and returns the location from '_' until ';' or null on failure
fn getDiscardLoc(text: []const u8, loc: offsets.Loc) ?offsets.Loc {
// check of the loc points to a valid identifier
for (offsets.locToSlice(text, loc)) |c| {
if (!isSymbolChar(c)) return null;
}
// check if the identifier is followed by a colon
const colon_position = found: {
var i = loc.end;
while (i < text.len) : (i += 1) {
switch (text[i]) {
' ' => continue,
';' => break :found i + 1,
else => return null,
}
}
return null;
};
// check if the identifier is preceed by a equal sign and then an underscore
var i: usize = loc.start - 1;
var found_equal_sign = false;
const underscore_position = found: {
while (true) : (i -= 1) {
if (i == 0) return null;
switch (text[i]) {
' ' => {},
'=' => {
if (found_equal_sign) return null;
found_equal_sign = true;
},
'_' => if (found_equal_sign) break :found i else return null,
else => return null,
}
}
};
// move backwards until we find a newline
i = underscore_position - 1;
const start_position = found: {
while (true) : (i -= 1) {
if (i == 0) break :found underscore_position;
switch (text[i]) {
' ' => {},
'\n' => break :found i,
else => break :found underscore_position,
}
}
};
return offsets.Loc{
.start = start_position,
.end = colon_position,
};
}
const CaptureLocs = struct {
loc: offsets.Loc,
value: offsets.Loc,
index: ?offsets.Loc,
};
/// takes the location of an identifier which is part of a payload `|value, index|`
/// and returns the location from '|' until '|' or null on failure
/// use `is_index_payload` to indicate whether `loc` points to `value` or `index`
fn getCaptureLoc(text: []const u8, loc: offsets.Loc, is_index_payload: bool) ?CaptureLocs {
const value_end = if (!is_index_payload) loc.end else found: {
// move back until we find a comma
const comma_position = found_comma: {
var i = loc.start - 1;
while (i != 0) : (i -= 1) {
switch (text[i]) {
' ' => continue,
',' => break :found_comma i,
else => return null,
}
} else return null;
};
// trim space
var i = comma_position - 1;
while (i != 0) : (i -= 1) {
switch (text[i]) {
' ' => continue,
else => {
if (!isSymbolChar(text[i])) return null;
break :found i + 1;
},
}
} else return null;
};
const value_start = if (!is_index_payload) loc.start else found: {
// move back until we find a non identifier character
var i = value_end - 1;
while (i != 0) : (i -= 1) {
if (isSymbolChar(text[i])) continue;
switch (text[i]) {
' ', '|', '*' => break :found i + 1,
else => return null,
}
} else return null;
};
var index: ?offsets.Loc = null;
if (is_index_payload) {
index = loc;
} else blk: {
// move forward until we find a comma
const comma_position = found_comma: {
var i = value_end;
while (i < text.len) : (i += 1) {
switch (text[i]) {
' ' => continue,
',' => break :found_comma i,
else => break :blk,
}
}
break :blk;
};
// trim space
const index_start = found_start: {
var i = comma_position + 1;
while (i < text.len) : (i += 1) {
switch (text[i]) {
' ' => continue,
else => {
if (!isSymbolChar(text[i])) break :blk;
break :found_start i;
},
}
}
break :blk;
};
// move forward until we find a non identifier character
var i = index_start + 1;
while (i < text.len) : (i += 1) {
if (isSymbolChar(text[i])) continue;
index = offsets.Loc{
.start = index_start,
.end = i,
};
break;
}
}
const start_pipe_position = found: {
var i = value_start - 1;
while (i != 0) : (i -= 1) {
switch (text[i]) {
' ' => continue,
'|' => break :found i,
else => return null,
}
} else return null;
};
const end_pipe_position = found: {
var i: usize = if (index) |index_loc| index_loc.end else value_end;
while (i < text.len) : (i += 1) {
switch (text[i]) {
' ' => continue,
'|' => break :found i + 1,
else => return null,
}
} else return null;
};
return CaptureLocs{
.loc = .{ .start = start_pipe_position, .end = end_pipe_position },
.value = .{ .start = value_start, .end = value_end },
.index = index,
};
}
fn isSymbolChar(char: u8) bool {
return std.ascii.isAlNum(char) or char == '_';
}