diff --git a/README.md b/README.md index f272e9c..f47413f 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,11 @@ Simply call `require("nvim-surround").setup` or More information on how to configure this plugin can be found in [`:h nvim-surround.configuration`](https://github.com/kylechui/nvim-surround/blob/main/doc/nvim-surround.txt). +### Which Key Support + +You can turn Which Key for showing surround hints with configured labels by +calling `require("nvim-surround.wk-surround-plugin").set_up()`. + ## Contributing See diff --git a/doc/nvim-surround.txt b/doc/nvim-surround.txt index 30b1a9c..ca18408 100644 --- a/doc/nvim-surround.txt +++ b/doc/nvim-surround.txt @@ -25,6 +25,7 @@ CONTENTS *nvim-surround.contents* 3.3.4 Cursor ....................... |nvim-surround.setup.move_cursor| 3.3.5 Indentation ................. |nvim-surround.setup.indent_lines| 3.4. Helpers ..................................... |nvim-surround.helpers| + 3.5. Which Key Support ...................... |nvim-surround.which_key| 4. Migration Guides ................................ |nvim-surround.migrating| 4.1. Migrating v3 to v4 ............... |nvim-surround.migrating.v3_to_v4| @@ -401,6 +402,7 @@ surrounds, for example setting up a `$` surround, but only in bash files: add = { "${", "}" }, find = "$%b{}", delete = "^(..)().-(.)()$", + label = "${…}", }, }, }) @@ -540,6 +542,11 @@ containing the following keys: are directly used as the replacement pair. For example, when changing HTML tag types, only `cst` is needed, instead of `cstt`. + *nvim-surround.setup.surrounds.label* + label: ~ + An optional string that represents the label for the surround. This + is used by the Which Key plugin to show a descriptive hint for the + surround. *nvim-surround.setup.surrounds.invalid_key_behavior* `invalid_key_behavior` is a special key in the `surrounds` table that defines @@ -774,6 +781,18 @@ config.get_selections({args}) The Lua pattern matches just the HTML tag type in both the beginning and end. +-------------------------------------------------------------------------------- +3.5. Which Key Support *nvim-surround.which_key* + +|nvim-surround| provides a plugin for |which-key.nvim| that shows a popup +containing the available surrounds and their labels. To enable this, call: +>lua + require("nvim-surround.wk-surround-plugin").set_up() +< +This will enable the Which Key popup for all surround actions. The labels +shown in the popup can be configured via the `label` key in the `surrounds` +table (see |nvim-surround.setup.surrounds.label|). + ================================================================================ 4. Migration Guides *nvim-surround.migrating* diff --git a/lua/nvim-surround/annotations.lua b/lua/nvim-surround/annotations.lua index 86aa088..2936d52 100644 --- a/lua/nvim-surround/annotations.lua +++ b/lua/nvim-surround/annotations.lua @@ -29,6 +29,7 @@ ---@field find find_func ---@field delete delete_func ---@field change change_table +---@field label? string ---@class options ---@field surrounds table @@ -51,6 +52,7 @@ ---@field find? user_find ---@field delete? user_delete ---@field change? user_change +---@field label? string ---@class user_options ---@field surrounds? table diff --git a/lua/nvim-surround/config.lua b/lua/nvim-surround/config.lua index efdb127..7cf9401 100644 --- a/lua/nvim-surround/config.lua +++ b/lua/nvim-surround/config.lua @@ -9,6 +9,7 @@ M.default_opts = { return M.get_selection({ motion = "a(" }) end, delete = "^(. ?)().-( ?.)()$", + label = "( … )", }, [")"] = { add = { "(", ")" }, @@ -16,6 +17,7 @@ M.default_opts = { return M.get_selection({ motion = "a)" }) end, delete = "^(.)().-(.)()$", + label = "(…)", }, ["{"] = { add = { "{ ", " }" }, @@ -23,6 +25,7 @@ M.default_opts = { return M.get_selection({ motion = "a{" }) end, delete = "^(. ?)().-( ?.)()$", + label = "{ … }", }, ["}"] = { add = { "{", "}" }, @@ -30,6 +33,7 @@ M.default_opts = { return M.get_selection({ motion = "a}" }) end, delete = "^(.)().-(.)()$", + label = "{…}", }, ["<"] = { add = { "< ", " >" }, @@ -37,6 +41,7 @@ M.default_opts = { return M.get_selection({ motion = "a<" }) end, delete = "^(. ?)().-( ?.)()$", + label = "< … >", }, [">"] = { add = { "<", ">" }, @@ -44,6 +49,7 @@ M.default_opts = { return M.get_selection({ motion = "a>" }) end, delete = "^(.)().-(.)()$", + label = "<…>", }, ["["] = { add = { "[ ", " ]" }, @@ -51,6 +57,7 @@ M.default_opts = { return M.get_selection({ motion = "a[" }) end, delete = "^(. ?)().-( ?.)()$", + label = "[ … ]", }, ["]"] = { add = { "[", "]" }, @@ -58,6 +65,7 @@ M.default_opts = { return M.get_selection({ motion = "a]" }) end, delete = "^(.)().-(.)()$", + label = "[…]", }, ["'"] = { add = { "'", "'" }, @@ -65,6 +73,7 @@ M.default_opts = { return M.get_selection({ motion = "a'" }) end, delete = "^(.)().-(.)()$", + label = "'…'", }, ['"'] = { add = { '"', '"' }, @@ -72,6 +81,7 @@ M.default_opts = { return M.get_selection({ motion = 'a"' }) end, delete = "^(.)().-(.)()$", + label = '"…"', }, ["`"] = { add = { "`", "`" }, @@ -79,6 +89,7 @@ M.default_opts = { return M.get_selection({ motion = "a`" }) end, delete = "^(.)().-(.)()$", + label = "`…`", }, ["i"] = { -- TODO: Add find/delete/change functions add = function() @@ -90,6 +101,7 @@ M.default_opts = { end, find = function() end, delete = function() end, + label = "?…?", }, ["t"] = { add = function() @@ -123,6 +135,7 @@ M.default_opts = { end end, }, + label = "", }, ["T"] = { add = function() @@ -156,6 +169,7 @@ M.default_opts = { end end, }, + label = "", }, ["f"] = { add = function() @@ -188,6 +202,7 @@ M.default_opts = { end end, }, + label = "foo(…)", }, invalid_key_behavior = { -- By default, we ignore control characters for adding/finding because they are more likely typos than @@ -333,6 +348,26 @@ M.get_alias = function(char) return char end +---Creates a table of available hints given surrounds and aliases. +--- +---@param surrounds table +---@param aliases table +---@return table hints +---@nodiscard +M.get_hints = function(surrounds, aliases) + local hints = {} + for char, surround in pairs(surrounds) do + -- Throw away "invalid_key_behavior" if present. + if string.len(char) == 1 then + hints[char] = surround.label or char + end + end + for char, alias in pairs(aliases) do + hints[char] = type(alias) == "table" and table.concat(alias, ",") or alias + end + return hints +end + -- Gets a delimiter pair for a user-inputted character. ---@param char string|nil The user-given character. ---@param line_mode boolean Whether or not the delimiters should be put on new lines. @@ -485,6 +520,7 @@ M.translate_surround = function(char, user_surround) find = M.translate_find(user_surround.find), delete = M.translate_delete(char, user_surround.delete), change = M.translate_change(char, user_surround.change), + label = user_surround.label, } end diff --git a/lua/nvim-surround/input.lua b/lua/nvim-surround/input.lua index a406bda..3c40a0e 100644 --- a/lua/nvim-surround/input.lua +++ b/lua/nvim-surround/input.lua @@ -13,10 +13,19 @@ M.replace_termcodes = function(char) return vim.api.nvim_replace_termcodes(char, true, true, true) end --- Gets a character input from the user. +-- Gets a surround character input from the user. +-- +-- If the user has set up the WK plugin, uses that instead of vim.fn.get_char ---@return string|nil @The input character, or nil if an escape character is pressed. ---@nodiscard M.get_char = function() + if require("nvim-surround.wk-surround-plugin").plugin_set_up then + local config = require("nvim-surround.config") + return require("nvim-surround.wk-surround-plugin").pick( + config.get_hints(config.get_opts().surrounds, config.get_opts().aliases), + "n" + ) + end local ok, char = pcall(vim.fn.getcharstr) -- Return nil if input is cancelled (e.g. or ) if not ok or char == "\27" then diff --git a/lua/nvim-surround/wk-surround-plugin.lua b/lua/nvim-surround/wk-surround-plugin.lua new file mode 100644 index 0000000..7b79db7 --- /dev/null +++ b/lua/nvim-surround/wk-surround-plugin.lua @@ -0,0 +1,78 @@ +---A WK plugin for showing available surrounds with keys. +--- +---Users need to set_up this plugin before using it. +--- +---To get a char with a WK popup, call pick(). + +---@diagnostic disable: missing-fields, inject-field +---@type wk.Plugin +local M = {} + +M.name = "nvim-surround" + +-- WK requires keymaps. +-- We use "⌨" here to make sure we never collide with actual keymaps. +local keys = "⌨S" + +M.mappings = { + { + [1] = keys, + plugin = M.name, + icon = { icon = "⌨", color = "blue" }, + desc = "Nvim-surround", + mode = { "n", "x" }, + }, +} + +local selected_key = nil +local expand_hints = {} + +function M.expand() + ---@type wk.Plugin.item[] + local items = {} + + for key, label in pairs(expand_hints) do + table.insert(items, { + key = key, + desc = label, + value = "", + action = function() + selected_key = key + end, + }) + end + + table.sort(items, function(a, b) + return a.key < b.key + end) + + return items +end + +---Gets a character from the user with the provided hints. +--- +---@param hints table A table from chars to their labels. +---@param mode "n"|"x" +---@return string? selected_key +function M.pick(hints, mode) + selected_key = nil + expand_hints = hints + require("which-key").show({ keys = keys, mode = mode }) + return selected_key +end + +---Whether the final user has set up this plugin. +--- +---@type boolean +M.plugin_set_up = false + +-- This function is called "set_up" to make sure it doesn't conflict with WK’s "setup." +function M.set_up() + local wk = require("which-key") + wk.add(M.mappings) + require("which-key.plugins").plugins[M.name] = M + require("which-key.plugins")._setup(M, {}) + M.plugin_set_up = true +end + +return M diff --git a/tests/configuration_spec.lua b/tests/configuration_spec.lua index 199940c..f0643d0 100644 --- a/tests/configuration_spec.lua +++ b/tests/configuration_spec.lua @@ -635,4 +635,35 @@ describe("configuration", function() vim.cmd("normal VSdVSdVSd") check_lines({ "foo", "foo", "foo", "foobarbaz", "bar", "bar", "bar" }) end) + + it("returns correct hints including formatted aliases", function() + require("nvim-surround").setup({}) + local hints = require("nvim-surround.config").get_hints( + require("nvim-surround.config").get_opts().surrounds, + require("nvim-surround.config").get_opts().aliases + ) + assert.are.same({ + ['"'] = '"…"', + ["'"] = "'…'", + ["("] = "( … )", + [")"] = "(…)", + ["<"] = "< … >", + [">"] = "<…>", + ["["] = "[ … ]", + ["]"] = "[…]", + ["`"] = "`…`", + ["{"] = "{ … }", + ["}"] = "{…}", + B = "}", + T = "", + a = ">", + b = ")", + f = "foo(…)", + i = "?…?", + q = "\",',`", + r = "]", + s = "},],),>,\",',`", + t = "", + }, hints) + end) end)