From 96368e13e9b1aaacc570e4825b8787307f0d05e1 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli <stevearc@stevearc.com> Date: Mon, 10 Jun 2024 16:44:59 -0500 Subject: [PATCH] feat: keymap actions can be parameterized --- README.md | 8 +- doc/oil.txt | 116 ++++++++++++++++++--------- lua/oil/actions.lua | 171 ++++++++++++++++++++++++++++++++++++---- lua/oil/config.lua | 8 +- lua/oil/keymap_util.lua | 24 +++++- scripts/generate.py | 60 +++++++++++++- 6 files changed, 321 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d96ed189..fa310590 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,16 @@ require("oil").setup({ keymaps = { ["g?"] = "actions.show_help", ["<CR>"] = "actions.select", - ["<C-s>"] = "actions.select_vsplit", - ["<C-h>"] = "actions.select_split", - ["<C-t>"] = "actions.select_tab", + ["<C-s>"] = { "actions.select_split", opts = { vertical = true } }, + ["<C-h>"] = { "actions.select_split", opts = { horizontal = true } }, + ["<C-t>"] = { "actions.select_split", opts = { tab = true } }, ["<C-p>"] = "actions.preview", ["<C-c>"] = "actions.close", ["<C-l>"] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/doc/oil.txt b/doc/oil.txt index 056dc524..548c7396 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -75,16 +75,16 @@ CONFIG *oil-confi keymaps = { ["g?"] = "actions.show_help", ["<CR>"] = "actions.select", - ["<C-s>"] = "actions.select_vsplit", - ["<C-h>"] = "actions.select_split", - ["<C-t>"] = "actions.select_tab", + ["<C-s>"] = { "actions.select_split", opts = { vertical = true } }, + ["<C-h>"] = { "actions.select_split", opts = { horizontal = true } }, + ["<C-t>"] = { "actions.select_split", opts = { tab = true } }, ["<C-p>"] = "actions.preview", ["<C-c>"] = "actions.close", ["<C-l>"] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", @@ -435,38 +435,71 @@ birthtime *column-birthtim -------------------------------------------------------------------------------- ACTIONS *oil-actions* -These are actions that can be used in the `keymaps` section of config options. -You can also call them directly with -`require("oil.actions").action_name.callback()` - -add_to_loclist *actions.add_to_loclist* - Adds files in the current oil directory to the location list, keeping the - previous entries. +The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. +>lua + keymaps = { + -- Mappings can be a string + ["~"] = "<cmd>edit $HOME<CR>", + -- Mappings can be a function + ["gd"] = function() + require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) + end, + -- You can pass additional opts to vim.keymap.set by using + -- a table with the mapping as the first element. + ["<leader>ff"] = { + function() + require("telescope.builtin").find_files({ + cwd = require("oil").get_current_dir() + }) + end, + mode = "n", + nowait = true, + desc = "Find files in the current directory" + }, + -- Mappings that are a string starting with "actions." will be + -- one of the built-in actions, documented below. + ["`"] = "actions.tcd", + -- Some actions have parameters. These are passed in via the `opts` key. + ["<leader>:"] = { + "actions.open_cmdline", + opts = { + shorten_path = true, + modify = ":h", + }, + desc = "Open the command line with the current directory as an argument", + }, + } -add_to_qflist *actions.add_to_qflist* - Adds files in the current oil directory to the quickfix list, keeping the - previous entries. +Below are the actions that can be used in the `keymaps` section of config +options. You can refer to them as strings (e.g. "actions.<action_name>") or you +can use the functions directly with +`require("oil.actions").action_name.callback()` cd *actions.cd* :cd to the current oil directory + Parameters: + {scope} `nil|"tab"|"win"` Scope of the directory change (e.g. use |:tcd| + or |:lcd|) + {silent} `boolean` Do not show a message when changing directories + change_sort *actions.change_sort* Change the sort order + Parameters: + {sort} `oil.SortSpec[]` List of columns plus direction (see + |oil.set_sort|) instead of interactive selection + close *actions.close* Close oil and restore original buffer -copy_entry_filename *actions.copy_entry_filename* - Yank the filename of the entry under the cursor to a register - -copy_entry_path *actions.copy_entry_path* - Yank the filepath of the entry under the cursor to a register - open_cmdline *actions.open_cmdline* Open vim cmdline with current entry as an argument -open_cmdline_dir *actions.open_cmdline_dir* - Open vim cmdline with current directory as an argument + Parameters: + {modify} `string` Modify the path with |fnamemodify()| using this as + the mods argument + {shorten_path} `boolean` Use relative paths when possible open_cwd *actions.open_cwd* Open oil in Neovim's current working directory @@ -493,38 +526,47 @@ preview_scroll_up *actions.preview_scroll_u refresh *actions.refresh* Refresh current directory list + Parameters: + {force} `boolean` When true, do not prompt user if they will be discarding + changes + select *actions.select* Open the entry under the cursor -select_split *actions.select_split* - Open the entry under the cursor in a horizontal split - -select_tab *actions.select_tab* - Open the entry under the cursor in a new tab - -select_vsplit *actions.select_vsplit* - Open the entry under the cursor in a vertical split - -send_to_loclist *actions.send_to_loclist* - Sends files in the current oil directory to the location list, replacing the - previous entries. + Parameters: + {close} `boolean` Close the original oil buffer once selection is + made + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + {tab} `boolean` Open the buffer in a new tab + {vertical} `boolean` Open the buffer in a vertical split send_to_qflist *actions.send_to_qflist* Sends files in the current oil directory to the quickfix list, replacing the previous entries. + Parameters: + {action} `"r"|"a"` Replace or add to current quickfix list (see + |setqflist-action|) + {target} `"qflist"|"loclist"` The target list to send files to + show_help *actions.show_help* Show default keymaps -tcd *actions.tcd* - :tcd to the current oil directory - toggle_hidden *actions.toggle_hidden* Toggle hidden files and directories toggle_trash *actions.toggle_trash* Jump to and from the trash for the current directory +yank_entry *actions.yank_entry* + Yank the filepath of the entry under the cursor to a register + + Parameters: + {modify} `string` Modify the path with |fnamemodify()| using this as the + mods argument + -------------------------------------------------------------------------------- HIGHLIGHTS *oil-highlights* diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1bb885ee..7fc27ce2 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -4,20 +4,48 @@ local util = require("oil.util") local M = {} M.show_help = { - desc = "Show default keymaps", callback = function() local config = require("oil.config") require("oil.keymap_util").show_help(config.keymaps) end, + desc = "Show default keymaps", } M.select = { desc = "Open the entry under the cursor", - callback = oil.select, + callback = function(opts) + opts = opts or {} + local callback = opts.callback + opts.callback = nil + oil.select(opts, callback) + end, + parameters = { + vertical = { + type = "boolean", + desc = "Open the buffer in a vertical split", + }, + horizontal = { + type = "boolean", + desc = "Open the buffer in a horizontal split", + }, + split = { + type = '"aboveleft"|"belowright"|"topleft"|"botright"', + desc = "Split modifier", + }, + tab = { + type = "boolean", + desc = "Open the buffer in a new tab", + }, + close = { + type = "boolean", + desc = "Close the original oil buffer once selection is made", + }, + }, } M.select_vsplit = { desc = "Open the entry under the cursor in a vertical split", + deprecated = true, callback = function() oil.select({ vertical = true }) end, @@ -25,6 +53,7 @@ M.select_vsplit = { M.select_split = { desc = "Open the entry under the cursor in a horizontal split", + deprecated = true, callback = function() oil.select({ horizontal = true }) end, @@ -32,6 +61,7 @@ M.select_split = { M.select_tab = { desc = "Open the entry under the cursor in a new tab", + deprecated = true, callback = function() oil.select({ tab = true }) end, @@ -98,11 +128,14 @@ M.close = { } ---@param cmd string -local function cd(cmd) +---@param silent? boolean +local function cd(cmd, silent) local dir = oil.get_current_dir() if dir then vim.cmd({ cmd = cmd, args = { dir } }) - vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) + if not silent then + vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) + end else vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) end @@ -110,13 +143,31 @@ end M.cd = { desc = ":cd to the current oil directory", - callback = function() - cd("cd") + callback = function(opts) + opts = opts or {} + local cmd = "cd" + if opts.scope == "tab" then + cmd = "tcd" + elseif opts.scope == "win" then + cmd = "lcd" + end + cd(cmd, opts.silent) end, + parameters = { + scope = { + type = 'nil|"tab"|"win"', + desc = "Scope of the directory change (e.g. use |:tcd| or |:lcd|)", + }, + silent = { + type = "boolean", + desc = "Do not show a message when changing directories", + }, + }, } M.tcd = { desc = ":tcd to the current oil directory", + deprecated = true, callback = function() cd("tcd") end, @@ -200,8 +251,12 @@ M.open_external = { return end local path = dir .. entry.name - -- TODO use vim.ui.open once this is resolved - -- https://github.com/neovim/neovim/issues/24567 + + if vim.ui.open then + vim.ui.open(path) + return + end + local cmd, err = get_open_cmd(path) if not cmd then vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR) @@ -214,8 +269,9 @@ M.open_external = { M.refresh = { desc = "Refresh current directory list", - callback = function() - if vim.bo.modified then + callback = function(opts) + opts = opts or {} + if vim.bo.modified and not opts.force then local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes") if not ok or choice ~= 2 then return @@ -226,6 +282,12 @@ M.refresh = { -- :h CTRL-L-default vim.cmd.nohlsearch() end, + parameters = { + force = { + desc = "When true, do not prompt user if they will be discarding changes", + type = "boolean", + }, + }, } local function open_cmdline_with_path(path) @@ -236,7 +298,10 @@ end M.open_cmdline = { desc = "Open vim cmdline with current entry as an argument", - callback = function() + callback = function(opts) + opts = vim.tbl_deep_extend("keep", opts or {}, { + shorten_path = true, + }) local config = require("oil.config") local fs = require("oil.fs") local entry = oil.get_cursor_entry() @@ -252,13 +317,53 @@ M.open_cmdline = { if not adapter or not path or adapter.name ~= "files" then return end - local fullpath = fs.shorten_path(fs.posix_to_os_path(path) .. entry.name) + local fullpath = fs.posix_to_os_path(path) .. entry.name + if opts.modify then + fullpath = vim.fn.fnamemodify(fullpath, opts.modify) + end + if opts.shorten_path then + fullpath = fs.shorten_path(fullpath) + end open_cmdline_with_path(fullpath) end, + parameters = { + modify = { + desc = "Modify the path with |fnamemodify()| using this as the mods argument", + type = "string", + }, + shorten_path = { + desc = "Use relative paths when possible", + type = "boolean", + }, + }, +} + +M.yank_entry = { + desc = "Yank the filepath of the entry under the cursor to a register", + callback = function(opts) + opts = opts or {} + local entry = oil.get_cursor_entry() + local dir = oil.get_current_dir() + if not entry or not dir then + return + end + local path = dir .. entry.name + if opts.modify then + path = vim.fn.fnamemodify(path, opts.modify) + end + vim.fn.setreg(vim.v.register, path) + end, + parameters = { + modify = { + desc = "Modify the path with |fnamemodify()| using this as the mods argument", + type = "string", + }, + }, } M.copy_entry_path = { desc = "Yank the filepath of the entry under the cursor to a register", + deprecated = true, callback = function() local entry = oil.get_cursor_entry() local dir = oil.get_current_dir() @@ -271,6 +376,7 @@ M.copy_entry_path = { M.copy_entry_filename = { desc = "Yank the filename of the entry under the cursor to a register", + deprecated = true, callback = function() local entry = oil.get_cursor_entry() if not entry then @@ -282,6 +388,7 @@ M.copy_entry_filename = { M.open_cmdline_dir = { desc = "Open vim cmdline with current directory as an argument", + deprecated = true, callback = function() local fs = require("oil.fs") local dir = oil.get_current_dir() @@ -293,7 +400,14 @@ M.open_cmdline_dir = { M.change_sort = { desc = "Change the sort order", - callback = function() + callback = function(opts) + opts = opts or {} + + if opts.sort then + oil.set_sort(opts.sort) + return + end + local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" } vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col) if not col then @@ -315,6 +429,12 @@ M.change_sort = { ) end) end, + parameters = { + sort = { + type = "oil.SortSpec[]", + desc = "List of columns plus direction (see |oil.set_sort|) instead of interactive selection", + }, + }, } M.toggle_trash = { @@ -348,16 +468,31 @@ M.toggle_trash = { M.send_to_qflist = { desc = "Sends files in the current oil directory to the quickfix list, replacing the previous entries.", - callback = function() - util.send_to_quickfix({ + callback = function(opts) + opts = vim.tbl_deep_extend("keep", opts or {}, { target = "qflist", - mode = "r", + action = "r", + }) + util.send_to_quickfix({ + target = opts.target, + action = opts.action, }) end, + parameters = { + target = { + type = '"qflist"|"loclist"', + desc = "The target list to send files to", + }, + action = { + type = '"r"|"a"', + desc = "Replace or add to current quickfix list (see |setqflist-action|)", + }, + }, } M.add_to_qflist = { desc = "Adds files in the current oil directory to the quickfix list, keeping the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "qflist", @@ -368,6 +503,7 @@ M.add_to_qflist = { M.send_to_loclist = { desc = "Sends files in the current oil directory to the location list, replacing the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "loclist", @@ -378,6 +514,7 @@ M.send_to_loclist = { M.add_to_loclist = { desc = "Adds files in the current oil directory to the location list, keeping the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "loclist", @@ -395,6 +532,8 @@ M._get_actions = function() table.insert(ret, { name = name, desc = action.desc, + deprecated = action.deprecated, + parameters = action.parameters, }) end end diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 1defbe1e..af19c051 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -58,16 +58,16 @@ local default_config = { keymaps = { ["g?"] = "actions.show_help", ["<CR>"] = "actions.select", - ["<C-s>"] = "actions.select_vsplit", - ["<C-h>"] = "actions.select_split", - ["<C-t>"] = "actions.select_tab", + ["<C-s>"] = { "actions.select_split", opts = { vertical = true } }, + ["<C-h>"] = { "actions.select_split", opts = { horizontal = true } }, + ["<C-t>"] = { "actions.select_split", opts = { tab = true } }, ["<C-p>"] = "actions.preview", ["<C-c>"] = "actions.close", ["<C-l>"] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 988a9d82..834ce93c 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -10,10 +10,14 @@ local M = {} ---@return string|nil mode local function resolve(rhs) if type(rhs) == "string" and vim.startswith(rhs, "actions.") then - return resolve(actions[vim.split(rhs, ".", { plain = true })[2]]) + local action_name = vim.split(rhs, ".", { plain = true })[2] + local action = actions[action_name] + assert(action, "Unknown action name: " .. action_name) + return resolve(action) elseif type(rhs) == "table" then local opts = vim.deepcopy(rhs) - local callback = opts.callback + -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap + local callback = resolve(opts.callback or opts[1]) local mode = opts.mode if type(rhs.callback) == "string" then local action_opts, action_mode @@ -21,8 +25,24 @@ local function resolve(rhs) opts = vim.tbl_extend("keep", opts, action_opts) mode = mode or action_mode end + + -- remove all the keys that we can't pass as options to `vim.keymap.set` opts.callback = nil opts.mode = nil + opts[1] = nil + opts.deprecated = nil + opts.parameters = nil + + if opts.opts and type(callback) == "function" then + local callback_args = opts.opts + opts.opts = nil + local orig_callback = callback + callback = function() + ---@diagnostic disable-next-line: redundant-parameter + orig_callback(callback_args) + end + end + return callback, opts, mode else return rhs, {} diff --git a/scripts/generate.py b/scripts/generate.py index bf2358f5..7202dc86 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -2,7 +2,7 @@ import os.path import re from dataclasses import dataclass, field -from typing import List +from typing import Any, Dict, List from nvim_doc_tools import ( LuaParam, @@ -13,7 +13,6 @@ indent, leftright, parse_directory, - parse_functions, read_nvim_json, read_section, render_md_api2, @@ -228,21 +227,76 @@ def get_highlights_vimdoc() -> "VimdocSection": return section +def load_params(params: Dict[str, Any]) -> List[LuaParam]: + ret = [] + for name, data in sorted(params.items()): + ret.append(LuaParam(name, data["type"], data["desc"])) + return ret + + def get_actions_vimdoc() -> "VimdocSection": section = VimdocSection("Actions", "oil-actions", ["\n"]) + section.body.append( + """The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. +>lua + keymaps = { + -- Mappings can be a string + ["~"] = "<cmd>edit $HOME<CR>", + -- Mappings can be a function + ["gd"] = function() + require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) + end, + -- You can pass additional opts to vim.keymap.set by using + -- a table with the mapping as the first element. + ["<leader>ff"] = { + function() + require("telescope.builtin").find_files({ + cwd = require("oil").get_current_dir() + }) + end, + mode = "n", + nowait = true, + desc = "Find files in the current directory" + }, + -- Mappings that are a string starting with "actions." will be + -- one of the built-in actions, documented below. + ["`"] = "actions.tcd", + -- Some actions have parameters. These are passed in via the `opts` key. + ["<leader>:"] = { + "actions.open_cmdline", + opts = { + shorten_path = true, + modify = ":h", + }, + desc = "Open the command line with the current directory as an argument", + }, + } +""" + ) + section.body.append("\n") section.body.extend( wrap( - """These are actions that can be used in the `keymaps` section of config options. You can also call them directly with `require("oil.actions").action_name.callback()`""" + """Below are the actions that can be used in the `keymaps` section of config options. You can refer to them as strings (e.g. "actions.<action_name>") or you can use the functions directly with `require("oil.actions").action_name.callback()`""" ) ) section.body.append("\n") actions = read_nvim_json('require("oil.actions")._get_actions()') actions.sort(key=lambda a: a["name"]) for action in actions: + if action.get("deprecated"): + continue name = action["name"] desc = action["desc"] section.body.append(leftright(name, f"*actions.{name}*")) section.body.extend(wrap(desc, 4)) + params = action.get("parameters") + if params: + section.body.append("\n") + section.body.append(" Parameters:\n") + section.body.extend( + format_vimdoc_params(load_params(params), LuaTypes(), 6) + ) + section.body.append("\n") return section