diff --git a/README.md b/README.md index baabed1..5a3abed 100644 --- a/README.md +++ b/README.md @@ -38,114 +38,51 @@ with `my_var` or vice versa? This plugin is for you! ## 🔽 Installation -
- - - - - - - - - - - - - - - - - - - - +If you decide not to use Telescope, you can ignore the dependencies. In that case, be sure to change +your config with `popup_type = 'select'` so that TextTransform never tries to load Telescope. - -
- -Package manager - - -Snippet -
- -[folke/lazy.nvim](https://github.com/folke/lazy.nvim) - - +### [Lazy](https://github.com/folke/lazy.nvim) ```lua --- stable version require("lazy").setup({ "chenasraf/text-transform.nvim", + -- stable version version = "*", -- or: tag = "stable" - dependencies = { - -- for Telescope popup - 'nvim-telescope/telescope.nvim', - 'nvim-lua/plenary.nvim', - }, -}) --- dev version -require("lazy").setup({ - "chenasraf/text-transform.nvim", - branch = "develop", - dependencies = { - -- for Telescope popup - 'nvim-telescope/telescope.nvim', - 'nvim-lua/plenary.nvim', - }, + -- dev version + -- branch = "develop", + -- Optional - for Telescope popup + dependencies = { 'nvim-telescope/telescope.nvim', 'nvim-lua/plenary.nvim' } }) ``` -
- -[wbthomason/packer.nvim](https://github.com/wbthomason/packer.nvim) - - +### [Packer](https://github.com/wbthomason/packer.nvim) ```lua --- stable version use { "chenasraf/text-transform.nvim", + -- stable version tag = "stable", - requires = { - -- for Telescope popup - 'nvim-telescope/telescope.nvim', - 'nvim-lua/plenary.nvim', - } -} --- dev version -use { "chenasraf/text-transform.nvim", - branch = "develop", - requires = { - -- for Telescope popup - 'nvim-telescope/telescope.nvim', - 'nvim-lua/plenary.nvim', - } + -- dev version + -- branch = "develop", + -- Optional - for Telescope popup + requires = { 'nvim-telescope/telescope.nvim', 'nvim-lua/plenary.nvim' } } ``` -
- -[junegunn/vim-plug](https://github.com/junegunn/vim-plug) - - +### [Plug](https://github.com/junegunn/vim-plug) ```vim --- Dependencies - for Telescope popup -Plug "nvim-telescope/telescope.nvim" -Plug "nvim-lua/plenary.nvim" - --- stable version -Plug "chenasraf/text-transform.nvim", { - "tag": "stable", -} --- dev version -Plug "chenasraf/text-transform.nvim", { - "branch": "develop", -} +" Dependencies - optional for Telescope popup +Plug 'nvim-telescope/telescope.nvim' +Plug 'nvim-lua/plenary.nvim' + +" stable version +Plug 'chenasraf/text-transform.nvim', { 'tag': 'stable' } +" dev version +Plug 'chenasraf/text-transform.nvim', { 'branch': 'develop' } ``` -
-
+It falls back to `vim.ui.select()` instead, which may or may not still be Telescope behind the +scenes, or something else; depending on your setup. ## 🚀 Getting started @@ -207,7 +144,11 @@ require("text-transform").setup({ --- Sort the replacers in the popup. --- Possible values: 'frequency', 'name' - sort_by = 'frequency', + sort_by = "frequency", + + --- The popup type to show. + --- Possible values: 'telescope', 'select' + popup_type = 'telescope' }) ``` @@ -215,16 +156,18 @@ require("text-transform").setup({ The following commands are available for your use in your own mappings or for reference. -| Command | Description | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `:TtTelescope` \| `:TextTransform` | Pops up a Telescope window with the available converters which will directly act on the selected text. | -| `:TtCamel` | Replaces selection with `camelCase`. | -| `:TtConst` | Replaces selection with `CONST_CASE`. | -| `:TtDot` | Replaces selection with `dot.case`. | -| `:TtKebab` | Replaces selection with `kebab-case`. | -| `:TtPascal` | Replaces selection with `PascalCase`. | -| `:TtSnake` | Replaces selection with `snake_case`. | -| `:TtTitle` | Replaces selection with `Title Case`. | +| Command | Description | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `:TextTransform` | Pop up a either a Telescope window or a selection popup, depending on the `popup_type` config. | +| `:TtTelescope` | Pop up a Telescope window with all the transformers, which will directly act on the selected text or highlighted word. | +| `:TtSelect` | Pop up a selection popup with all the transformers, which will directly act on the selected text or highlighted word. | +| `:TtCamel` | Replace selection/word with `camelCase`. | +| `:TtSnake` | Replace selection/word with `snake_case`. | +| `:TtPascal` | Replace selection/word with `PascalCase`. | +| `:TtConst` | Replace selection/word with `CONST_CASE`. | +| `:TtDot` | Replace selection/word with `dot.case`. | +| `:TtKebab` | Replace selection/word with `kebab-case`. | +| `:TtTitle` | Replace selection/word with `Title Case`. | ## ⌨️⌨️ Keymaps @@ -254,17 +197,16 @@ You can also create custom mappings to specific case conversions or to the Teles ```lua -- Trigger telescope popup -local telescope = require('text-transform.telescope') -vim.keymap.set("n", "~~", telescope.popup, { silent = true }) +vim.keymap.set("n", "~~", ":TtTelescope", { silent = true, desc = "Transform Text" }) -- Trigger case converters directly -vim.keymap.set({ "n", "v" }, "Ccc", ":TtCamel", { silent = true }) -vim.keymap.set({ "n", "v" }, "Cco", ":TtConst", { silent = true }) -vim.keymap.set({ "n", "v" }, "Cdo", ":TtDot", { silent = true }) -vim.keymap.set({ "n", "v" }, "Cke", ":TtKebab", { silent = true }) -vim.keymap.set({ "n", "v" }, "Cpa", ":TtPascal", { silent = true }) -vim.keymap.set({ "n", "v" }, "Csn", ":TtSnake", { silent = true }) -vim.keymap.set({ "n", "v" }, "Ctt", ":TtTitle", { silent = true }) +vim.keymap.set({ "n", "v" }, "Ccc", ":TtCamel", { silent = true, desc = "To camelCase" }) +vim.keymap.set({ "n", "v" }, "Csn", ":TtSnake", { silent = true, desc = "To snake_case" }) +vim.keymap.set({ "n", "v" }, "Cpa", ":TtPascal", { silent = true, desc = "To PascalCase" }) +vim.keymap.set({ "n", "v" }, "Cco", ":TtConst", { silent = true, desc = "To CONST_CASE" }) +vim.keymap.set({ "n", "v" }, "Cdo", ":TtDot", { silent = true, desc = "To dot.case" }) +vim.keymap.set({ "n", "v" }, "Cke", ":TtKebab", { silent = true, desc = "To kebab-case" }) +vim.keymap.set({ "n", "v" }, "Ctt", ":TtTitle", { silent = true, desc = "To Title Case" }) ``` ## 💁🏻 Contributing diff --git a/doc/tags b/doc/tags index d4cf483..f74aace 100644 --- a/doc/tags +++ b/doc/tags @@ -1,13 +1,9 @@ -TextTransform.enable() text-transform.txt /*TextTransform.enable()* +TextTransform.config text-transform.txt /*TextTransform.config* TextTransform.get_visual_selection_details() text-transform.txt /*TextTransform.get_visual_selection_details()* TextTransform.init_commands() text-transform.txt /*TextTransform.init_commands()* -TextTransform.options text-transform.txt /*TextTransform.options* -TextTransform.popup() text-transform.txt /*TextTransform.popup()* TextTransform.replace_columns() text-transform.txt /*TextTransform.replace_columns()* TextTransform.replace_selection() text-transform.txt /*TextTransform.replace_selection()* TextTransform.replace_word() text-transform.txt /*TextTransform.replace_word()* -TextTransform.restore_positions() text-transform.txt /*TextTransform.restore_positions()* -TextTransform.save_positions() text-transform.txt /*TextTransform.save_positions()* TextTransform.setup() text-transform.txt /*TextTransform.setup()* TextTransform.to_camel_case() text-transform.txt /*TextTransform.to_camel_case()* TextTransform.to_const_case() text-transform.txt /*TextTransform.to_const_case()* @@ -17,8 +13,13 @@ TextTransform.to_pascal_case() text-transform.txt /*TextTransform.to_pascal_case TextTransform.to_snake_case() text-transform.txt /*TextTransform.to_snake_case()* TextTransform.to_title_case() text-transform.txt /*TextTransform.to_title_case()* TextTransform.to_words() text-transform.txt /*TextTransform.to_words()* -TextTransform.toggle() text-transform.txt /*TextTransform.toggle()* TextTransform.transform_words() text-transform.txt /*TextTransform.transform_words()* find_word_boundaries() text-transform.txt /*find_word_boundaries()* +init() text-transform.txt /*init()* +state.enable() text-transform.txt /*state.enable()* +state.restore_positions() text-transform.txt /*state.restore_positions()* +state.save_positions() text-transform.txt /*state.save_positions()* +state.toggle() text-transform.txt /*state.toggle()* +telescope.telescope_popup() text-transform.txt /*telescope.telescope_popup()* utils.dump() text-transform.txt /*utils.dump()* utils.merge() text-transform.txt /*utils.merge()* diff --git a/doc/text-transform.txt b/doc/text-transform.txt index fff52dd..dd3d19c 100644 --- a/doc/text-transform.txt +++ b/doc/text-transform.txt @@ -8,13 +8,13 @@ Initializes user commands ============================================================================== ------------------------------------------------------------------------------ - *TextTransform.options* - `TextTransform.options` + *TextTransform.config* + `TextTransform.config` Your plugin configuration with its default values. Default values: > - TextTransform.options = { + TextTransform.config = { --- Prints information about internals of the plugin. Very verbose, only useful for debugging. debug = false, --- Keymap configurations @@ -46,26 +46,19 @@ Default values: --- Sort the replacers in the popup. --- Possible values: 'frequency', 'name' sort_by = "frequency", - } - local function init() - local o = TextTransform.options - D.log("config", "Initializing TextTransform with %s", utils.dump(o)) - commands.init_commands() - - if o.keymap.telescope_popup then - local keys = o.keymap.telescope_popup - if keys.n then - vim.keymap.set("n", keys.n, telescope.popup, { silent = true }) - end - if keys.v then - vim.keymap.set("v", keys.v, telescope.popup, { silent = true }) - end - end - end + --- The popup type to show. + --- Possible values: 'telescope', 'select' + popup_type = "telescope", + } < +------------------------------------------------------------------------------ + *init()* + `init`() +@internal + ------------------------------------------------------------------------------ *TextTransform.setup()* `TextTransform.setup`({options}) @@ -124,34 +117,34 @@ the full information around the selection logic. ============================================================================== ------------------------------------------------------------------------------ - *TextTransform.toggle()* - `TextTransform.toggle`() + *state.toggle()* + `state.toggle`() Toggle the plugin by calling the `enable`/`disable` methods respectively. @private ------------------------------------------------------------------------------ - *TextTransform.enable()* - `TextTransform.enable`() + *state.enable()* + `state.enable`() Enables the plugin @private ------------------------------------------------------------------------------ - *TextTransform.save_positions()* - `TextTransform.save_positions`() + *state.save_positions()* + `state.save_positions`() Save the current cursor position, mode, and visual selection ranges @private ------------------------------------------------------------------------------ - *TextTransform.restore_positions()* - `TextTransform.restore_positions`({state}) + *state.restore_positions()* + `state.restore_positions`({new_state}) Restore the cursor position, mode, and visual selection ranges saved using `save_position()`, or a given modified state, if passed as the first argument ============================================================================== ------------------------------------------------------------------------------ - *TextTransform.popup()* - `TextTransform.popup`() + *telescope.telescope_popup()* + `telescope.telescope_popup`() Pops up a telescope menu, containing the available case transformers. When a transformer is selected, the cursor position/range/columns will be used to replace the words around the cursor or inside the selection. diff --git a/lua/text-transform/commands.lua b/lua/text-transform/commands.lua index bb9c8e9..ec188eb 100644 --- a/lua/text-transform/commands.lua +++ b/lua/text-transform/commands.lua @@ -1,6 +1,9 @@ +-- local D = require("text-transform.util.debug") +local util = require("text-transform.util") local state = require("text-transform.state") local replacers = require("text-transform.replacers") -local telescope = require("text-transform.telescope") +local popup = require("text-transform.popup") +local common = require("text-transform.popup_common") local TextTransform = {} --- Initializes user commands @@ -16,15 +19,37 @@ function TextTransform.init_commands() TtTitle = "title_case", } + local cmdopts = { range = true, force = true } + local opts = function(desc) + return util.merge(cmdopts, { desc = desc }) + end + for cmd, transformer_name in pairs(map) do + local item + for _, t in ipairs(common.items) do + if t.value == transformer_name then + item = t.label + break + end + end vim.api.nvim_create_user_command(cmd, function() state.save_positions() replacers.replace_selection(transformer_name) - end, {}) + end, opts("Change to " .. item)) end - vim.api.nvim_create_user_command("TtTelescope", telescope.popup, {}) - vim.api.nvim_create_user_command("TextTransform", telescope.popup, {}) + -- specific popups + vim.api.nvim_create_user_command("TtTelescope", function() + local telescope = require("text-transform.telescope") + telescope.telescope_popup() + end, opts("Change Case with Telescope")) + vim.api.nvim_create_user_command("TtSelect", function() + local select = require("text-transform.select") + select.select_popup() + end, opts("Change Case with Select")) + + -- auto popup by config + vim.api.nvim_create_user_command("TextTransform", popup.show_popup, opts("Change Case")) end return TextTransform diff --git a/lua/text-transform/config.lua b/lua/text-transform/config.lua index 97487a4..3cfc1a5 100644 --- a/lua/text-transform/config.lua +++ b/lua/text-transform/config.lua @@ -1,4 +1,4 @@ -local telescope = require("text-transform.telescope") +local popup = require("text-transform.popup") local commands = require("text-transform.commands") local D = require("text-transform.util.debug") local utils = require("text-transform.util") @@ -8,7 +8,7 @@ local TextTransform = {} --- --- Default values: ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) -TextTransform.options = { +TextTransform.config = { --- Prints information about internals of the plugin. Very verbose, only useful for debugging. debug = false, --- Keymap configurations @@ -40,20 +40,25 @@ TextTransform.options = { --- Sort the replacers in the popup. --- Possible values: 'frequency', 'name' sort_by = "frequency", + + --- The popup type to show. + --- Possible values: 'telescope', 'select' + popup_type = "telescope", } +--- @internal local function init() - local o = TextTransform.options + local o = TextTransform.config D.log("config", "Initializing TextTransform with %s", utils.dump(o)) commands.init_commands() if o.keymap.telescope_popup then local keys = o.keymap.telescope_popup if keys.n then - vim.keymap.set("n", keys.n, telescope.popup, { silent = true }) + vim.keymap.set("n", keys.n, popup.show_popup, { silent = true, desc = "Change Case" }) end if keys.v then - vim.keymap.set("v", keys.v, telescope.popup, { silent = true }) + vim.keymap.set("v", keys.v, popup.show_popup, { silent = true, desc = "Change Case" }) end end end @@ -66,7 +71,7 @@ end function TextTransform.setup(options) options = options or {} - TextTransform.options = utils.merge(TextTransform.options, options) + TextTransform.config = utils.merge(TextTransform.config, options) if vim.api.nvim_get_vvar("vim_did_enter") == 0 then vim.defer_fn(function() @@ -76,7 +81,7 @@ function TextTransform.setup(options) init() end - return TextTransform.options + return TextTransform.config end return TextTransform diff --git a/lua/text-transform/main.lua b/lua/text-transform/main.lua index 3efc1c2..034c183 100644 --- a/lua/text-transform/main.lua +++ b/lua/text-transform/main.lua @@ -2,7 +2,7 @@ local utils = require("text-transform.util") local tt = require("text-transform.transformers") local replacers = require("text-transform.replacers") local state = require("text-transform.state") -local telescope = require("text-transform.telescope") +local popup = require("text-transform.popup") local TextTransform = {} @@ -13,6 +13,6 @@ end merge(tt) merge(replacers) merge(state) -merge(telescope) +merge(popup) return TextTransform diff --git a/lua/text-transform/popup.lua b/lua/text-transform/popup.lua new file mode 100644 index 0000000..b23b025 --- /dev/null +++ b/lua/text-transform/popup.lua @@ -0,0 +1,14 @@ +local popup = {} + +function popup.show_popup() + local config = _G.TextTransform.config + if config.popup_type == "telescope" then + local telescope = require("text-transform.telescope") + telescope.telescope_popup() + else + local select = require("text-transform.select") + select.select_popup() + end +end + +return popup diff --git a/lua/text-transform/popup_common.lua b/lua/text-transform/popup_common.lua new file mode 100644 index 0000000..8693cde --- /dev/null +++ b/lua/text-transform/popup_common.lua @@ -0,0 +1,66 @@ +local D = require("text-transform.util.debug") +local state = require("text-transform.state") +local replacers = require("text-transform.replacers") + +local popup_common = {} + +popup_common.items = { + { label = "camelCase", value = "camel_case" }, + { label = "snake_case", value = "snake_case" }, + { label = "PascalCase", value = "pascal_case" }, + { label = "kebab-case", value = "kebab_case" }, + { label = "dot.case", value = "dot_case" }, + { label = "Title Case", value = "title_case" }, + { label = "CONST_CASE", value = "const_case" }, +} + +popup_common.default_frequency = { + camel_case = 1, + snake_case = 1, + pascal_case = 1, + kebab_case = 1, + dot_case = 1, + title_case = 1, + const_case = 1, +} + +local frequency_file = vim.fn.stdpath("config") .. "/text-transform-frequency.json" +local frequency + +function popup_common.load_frequency() + if frequency then + return frequency + end + if vim.fn.filereadable(frequency_file) == 0 then + frequency = popup_common.default_frequency + vim.fn.writefile({ vim.fn.json_encode(frequency) }, frequency_file) + else + frequency = vim.fn.json_decode(vim.fn.readfile(frequency_file)) + end + D.log("telescope", "frequency loaded: %s", vim.inspect(frequency)) + return frequency +end + +function popup_common.inc_frequency(name) + frequency[name] = (frequency[name] or 0) + 1 + D.log("telescope", "new frequency: %s %d", name, frequency[name]) + vim.fn.writefile({ vim.fn.json_encode(frequency) }, frequency_file) +end + +function popup_common.entry_maker(entry) + return { + value = entry.value, + display = entry.label, + ordinal = entry.label, + frequency = frequency[entry.value] or 1, + } +end + +function popup_common.select(selection) + vim.schedule(function() + replacers.replace_selection(selection.value) + state.restore_positions() + end) +end + +return popup_common diff --git a/lua/text-transform/replacers.lua b/lua/text-transform/replacers.lua index 14d30a6..a9d8578 100644 --- a/lua/text-transform/replacers.lua +++ b/lua/text-transform/replacers.lua @@ -19,14 +19,18 @@ local function find_word_boundaries(line, start_col) local word_end_col = vim.fn.match(line_text:sub(word_start_col), non_word_pat) + word_start_col - 1 - D.log("replacers", "Found word boundaries: %s", vim.inspect({ word_start_col, word_end_col })) - D.log("replacers", "Word text: %s", line_text:sub(word_start_col, word_end_col)) - D.log("replacers", "Line text: %s", line_text) + D.log( + "find_word_boundaries", + "Found word boundaries: %s", + vim.inspect({ word_start_col, word_end_col }) + ) + D.log("find_word_boundaries", "Word text: %s", line_text:sub(word_start_col, word_end_col)) + D.log("find_word_boundaries", "Line text: %s", line_text) return word_start_col, word_end_col end function TextTransform.replace_range(start_line, start_col, end_line, end_col, transform_name) - D.log("replacers", "Replacing range with %s", transform_name) + D.log("replace_range", "Replacing range with %s", transform_name) local transform = t["to_" .. transform_name] local lines = vim.fn.getline(start_line, end_line) --- @type any local transformed = {} @@ -58,7 +62,7 @@ end --- @param transform_name string The transformer name --- @param position table|nil A table containing the position of the word to replace function TextTransform.replace_word(transform_name, position) - D.log("replacers", "Replacing word with %s", transform_name) + D.log("replace_word", "Replacing word with %s", transform_name) local word, line, col, start_col, end_col if not position then word = vim.fn.expand("") @@ -67,11 +71,11 @@ function TextTransform.replace_word(transform_name, position) start_col, end_col = find_word_boundaries(line, col) word = vim.fn.getline(line):sub(start_col, end_col) end - D.log("replacers", "Found word %s", word) - D.log("replacers", "Using transformer %s", transform_name) + D.log("replace_word", "Found word %s", word) + D.log("replace_word", "Using transformer %s", transform_name) local transformer = t["to_" .. transform_name] local transformed = transformer(word) - D.log("replacers", "New value %s", transformed) + D.log("replace_word", "New value %s", transformed) if not position then vim.cmd("normal ciw" .. transformed) else @@ -83,7 +87,7 @@ end --- Assumes that the each selection is 1 character and operates on the whole word under each cursor. function TextTransform.replace_columns(transform_name) local selections = TextTransform.get_visual_selection_details() - D.log("replacers", "Replacing columns with %s", transform_name) + D.log("replace_columns", "Replacing columns with %s", transform_name) for _, sel in ipairs(selections) do TextTransform.replace_word(transform_name, { 0, sel.start_line, sel.start_col, 0 }) end @@ -95,20 +99,22 @@ end --- --- @param transform_name string The transformer name function TextTransform.replace_selection(transform_name) - D.log("replacers", "Replacing selection with %s", transform_name) + D.log("replace_selection", "Replacing selection with %s", transform_name) -- determine if cursor is a 1-width column across multiple lines or a normal selection -- local start_line, start_col, end_line, end_col = unpack(vim.fn.getpos("'<")) local selections = TextTransform.get_visual_selection_details() - D.log("replacers", "Selections: %s", utils.dump(selections)) + D.log("replace_selection", "Selections: %s", utils.dump(selections)) local is_multiline = #selections > 1 local is_column = is_multiline and selections[1].start_col == selections[#selections].end_col local is_single_cursor = not is_multiline and not is_column + and selections + and selections[1] and selections[1].start_col == selections[1].end_col D.log( - "replacers", + "replace_selection", "is_multiline: %s, is_column: %s, is_word: %s", is_multiline, is_column, @@ -139,19 +145,31 @@ end --- the full information around the selection logic. function TextTransform.get_visual_selection_details() if not state.positions then - D.log("replacers", "No positions saved") + D.log("get_visual_selection_details", "No positions saved") return {} end D.log( - "replacers", + "get_visual_selection_details", "Getting visual selection details - mode: %s, is_visual: %s, is_block: %s", state.positions.mode, utils.is_visual_mode(), utils.is_block_visual_mode() ) + + -- Get the start and end positions of the selection + local start_pos = state.positions.visual_start + local end_pos = state.positions.visual_end + local start_line, start_col = start_pos[2], start_pos[3] + local end_line, end_col = end_pos[2], end_pos[3] + -- Check if currently in visual mode; if not, return the cursor position - if not utils.is_visual_mode() and not utils.is_block_visual_mode() then + if + not utils.is_visual_mode() + and not utils.is_block_visual_mode() + and not state.has_range(start_pos, end_pos) + then local pos = state.positions.pos + D.log("get_visual_selection_details", "Returning single cursor position") return { { start_line = pos[2], @@ -162,12 +180,6 @@ function TextTransform.get_visual_selection_details() } end - -- Get the start and end positions of the selection - local start_pos = state.positions.visual_start - local end_pos = state.positions.visual_end - local start_line, start_col = start_pos[2], start_pos[3] - local end_line, end_col = end_pos[2], end_pos[3] - -- Swap if selection is made upwards or backwards if start_line > end_line or (start_line == end_line and start_col > end_col) then start_line, end_line = end_line, start_line @@ -175,7 +187,7 @@ function TextTransform.get_visual_selection_details() end -- If it's block visual mode, return table for each row - if utils.is_block_visual_mode() then + if utils.is_block_visual_mode() or state.has_range(start_pos, end_pos) then local block_selection = {} for line = start_line, end_line do if start_col == end_col then @@ -189,9 +201,15 @@ function TextTransform.get_visual_selection_details() end_col = start_col, }) end + D.log( + "get_visual_selection_details", + "Returning block selection: %s", + utils.dump(block_selection) + ) return block_selection else -- Normal visual mode, return single table entry + D.log("get_visual_selection_details", "Returning normal selection") return { { start_line = start_line, diff --git a/lua/text-transform/select.lua b/lua/text-transform/select.lua new file mode 100644 index 0000000..56c813b --- /dev/null +++ b/lua/text-transform/select.lua @@ -0,0 +1,24 @@ +local common = require("text-transform.popup_common") +local state = require("text-transform.state") + +local select = {} + +function select.select_popup() + common.load_frequency() + state.save_positions() + + vim.ui.select(common.items, { + prompt = "Change Case", + format_item = function(item) + return item.label + end, + }, function(choice) + if not choice then + return + end + local item = common.entry_maker(choice) + common.select(item) + end) +end + +return select diff --git a/lua/text-transform/state.lua b/lua/text-transform/state.lua index 5e6e5a2..dfeb3b7 100644 --- a/lua/text-transform/state.lua +++ b/lua/text-transform/state.lua @@ -3,12 +3,12 @@ local D = require("text-transform.util.debug") local function ensure_config() -- when the config is not set to the global object, we set it if _G.TextTransform.config == nil then - _G.TextTransform.config = require("text-transform.config").options + _G.TextTransform.config = require("text-transform.config").config end end -- methods -local TextTransform = { +local state = { -- Boolean determining if the plugin is enabled or not. enabled = false, -- A table containing cursor position and visual selection details, @@ -18,40 +18,40 @@ local TextTransform = { --- Toggle the plugin by calling the `enable`/`disable` methods respectively. --- @private -function TextTransform.toggle() - if TextTransform.enabled then - return TextTransform.disable() +function state.toggle() + if state.enabled then + return state.disable() end - return TextTransform.enable() + return state.enable() end --- Enables the plugin --- @private -function TextTransform.enable() +function state.enable() ensure_config() - if TextTransform.enabled then - return TextTransform + if state.enabled then + return state end - TextTransform.enabled = true - return TextTransform + state.enabled = true + return state end ---Disables the plugin and reset the internal state. ---@private -function TextTransform.disable() +function state.disable() ensure_config() - if not TextTransform.enabled then - return TextTransform + if not state.enabled then + return state end -- reset the state - TextTransform.enabled = false - TextTransform.positions = nil - return TextTransform + state.enabled = false + state.positions = nil + return state end local function get_mode_type(mode) @@ -64,42 +64,58 @@ local function get_mode_type(mode) return mode_map[mode] or "normal" end +function state.has_range(visual_start, visual_end) + return visual_start and visual_end and visual_start[2] ~= visual_end[2] +end + +local function capture_part(start_sel, end_sel, return_type) + local l, sel + if return_type == "start" then + l = math.min(start_sel[2], end_sel[2]) + sel = start_sel + else + l = math.max(start_sel[2], end_sel[2]) + sel = end_sel + end + return { sel[1], l, sel[3], sel[4] } +end + --- Save the current cursor position, mode, and visual selection ranges --- @private -function TextTransform.save_positions() +function state.save_positions() local buf = vim.api.nvim_get_current_buf() local mode_info = vim.api.nvim_get_mode() local mode = get_mode_type(mode_info.mode) local pos = vim.fn.getcurpos() - -- leave mode + -- leave mode, required to get the positions - they only register on mode leave + -- in case of visual mode local esc = vim.api.nvim_replace_termcodes("", true, false, true) vim.api.nvim_feedkeys(esc, "x", true) local visual_start = vim.fn.getpos("'<") local visual_end = vim.fn.getpos("'>") - D.log("popup_menu", "Saved mode %s, cursor %s", mode, vim.inspect(pos)) + D.log("save_positions", "Saved mode %s, cursor %s", mode, vim.inspect(pos)) if mode == "visual" or mode == "line" or mode == "block" then - if mode == "block" then -- for block visual mode - D.log("popup_menu", "Visual mode is block, %s", vim.inspect({ visual_start, visual_end })) + if state.has_range(visual_start, visual_end) then -- for ranges + D.log( + "save_positions", + "Visual range, mode is %s, %s", + mode, + vim.inspect({ visual_start, visual_end }) + ) -- Adjust the positions to correctly capture the entire block - visual_start = { - visual_start[1], - math.min(visual_start[2], visual_end[2]), - visual_start[3], - visual_start[4], - } - visual_end = - { visual_end[1], math.max(visual_start[2], visual_end[2]), visual_end[3], visual_end[4] } + visual_start = capture_part(visual_start, visual_end, "start") + visual_end = capture_part(visual_start, visual_end, "end") end D.log( - "popup_menu", + "state", "Saved visual mode %s, cursor %s", mode, vim.inspect({ visual_start, visual_end }) ) end - local state = { + local positions = { buf = buf, mode = mode, pos = pos, @@ -107,32 +123,37 @@ function TextTransform.save_positions() visual_end = visual_end, } - D.log("popup_menu", "State: %s", vim.inspect(state)) - TextTransform.positions = state - return state + D.log("save_positions", "State: %s", vim.inspect(positions)) + state.positions = positions + return positions end --- Restore the cursor position, mode, and visual selection ranges saved using `save_position()`, --- or a given modified state, if passed as the first argument -function TextTransform.restore_positions(state) - state = state or TextTransform.positions - vim.api.nvim_set_current_buf(state.buf) - vim.fn.setpos(".", state.pos) - D.log("popup_menu", "Restored mode %s, cursor %s", state.mode, vim.inspect(state.pos)) +function state.restore_positions(new_state) + new_state = new_state or new_state.positions + vim.api.nvim_set_current_buf(new_state.buf) + vim.fn.setpos(".", new_state.pos) + D.log( + "restore_positions", + "Restored mode %s, cursor %s", + new_state.mode, + vim.inspect(new_state.pos) + ) -- Attempt to restore visual mode accurately if - (state.mode == "visual" or state.mode == "block") - and state.visual_start - and state.visual_end + (new_state.mode == "visual" or new_state.mode == "block") + and new_state.visual_start + and new_state.visual_end then - vim.fn.setpos("'<", state.visual_start) - vim.fn.setpos("'>", state.visual_end) + vim.fn.setpos("'<", new_state.visual_start) + vim.fn.setpos("'>", new_state.visual_end) local command = "normal! gv" vim.cmd(command) - D.log("popup_menu", [[Restored visual mode %s using "%s"]], state.mode, command) + D.log("restore_positions", [[Restored visual mode %s using "%s"]], new_state.mode, command) end - TextTransform.positions = nil + new_state.positions = nil end -return TextTransform +return state diff --git a/lua/text-transform/telescope.lua b/lua/text-transform/telescope.lua index 31963bf..503c8a9 100644 --- a/lua/text-transform/telescope.lua +++ b/lua/text-transform/telescope.lua @@ -1,6 +1,6 @@ +local common = require("text-transform.popup_common") local D = require("text-transform.util.debug") local state = require("text-transform.state") -local replacers = require("text-transform.replacers") local pickers = require("telescope.pickers") local finders = require("telescope.finders") @@ -9,116 +9,58 @@ local actions = require("telescope.actions") local action_state = require("telescope.actions.state") local dropdown = require("telescope.themes").get_dropdown({}) local Sorter = require("telescope.sorters").Sorter -local generic_sorter = telescope_conf.generic_sorter() - -local TextTransform = {} - -local items = { - { label = "camelCase", value = "camel_case" }, - { label = "snake_case", value = "snake_case" }, - { label = "PascalCase", value = "pascal_case" }, - { label = "kebab-case", value = "kebab_case" }, - { label = "dot.case", value = "dot_case" }, - { label = "Title Case", value = "title_case" }, - { label = "CONST_CASE", value = "const_case" }, -} - -local default_frequency = { - camel_case = 1, - snake_case = 1, - pascal_case = 1, - kebab_case = 1, - dot_case = 1, - title_case = 1, - const_case = 1, -} - -local frequency_file = vim.fn.stdpath("config") .. "/text-transform-frequency.json" -local frequency - -local function load_frequency() - if frequency then - return frequency - end - if vim.fn.filereadable(frequency_file) == 0 then - frequency = default_frequency - vim.fn.writefile({ vim.fn.json_encode(frequency) }, frequency_file) - else - frequency = vim.fn.json_decode(vim.fn.readfile(frequency_file)) - end - D.log("telescope", "frequency loaded: %s", vim.inspect(frequency)) - return frequency -end - -local function inc_frequency(name) - frequency[name] = (frequency[name] or 0) + 1 - D.log("telescope", "new frequency: %s %d", name, frequency[name]) - vim.fn.writefile({ vim.fn.json_encode(frequency) }, frequency_file) -end -local function entry_maker(entry) - return { - value = entry.value, - display = entry.label, - ordinal = entry.label, - frequency = frequency[entry.value] or 1, - } -end +local telescope = {} local frequency_sorter = Sorter:new({ ---@diagnostic disable-next-line: unused-local scoring_function = function(self, prompt, line) + local generic_sorter = telescope_conf.generic_sorter() + generic_sorter:init() local entry - for _, item in ipairs(items) do + for _, item in ipairs(common.items) do if item.label == line then - entry = entry_maker(item) + entry = common.entry_maker(item) break end end - D.log("telescope", "prompt %s line %s", prompt, line) + D.log("frequency_sorter", "entry %s", vim.inspect(entry)) + D.log("frequency_sorter", "prompt %s line %s", prompt, line) -- Basic filtering based on prompt matching, non-matching items score below 0 to exclude them - local basic_score = (generic_sorter:scoring_function(prompt, line) or 0) - D.log("telescope", "%s basic_score: %s", entry.value, basic_score) + local basic_score = (generic_sorter:scoring_function(prompt, line) or 1) + D.log("frequency_sorter", "%s basic_score: %s", entry.value, basic_score) if basic_score < 0 then return basic_score end - -- D.log("telescope", "entry: %s", vim.inspect(entry)) - -- D.log("telescope", "prompt: %s", prompt) + -- D.log("frequency_sorter", "entry: %s", vim.inspect(entry)) + -- D.log("frequency_sorter", "prompt: %s", prompt) -- Calculate score based on frequency, higher frequency should have lower score local freq_score = (entry.frequency or 1) * 10 - D.log("telescope", "freq_score: %s", freq_score) + D.log("frequency_sorter", "freq_score: %s", freq_score) local final_score = 999999999 - freq_score + basic_score - D.log("telescope", "%s final_score: %s", line, final_score) + D.log("frequency_sorter", "%s final_score: %s", line, final_score) -- Combine scores, with frequency having the primary influence if present return final_score end, }) +local generic_sorter = telescope_conf.generic_sorter() local sorter_map = { frequency = frequency_sorter, name = generic_sorter, } ----@diagnostic disable-next-line: unused-local --- for _i, k in pairs(default_ordered_keys) do --- local v = map[k] --- vim.cmd("amenu TransformsWord." .. k .. " :lua TextTransform.replace_word('" .. v .. "')") --- vim.cmd( --- "amenu TransformsSelection." .. k .. " :lua TextTransform.replace_columns('" .. v .. "')" --- ) --- end - --- Pops up a telescope menu, containing the available case transformers. --- When a transformer is selected, the cursor position/range/columns will be used to replace the --- words around the cursor or inside the selection. --- --- The cursor positions/ranges are saved before opening the menu and restored once a selection is --- made. -function TextTransform.popup() +function telescope.telescope_popup() state.save_positions() local filtered = {} @@ -126,11 +68,11 @@ function TextTransform.popup() local sorter = sorter_map[config.sort_by] or generic_sorter if config.sort_by == "frequency" then - load_frequency() + common.load_frequency() end - for _, item in ipairs(items) do - if not config.replacers[item.value] or not config.replacers[item.value].enabled then + for _, item in ipairs(common.items) do + if config.replacers[item.value] and not config.replacers[item.value].enabled then goto continue end table.insert(filtered, item) @@ -140,19 +82,16 @@ function TextTransform.popup() local picker = pickers.new(dropdown, { prompt_title = "Change Case", finder = finders.new_table({ - results = items, - entry_maker = entry_maker, + results = common.items, + entry_maker = common.entry_maker, }), sorter = sorter, attach_mappings = function(prompt_bufnr) actions.select_default:replace(function() local selection = action_state.get_selected_entry() - inc_frequency(selection.value) + common.inc_frequency(selection.value) actions.close(prompt_bufnr) - vim.schedule(function() - replacers.replace_selection(selection.value) - state.restore_positions() - end) + common.select(selection) end) return true end, @@ -162,4 +101,4 @@ function TextTransform.popup() end) end -return TextTransform +return telescope diff --git a/lua/text-transform/transformers.lua b/lua/text-transform/transformers.lua index 4c80b6f..87991cc 100644 --- a/lua/text-transform/transformers.lua +++ b/lua/text-transform/transformers.lua @@ -47,12 +47,12 @@ function TextTransform.to_words(string) -- Append current character to the current word word = word .. char end - D.log("transformers", "i %d char %s word %s words %s", i, char, word, utils.dump(words)) + -- D.log("to_words", "i %d char %s word %s words %s", i, char, word, utils.dump(words)) end if word ~= "" then table.insert(words, word:lower()) end - D.log("transformers", "words %s", vim.inspect(words)) + D.log("to_words", "words %s", vim.inspect(words)) return words end @@ -75,7 +75,7 @@ function TextTransform.transform_words(words, with_word_cb, separator) new_word = separator .. new_word end out = out .. new_word - D.log("transformers", "word %s (%d) new_word %s out %s", word, i, new_word, out) + D.log("transform_words", "word %s (%d) new_word %s out %s", word, i, new_word, out) end return out end diff --git a/lua/text-transform/util/debug.lua b/lua/text-transform/util/debug.lua index f36d5ac..70ec271 100644 --- a/lua/text-transform/util/debug.lua +++ b/lua/text-transform/util/debug.lua @@ -26,10 +26,10 @@ function D.log(scope, str, ...) print( string.format( - "[text-transform:%s %s in %s] > %s", + "%s [text-transform:%s in %s] > %s", os.date("%H:%M:%S"), - line, scope, + line, string.format(str, ...) ) ) diff --git a/lua/text-transform/util/init.lua b/lua/text-transform/util/init.lua index 0c80a18..4fe4d5d 100644 --- a/lua/text-transform/util/init.lua +++ b/lua/text-transform/util/init.lua @@ -30,4 +30,8 @@ function utils.is_visual_mode() -- return vim.fn.mode() == 'v' end +function utils.has_range(visual_start, visual_end) + return visual_start and visual_end and visual_start[2] ~= visual_end[2] +end + return utils diff --git a/tests/helpers.lua b/tests/helpers.lua index 1582bdd..89742f9 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -162,4 +162,9 @@ Helpers.new_child_neovim = function() return child end +function Helpers.init_plugin(child, config) + config = config or "" + child.lua([[require('text-transform').setup(]] .. config .. [[)]]) +end + return Helpers diff --git a/tests/test_config.lua b/tests/test_config.lua index 7997a61..0a10a3c 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -55,14 +55,17 @@ T["setup()"]["sets exposed methods and default options value"] = function() end T["setup()"]["overrides default values"] = function() - child.lua([[require('text-transform').setup({ + helpers.init_plugin( + child, + [[{ -- write all the options with a value different than the default ones debug = true, keymap = { ["v"] = "c", ["n"] = "c", }, - })]]) + }]] + ) -- assert the value, and the type eq_type_config(child, "debug", "boolean") diff --git a/tests/test_popups.lua b/tests/test_popups.lua new file mode 100644 index 0000000..5d5d0c2 --- /dev/null +++ b/tests/test_popups.lua @@ -0,0 +1,63 @@ +local helpers = dofile("tests/helpers.lua") +local MiniTest = require("mini.test") + +-- See https://github.com/echasnovski/mini.nvim/blob/main/lua/mini/test.lua for more documentation + +local child = helpers.new_child_neovim() +local eq_global, eq_config, eq_state = + helpers.expect.global_equality, helpers.expect.config_equality, helpers.expect.state_equality +local eq_type_global, eq_type_config, eq_type_state = + helpers.expect.global_type_equality, + helpers.expect.config_type_equality, + helpers.expect.state_type_equality + +local T = MiniTest.new_set({ + hooks = { + -- This will be executed before every (even nested) case + pre_case = function() + -- Restart child process with custom 'init.lua' script + child.restart({ "-u", "scripts/minimal_init.lua" }) + end, + -- This will be executed one after all tests from this set are finished + post_once = child.stop, + }, +}) + +-- Tests related to the `setup` method. +T["popups"] = MiniTest.new_set() + +T["popups"]["exposes show_popup"] = function() + helpers.init_plugin(child) + + eq_type_global(child, "_G.TextTransform", "table") + + eq_type_global(child, "_G.TextTransform.show_popup", "function") + + eq_type_global(child, "_G.TextTransform.telescope_popup", "nil") +end + +T["popups"]["telescope exposes telescope_popup"] = function() + helpers.init_plugin(child) + + eq_type_global(child, "_G.TextTransform", "table") + + eq_type_global(child, "_G.TextTransform.telescope_popup", "nil") + + child.lua([[Telescope = require('text-transform.telescope')]]) + + eq_type_global(child, "Telescope.telescope_popup", "function") +end + +T["popups"]["select exposes select_popup"] = function() + helpers.init_plugin(child) + + eq_type_global(child, "_G.TextTransform", "table") + + eq_type_global(child, "_G.TextTransform.select_popup", "nil") + + child.lua([[Select = require('text-transform.select')]]) + + eq_type_global(child, "Select.select_popup", "function") +end + +return T diff --git a/tests/test_to_words.lua b/tests/test_to_words.lua index f748151..33a7cd4 100644 --- a/tests/test_to_words.lua +++ b/tests/test_to_words.lua @@ -24,7 +24,7 @@ local T = MiniTest.new_set({ }) local function test_string(child, str) - child.lua([[require('text-transform').setup()]]) + helpers.init_plugin(child) child.lua([[result = require('text-transform').to_words("]] .. str .. [[")]]) end