diff --git a/vim/autoload/yac_lsp_edit.vim b/vim/autoload/yac_lsp_edit.vim index 09ab7ce..0a9cd4b 100644 --- a/vim/autoload/yac_lsp_edit.vim +++ b/vim/autoload/yac_lsp_edit.vim @@ -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 diff --git a/yacd/src/app.zig b/yacd/src/app.zig index 7f5a6ae..138f93f 100644 --- a/yacd/src/app.zig +++ b/yacd/src/app.zig @@ -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); diff --git a/yacd/src/handlers/navigation.zig b/yacd/src/handlers/navigation.zig index 9002edc..2b5166b 100644 --- a/yacd/src/handlers/navigation.zig +++ b/yacd/src/handlers/navigation.zig @@ -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; diff --git a/yacd/src/lsp/proxy.zig b/yacd/src/lsp/proxy.zig index 12bd22a..d5ba3a1 100644 --- a/yacd/src/lsp/proxy.zig +++ b/yacd/src/lsp/proxy.zig @@ -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); diff --git a/yacd/src/vim/types.zig b/yacd/src/vim/types.zig index 0b92065..762fb6f 100644 --- a/yacd/src/vim/types.zig +++ b/yacd/src/vim/types.zig @@ -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]; @@ -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]; @@ -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 // ============================================================================