Skip to content
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
16 changes: 6 additions & 10 deletions vim/autoload/yac_lsp_edit.vim
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,19 @@ function! s:code_action_callback(id, result) abort
endfunction

function! s:execute_code_action(action) abort
if has_key(a:action, 'has_edit') && a:action.has_edit
" This action has a direct workspace edit - we need to request it again
" For now, show a message that this isn't fully implemented
echo "Direct edit actions not yet supported. Use command-based actions."
return
" Apply workspace edits if present (LSP spec: edit before command)
if has_key(a:action, 'edits') && !empty(a:action.edits)
call yac_lsp_edit#apply_workspace_edit(a:action.edits)
endif

" Execute command if present
if has_key(a:action, 'command') && !empty(a:action.command)
" Execute the command
let arguments = has_key(a:action, 'arguments') ? a:action.arguments : []
call yac#_request('execute_command', {
\ 'file': expand('%:p'),
\ 'command_name': a:action.command,
\ 'arguments': arguments
\ }, '')
echo "Executing: " . a:action.title
else
echo "Action has no executable command"
\ }, 'yac_lsp_edit#_handle_execute_command_response')
endif
endfunction

Expand Down
2 changes: 2 additions & 0 deletions yacd/src/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ pub const App = struct {
try app.dispatcher.register("goto_declaration", &app.nav, NavigationHandler.gotoDeclaration);
try app.dispatcher.register("goto_implementation", &app.nav, NavigationHandler.gotoImplementation);
try app.dispatcher.register("references", &app.nav, NavigationHandler.references);
try app.dispatcher.register("code_action", &app.nav, NavigationHandler.codeAction);
try app.dispatcher.register("execute_command", &app.nav, NavigationHandler.executeCommand);
try app.dispatcher.register("completion", &app.comp, CompletionHandler.completion);
try app.dispatcher.register("signature_help", &app.nav, NavigationHandler.signatureHelp);
try app.dispatcher.register("did_open", &app.doc, DocumentHandler.didOpen);
Expand Down
105 changes: 105 additions & 0 deletions yacd/src/handlers/navigation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,113 @@ pub const NavigationHandler = struct {
log.debug("references: {d} locations", .{locs.items.len});
return .{ .locations = locs.items };
}

pub fn codeAction(self: *NavigationHandler, allocator: Allocator, params: vim.types.CodeActionParams) !vim.types.CodeActionResult {
log.info("codeAction {s}:{d}:{d}", .{ params.file, params.line, params.column });
const proxy = self.registry.resolve(params.file, null) catch
return .{ .actions = &.{} };

const uri = try config.fileToUri(allocator, params.file);
const lang_config = config.detectConfig(params.file) orelse
return .{ .actions = &.{} };

proxy.ensureOpen(uri, lang_config.language_id) catch {};

const pos: lsp.types.Position = .{ .line = params.line, .character = params.column };
const result = proxy.codeAction(allocator, .{
.textDocument = .{ .uri = uri },
.range = .{ .start = pos, .end = pos },
.context = .{ .diagnostics = &.{} },
}) catch return .{ .actions = &.{} };

const lsp_actions = result orelse return .{ .actions = &.{} };

var actions: std.ArrayList(vim.types.CodeActionItem) = .empty;
for (lsp_actions) |action| {
switch (action) {
.code_action => |ca| {
actions.append(allocator, convertCodeAction(allocator, ca)) catch continue;
},
.command => |cmd| {
actions.append(allocator, .{
.title = cmd.title,
.command = cmd.command,
.arguments = if (cmd.arguments) |args| args else &.{},
}) catch continue;
},
}
}

return .{ .actions = actions.items };
}

pub fn executeCommand(self: *NavigationHandler, allocator: Allocator, params: vim.types.ExecuteCommandParams) !void {
log.info("executeCommand {s}", .{params.command_name});
const proxy = self.registry.resolve(params.file, null) catch return;

_ = proxy.executeCommand(allocator, .{
.command = params.command_name,
.arguments = if (params.arguments.len > 0) params.arguments else null,
}) catch |err| {
log.warn("executeCommand failed: {s}", .{@errorName(err)});
};
}
};

