diff --git a/README.md b/README.md index 1edd7eb..dd6155f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ with my_var or vice versa? This plugin is for you! - Works on column selections in **Visual Block Mode** - Will detect if the block is a single column or multiple columns - If it's a single column, will replace the word under each cursor - - If it's a selection with length, will replace only inside the selection range + - If it's a selection with length, will replace only inside the selection ranges | Transformation | Example Inputs | Output | | -------------- | --------------------------- | -------- | @@ -123,11 +123,13 @@ To get started, [install](#-installation) the plugin via your favorite package m ## ⚙️ Configuration -
-Click to unfold the full list of options with their default values - > **Note**: The options are also available in Neovim by calling `:h TextTransform.options` +The following are the default options when none are configured by the user. + +To merge any new config into the default, you can override only the keys you need, and leave the +rest to use the defaults. + ```lua require("text-transform").setup({ -- Prints useful logs about what event are triggered, and reasons actions are executed. @@ -142,8 +144,6 @@ require("text-transform").setup({ }) ``` -
- ## 📝 Commands Use the following as example, you can mix & match the different replacement functions with the diff --git a/doc/tags b/doc/tags index f97b3c7..90eda90 100644 --- a/doc/tags +++ b/doc/tags @@ -1,14 +1,24 @@ -config.options text-transform.txt /*config.options* -config.setup() text-transform.txt /*config.setup()* -fn.replace_columns() text-transform.txt /*fn.replace_columns()* -transformers.to_camel_case() text-transform.txt /*transformers.to_camel_case()* -transformers.to_const_case() text-transform.txt /*transformers.to_const_case()* -transformers.to_dot_case() text-transform.txt /*transformers.to_dot_case()* -transformers.to_kebab_case() text-transform.txt /*transformers.to_kebab_case()* -transformers.to_pascal_case() text-transform.txt /*transformers.to_pascal_case()* -transformers.to_snake_case() text-transform.txt /*transformers.to_snake_case()* -transformers.to_title_case() text-transform.txt /*transformers.to_title_case()* -transformers.to_words() text-transform.txt /*transformers.to_words()* -transformers.transform_words() text-transform.txt /*transformers.transform_words()* +TextTransform.enable() text-transform.txt /*TextTransform.enable()* +TextTransform.get_visual_selection_details() text-transform.txt /*TextTransform.get_visual_selection_details()* +TextTransform.options text-transform.txt /*TextTransform.options* +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.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()* +TextTransform.to_dot_case() text-transform.txt /*TextTransform.to_dot_case()* +TextTransform.to_kebab_case() text-transform.txt /*TextTransform.to_kebab_case()* +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()* +popup_menu() text-transform.txt /*popup_menu()* 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 b75b0ea..8352d7a 100644 --- a/doc/text-transform.txt +++ b/doc/text-transform.txt @@ -1,12 +1,19 @@ ============================================================================== ------------------------------------------------------------------------------ - *config.options* - `config.options` + *TextTransform.setup()* + `TextTransform.setup`({opts}) +Setup TextTransform options and merge them with user provided ones. + + +============================================================================== +------------------------------------------------------------------------------ + *TextTransform.options* + `TextTransform.options` Your plugin configuration with its default values. Default values: > - config.options = { + TextTransform.options = { -- Prints useful logs about what event are triggered, and reasons actions are executed. debug = false, -- Keymap to trigger the transform. @@ -19,7 +26,7 @@ Default values: } local function init() - local o = config.options + local o = TextTransform.options D.log("config", "Initializing TextTransform with %s", utils.dump(o)) vim.keymap.set("n", o.keymap.n, telescope_popup, { silent = true }) @@ -29,8 +36,8 @@ Default values: < ------------------------------------------------------------------------------ - *config.setup()* - `config.setup`({options}) + *TextTransform.setup()* + `TextTransform.setup`({options}) Define your text-transform setup. Parameters ~ @@ -42,23 +49,97 @@ Usage ~ ============================================================================== ------------------------------------------------------------------------------ - *fn.replace_columns()* - `fn.replace_columns`({transform_name}) + *find_word_boundaries()* + `find_word_boundaries`({line}, {start_col}) +Finds the boundaries of the surrounding word around `start_col` within `line`. +@param line number +@param start_col number +@return number start_col, number end_col + +------------------------------------------------------------------------------ + *TextTransform.replace_word()* + `TextTransform.replace_word`({transform_name}, {position}) +Replace the word under the cursor with the given transform. +If `position` is provided, replace the word under the given position. +Otherwise, attempts to find the word under the cursor. + +@param transform_name string The transformer name +@param position table|nil A table containing the position of the word to replace + +------------------------------------------------------------------------------ + *TextTransform.replace_columns()* + `TextTransform.replace_columns`({transform_name}) Replaces each column in visual block mode selection with the given transform. Assumes that the each selection is 1 character and operates on the whole word under each cursor. +------------------------------------------------------------------------------ + *TextTransform.replace_selection()* + `TextTransform.replace_selection`({transform_name}) +Replaces a selection with the given transform. This function attempts to infer the replacement +type based on the cursor positiono and visual selections, and passes information to relevant +range replacement functions. + +@param transform_name string The transformer name + +------------------------------------------------------------------------------ + *TextTransform.get_visual_selection_details()* + `TextTransform.get_visual_selection_details`() +Takes the saved positions and translates them into individual visual ranges, regardless of how +the original selection was performed. + +This allows to treat all ranges equally and allows to work on each selection without knowing +the full information around the selection logic. + ============================================================================== ------------------------------------------------------------------------------ - *transformers.to_words()* - `transformers.to_words`({string}) + *TextTransform.toggle()* + `TextTransform.toggle`() +Toggle the plugin by calling the `enable`/`disable` methods respectively. +@private + +------------------------------------------------------------------------------ + *TextTransform.enable()* + `TextTransform.enable`() +Enables the plugin +@private + +------------------------------------------------------------------------------ + *TextTransform.save_positions()* + `TextTransform.save_positions`() +Save the current cursor position, mode, and visual selection ranges +@private + +------------------------------------------------------------------------------ + *TextTransform.restore_positions()* + `TextTransform.restore_positions`({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 + + +============================================================================== +------------------------------------------------------------------------------ + *popup_menu()* + `popup_menu`() +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. + + +============================================================================== +------------------------------------------------------------------------------ + *TextTransform.to_words()* + `TextTransform.to_words`({string}) Splits a string into words. @param string string @return table ------------------------------------------------------------------------------ - *transformers.transform_words()* - `transformers.transform_words`({words}, {with_word_cb}, {separator}) + *TextTransform.transform_words()* + `TextTransform.transform_words`({words}, {with_word_cb}, {separator}) Transforms a table of strings into a string using a callback and separator. The callback is called with the word, the index, and the table of words. The separator is added between each word. @@ -69,50 +150,50 @@ The separator is added between each word. @return string ------------------------------------------------------------------------------ - *transformers.to_camel_case()* - `transformers.to_camel_case`({string}) + *TextTransform.to_camel_case()* + `TextTransform.to_camel_case`({string}) Transforms a string into camelCase. @param string string @return string ------------------------------------------------------------------------------ - *transformers.to_snake_case()* - `transformers.to_snake_case`({string}) + *TextTransform.to_snake_case()* + `TextTransform.to_snake_case`({string}) Transfroms a string into snake_case. @param string any @return string ------------------------------------------------------------------------------ - *transformers.to_pascal_case()* - `transformers.to_pascal_case`({string}) + *TextTransform.to_pascal_case()* + `TextTransform.to_pascal_case`({string}) Transforms a string into PascalCase. @param string string @return string ------------------------------------------------------------------------------ - *transformers.to_title_case()* - `transformers.to_title_case`({string}) + *TextTransform.to_title_case()* + `TextTransform.to_title_case`({string}) Transforms a string into Title Case. @param string string @return string ------------------------------------------------------------------------------ - *transformers.to_kebab_case()* - `transformers.to_kebab_case`({string}) + *TextTransform.to_kebab_case()* + `TextTransform.to_kebab_case`({string}) Transforms a string into kebab-case. @param string string @return string ------------------------------------------------------------------------------ - *transformers.to_dot_case()* - `transformers.to_dot_case`({string}) + *TextTransform.to_dot_case()* + `TextTransform.to_dot_case`({string}) Transforms a string into dot.case. @param string string @return string ------------------------------------------------------------------------------ - *transformers.to_const_case()* - `transformers.to_const_case`({string}) + *TextTransform.to_const_case()* + `TextTransform.to_const_case`({string}) Transforms a string into CONSTANT_CASE. @param string string @return string diff --git a/lua/text-transform/config.lua b/lua/text-transform/config.lua index 50e7087..8b250c5 100644 --- a/lua/text-transform/config.lua +++ b/lua/text-transform/config.lua @@ -1,13 +1,13 @@ local telescope_popup = require("text-transform.telescope") local D = require("text-transform.util.debug") local utils = require("text-transform.util") -local config = {} +local TextTransform = {} --- Your plugin configuration with its default values. --- --- Default values: ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) -config.options = { +TextTransform.options = { -- Prints useful logs about what event are triggered, and reasons actions are executed. debug = false, -- Keymap to trigger the transform. @@ -20,7 +20,7 @@ config.options = { } local function init() - local o = config.options + local o = TextTransform.options D.log("config", "Initializing TextTransform with %s", utils.dump(o)) vim.keymap.set("n", o.keymap.n, telescope_popup, { silent = true }) @@ -32,10 +32,10 @@ end ---@param options table Module config table. See |TextTransform.options|. --- ---@usage `require("text-transform").setup()` (add `{}` with your |TextTransform.options| table) -function config.setup(options) +function TextTransform.setup(options) options = options or {} - config.options = utils.merge(config.options, options) + TextTransform.options = utils.merge(TextTransform.options, options) if vim.api.nvim_get_vvar("vim_did_enter") == 0 then vim.defer_fn(function() @@ -45,7 +45,7 @@ function config.setup(options) init() end - return config.options + return TextTransform.options end -return config +return TextTransform diff --git a/lua/text-transform/init.lua b/lua/text-transform/init.lua index eb51304..2d64b36 100644 --- a/lua/text-transform/init.lua +++ b/lua/text-transform/init.lua @@ -1,7 +1,7 @@ local D = require("text-transform.util.debug") local TextTransform = require("text-transform.main") --- setup TextTransform options and merge them with user provided ones. +--- Setup TextTransform options and merge them with user provided ones. function TextTransform.setup(opts) _G.TextTransform.config = require("text-transform.config").setup(opts) end diff --git a/lua/text-transform/replacers.lua b/lua/text-transform/replacers.lua index 08349b5..d018bf2 100644 --- a/lua/text-transform/replacers.lua +++ b/lua/text-transform/replacers.lua @@ -3,13 +3,18 @@ local state = require("text-transform.state") local utils = require("text-transform.util") local t = require("text-transform.transformers") -local fn = {} +local TextTransform = {} +--- Finds the boundaries of the surrounding word around `start_col` within `line`. +--- @param line number +--- @param start_col number +--- @return number start_col, number end_col local function find_word_boundaries(line, start_col) local line_text = vim.fn.getline(line) -- dashes, underscores, and periods are considered part of a word local word_pat = "[A-Za-z0-9_.\\-]" local non_word_pat = "[^A-Za-z0-9_.\\-]" + -- TODO handle searching backwards local word_start_col = vim.fn.match(line_text:sub(start_col), word_pat) + start_col local word_end_col = vim.fn.match(line_text:sub(word_start_col), non_word_pat) + word_start_col @@ -20,7 +25,7 @@ local function find_word_boundaries(line, start_col) return word_start_col, word_end_col end -function fn.replace_range(start_line, start_col, end_line, end_col, transform_name) +function TextTransform.replace_range(start_line, start_col, end_line, end_col, transform_name) D.log("replacers", "Replacing range with %s", transform_name) local transform = t["to_" .. transform_name] local lines = vim.fn.getline(start_line, end_line) --- @type any @@ -46,7 +51,13 @@ function fn.replace_range(start_line, start_col, end_line, end_col, transform_na vim.fn.setline(start_line, transformed) end -function fn.replace_word(transform_name, position) +--- Replace the word under the cursor with the given transform. +--- If `position` is provided, replace the word under the given position. +--- Otherwise, attempts to find the word under the cursor. +--- +--- @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) local word, line, col, start_col, end_col if not position then @@ -64,27 +75,30 @@ function fn.replace_word(transform_name, position) if not position then vim.cmd("normal ciw" .. transformed) else - fn.replace_range(line, start_col, line, end_col, transform_name) + TextTransform.replace_range(line, start_col, line, end_col, transform_name) end end --- Replaces each column in visual block mode selection with the given transform. --- Assumes that the each selection is 1 character and operates on the whole word under each cursor. -function fn.replace_columns(transform_name) - local selections = fn.get_visual_selection_details() +function TextTransform.replace_columns(transform_name) + local selections = TextTransform.get_visual_selection_details() D.log("replacers", "Replacing columns with %s", transform_name) for _, sel in ipairs(selections) do - fn.replace_word(transform_name, { 0, sel.start_line, sel.start_col, 0 }) - -- TODO replace with replace_word? if so, need to find how to get the word under a given position - -- fn.replace_range(sel.start_line, sel.start_col, sel.end_line, sel.end_col, transform_name) + TextTransform.replace_word(transform_name, { 0, sel.start_line, sel.start_col, 0 }) end end -function fn.replace_selection(transform_name) +--- Replaces a selection with the given transform. This function attempts to infer the replacement +--- type based on the cursor positiono and visual selections, and passes information to relevant +--- range replacement functions. +--- +--- @param transform_name string The transformer name +function TextTransform.replace_selection(transform_name) D.log("replacers", "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 = fn.get_visual_selection_details() + local selections = TextTransform.get_visual_selection_details() D.log("replacers", "Selections: %s", utils.dump(selections)) local is_multiline = #selections > 1 @@ -102,17 +116,28 @@ function fn.replace_selection(transform_name) ) if is_single_cursor then - fn.replace_word(transform_name) + TextTransform.replace_word(transform_name) elseif is_column then - fn.replace_columns(transform_name) + TextTransform.replace_columns(transform_name) else for _, sel in pairs(selections) do - fn.replace_range(sel.start_line, sel.start_col, sel.end_line, sel.end_col, transform_name) + TextTransform.replace_range( + sel.start_line, + sel.start_col, + sel.end_line, + sel.end_col, + transform_name + ) end end end -function fn.get_visual_selection_details() +--- Takes the saved positions and translates them into individual visual ranges, regardless of how +--- the original selection was performed. +--- +--- This allows to treat all ranges equally and allows to work on each selection without knowing +--- the full information around the selection logic. +function TextTransform.get_visual_selection_details() D.log( "replacers", "Getting visual selection details - mode: %s, is_visual: %s, is_block: %s", @@ -174,4 +199,4 @@ function fn.get_visual_selection_details() end end -return fn +return TextTransform diff --git a/lua/text-transform/state.lua b/lua/text-transform/state.lua index 065db47..5e6e5a2 100644 --- a/lua/text-transform/state.lua +++ b/lua/text-transform/state.lua @@ -8,48 +8,50 @@ local function ensure_config() end -- methods -local State = { +local TextTransform = { -- Boolean determining if the plugin is enabled or not. enabled = false, + -- A table containing cursor position and visual selection details, + -- saved using `save_position()` and can be restored using `restore_positions()` positions = nil, } ----Toggle the plugin by calling the `enable`/`disable` methods respectively. ----@private -function State.toggle() - if State.enabled then - return State.disable() +--- Toggle the plugin by calling the `enable`/`disable` methods respectively. +--- @private +function TextTransform.toggle() + if TextTransform.enabled then + return TextTransform.disable() end - return State.enable() + return TextTransform.enable() end ----Initializes the plugin. ----@private -function State.enable() +--- Enables the plugin +--- @private +function TextTransform.enable() ensure_config() - if State.enabled then - return State + if TextTransform.enabled then + return TextTransform end - State.enabled = true - return State + TextTransform.enabled = true + return TextTransform end ---Disables the plugin and reset the internal state. ---@private -function State.disable() +function TextTransform.disable() ensure_config() - if not State.enabled then - return State + if not TextTransform.enabled then + return TextTransform end -- reset the state - State.enabled = false - State.positions = nil - return State + TextTransform.enabled = false + TextTransform.positions = nil + return TextTransform end local function get_mode_type(mode) @@ -62,7 +64,9 @@ local function get_mode_type(mode) return mode_map[mode] or "normal" end -function State.save_positions() +--- Save the current cursor position, mode, and visual selection ranges +--- @private +function TextTransform.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) @@ -104,12 +108,14 @@ function State.save_positions() } D.log("popup_menu", "State: %s", vim.inspect(state)) - State.positions = state + TextTransform.positions = state return state end -function State.restore_positions(state) - state = state or State.positions +--- 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)) @@ -126,7 +132,7 @@ function State.restore_positions(state) vim.cmd(command) D.log("popup_menu", [[Restored visual mode %s using "%s"]], state.mode, command) end - State.positions = nil + TextTransform.positions = nil end -return State +return TextTransform diff --git a/lua/text-transform/telescope.lua b/lua/text-transform/telescope.lua index 2145c74..4f5caa4 100644 --- a/lua/text-transform/telescope.lua +++ b/lua/text-transform/telescope.lua @@ -1,6 +1,5 @@ local TextTransform = require("text-transform.main") local state = require("text-transform.state") -local D = require("text-transform.util.debug") local pickers = require("telescope.pickers") local finders = require("telescope.finders") @@ -28,6 +27,12 @@ local items = { -- ) -- 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. local popup_menu = function() state.save_positions() diff --git a/lua/text-transform/transformers.lua b/lua/text-transform/transformers.lua index 5d4235e..4c80b6f 100644 --- a/lua/text-transform/transformers.lua +++ b/lua/text-transform/transformers.lua @@ -1,21 +1,21 @@ local D = require("text-transform.util.debug") local utils = require("text-transform.util") -local transformers = {} +local TextTransform = {} -transformers.WORD_BOUNDRY = "[%_%-%s%.]" +TextTransform.WORD_BOUNDRY = "[%_%-%s%.]" --- Splits a string into words. --- @param string string --- @return table -function transformers.to_words(string) +function TextTransform.to_words(string) local words = {} local word = "" local last_is_upper = false local last_is_digit = false for i = 1, #string do local char = string:sub(i, i) - if char:match(transformers.WORD_BOUNDRY) then + if char:match(TextTransform.WORD_BOUNDRY) then if word ~= "" then table.insert(words, word:lower()) end @@ -64,9 +64,9 @@ end --- @param with_word_cb function (word: string, index: number, words: table) -> string --- @param separator string|nil (optional) --- @return string -function transformers.transform_words(words, with_word_cb, separator) +function TextTransform.transform_words(words, with_word_cb, separator) if type(words) ~= "table" then - words = transformers.to_words(words) + words = TextTransform.to_words(words) end local out = "" for i, word in ipairs(words) do @@ -83,8 +83,8 @@ end --- Transforms a string into camelCase. --- @param string string --- @return string -function transformers.to_camel_case(string) - return transformers.transform_words(string, function(word, i) +function TextTransform.to_camel_case(string) + return TextTransform.transform_words(string, function(word, i) if i == 1 then return word:lower() end @@ -95,8 +95,8 @@ end --- Transfroms a string into snake_case. --- @param string any --- @return string -function transformers.to_snake_case(string) - return transformers.transform_words(string, function(word, i) +function TextTransform.to_snake_case(string) + return TextTransform.transform_words(string, function(word, i) if i == 1 then return word:lower() end @@ -107,16 +107,16 @@ end --- Transforms a string into PascalCase. --- @param string string --- @return string -function transformers.to_pascal_case(string) - local cc = transformers.to_camel_case(string) +function TextTransform.to_pascal_case(string) + local cc = TextTransform.to_camel_case(string) return cc:sub(1, 1):upper() .. cc:sub(2) end --- Transforms a string into Title Case. --- @param string string --- @return string -function transformers.to_title_case(string) - return transformers.transform_words(string, function(word) +function TextTransform.to_title_case(string) + return TextTransform.transform_words(string, function(word) return word:sub(1, 1):upper() .. word:sub(2):lower() end, " ") end @@ -124,8 +124,8 @@ end --- Transforms a string into kebab-case. --- @param string string --- @return string -function transformers.to_kebab_case(string) - return transformers.transform_words(string, function(word) +function TextTransform.to_kebab_case(string) + return TextTransform.transform_words(string, function(word) return word:lower() end, "-") end @@ -133,8 +133,8 @@ end --- Transforms a string into dot.case. --- @param string string --- @return string -function transformers.to_dot_case(string) - return transformers.transform_words(string, function(word) +function TextTransform.to_dot_case(string) + return TextTransform.transform_words(string, function(word) return word:lower() end, ".") end @@ -142,10 +142,10 @@ end --- Transforms a string into CONSTANT_CASE. --- @param string string --- @return string -function transformers.to_const_case(string) - return transformers.transform_words(string, function(word) +function TextTransform.to_const_case(string) + return TextTransform.transform_words(string, function(word) return word:upper() end, "_") end -return transformers +return TextTransform