/// Convert an LSP CodeAction to a Vim-friendly CodeActionItem.
fn convertCodeAction(allocator: Allocator, ca: lsp.types.CodeAction) vim.types.CodeActionItem {
const cmd_name = if (ca.command) |cmd| cmd.command else "";
const cmd_args: []const std.json.Value = if (ca.command) |cmd|
if (cmd.arguments) |args| args else &.{}
else
&.{};

const kind_str: []const u8 = if (ca.kind) |k| switch (k) {
.quickfix => "quickfix",
.refactor => "refactor",
.@"refactor.extract" => "refactor.extract",
.@"refactor.inline" => "refactor.inline",
.@"refactor.rewrite" => "refactor.rewrite",
.source => "source",
.@"source.organizeImports" => "source.organizeImports",
.@"source.fixAll" => "source.fixAll",
.custom_value => |s| s,
else => "",
} else "";

// Group edits by file to match VimScript apply_workspace_edit's expected format
var file_edits_list: std.ArrayList(vim.types.FileEdits) = .empty;
if (ca.edit) |workspace_edit| {
if (workspace_edit.changes) |changes| {
for (changes.map.keys(), changes.map.values()) |file_uri, lsp_edits| {
const file = config.uriToFile(allocator, file_uri) catch continue;
var text_edits: std.ArrayList(vim.types.TextEdit) = .empty;
for (lsp_edits) |te| {
text_edits.append(allocator, .{
.start_line = te.range.start.line,
.start_column = te.range.start.character,
.end_line = te.range.end.line,
.end_column = te.range.end.character,
.new_text = te.newText,
}) catch continue;
}
file_edits_list.append(allocator, .{
.file = file,
.edits = text_edits.items,
}) catch continue;
}
}
}

return .{
.title = ca.title,
.kind = kind_str,
.edits = file_edits_list.items,
.command = cmd_name,
.arguments = cmd_args,
};
}

/// Extract a single Location from an LSP Definition result.
fn extractLocation(result: lsp.ResultType("textDocument/definition")) ?lsp.types.Location {
const def_result = result orelse return null;
Expand Down
9 changes: 9 additions & 0 deletions yacd/src/lsp/proxy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ pub const LspProxy = struct {
return self.connection.request(allocator, "textDocument/references", params);
}

pub fn codeAction(self: *LspProxy, allocator: Allocator, params: lsp.ParamsType("textDocument/codeAction")) !lsp.ResultType("textDocument/codeAction") {
try self.ensureCapability(.codeActionProvider);
return self.connection.request(allocator, "textDocument/codeAction", params);
}

pub fn executeCommand(self: *LspProxy, allocator: Allocator, params: lsp.ParamsType("workspace/executeCommand")) !lsp.ResultType("workspace/executeCommand") {
return self.connection.request(allocator, "workspace/executeCommand", params);
}

pub fn documentSymbol(self: *LspProxy, allocator: Allocator, params: lsp.ParamsType("textDocument/documentSymbol")) !lsp.ResultType("textDocument/documentSymbol") {
try self.ensureCapability(.documentSymbolProvider);
return self.connection.request(allocator, "textDocument/documentSymbol", params);
Expand Down
47 changes: 47 additions & 0 deletions yacd/src/vim/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ pub fn ParamsType(comptime method: []const u8) type {
.{ "copilot_accept", CopilotAcceptParams },
.{ "copilot_partial_accept", CopilotPartialAcceptParams },
.{ "copilot_did_focus", FileParams },
// Code actions
.{ "code_action", CodeActionParams },
.{ "execute_command", ExecuteCommandParams },
};
inline for (map) |entry| {
if (comptime std.mem.eql(u8, method, entry[0])) return entry[1];
Expand Down Expand Up @@ -120,6 +123,9 @@ pub fn ResultType(comptime method: []const u8) type {
.{ "copilot_accept", void },
.{ "copilot_partial_accept", void },
.{ "copilot_did_focus", void },
// Code actions
.{ "code_action", CodeActionResult },
.{ "execute_command", void },
};
inline for (map) |entry| {
if (comptime std.mem.eql(u8, method, entry[0])) return entry[1];
Expand Down Expand Up @@ -529,6 +535,47 @@ pub const CopilotPartialAcceptParams = struct {
accepted_text: ?[]const u8 = null,
};

// ============================================================================
// Code Action types
// ============================================================================

pub const CodeActionParams = struct {
file: []const u8,
line: u32,
column: u32,
};

pub const TextEdit = struct {
start_line: u32,
start_column: u32,
end_line: u32,
end_column: u32,
new_text: []const u8,
};

pub const FileEdits = struct {
file: []const u8,
edits: []const TextEdit,
};

pub const CodeActionItem = struct {
title: []const u8,
kind: []const u8 = "",
edits: []const FileEdits = &.{},
command: []const u8 = "",
arguments: []const std.json.Value = &.{},
};

pub const CodeActionResult = struct {
actions: []const CodeActionItem,
};

pub const ExecuteCommandParams = struct {
file: []const u8,
command_name: []const u8,
arguments: []const std.json.Value = &.{},
};

// ============================================================================
// Tests
// ============================================================================
Expand Down
Loading