Created
April 22, 2026 17:42
-
-
Save eduardoarandah/0820b469b2921461558862730578a393 to your computer and use it in GitHub Desktop.
neovim text-case improved
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ---------------------------------------------------- | |
| -- text-case | |
| -- https://github.com/johmsalas/text-case.nvim#setup | |
| ---------------------------------------------------- | |
| vim.pack.add({ | |
| "https://github.com/johmsalas/text-case.nvim", | |
| }) | |
| local textcase = require("textcase") | |
| textcase.setup({ | |
| default_keymappings_enabled = false, | |
| }) | |
| -- Load extension only if telescope is available | |
| local ok, telescope = pcall(require, "telescope") | |
| if ok then | |
| telescope.load_extension("textcase") | |
| end | |
| -- ════════════════════════════════════════════════════════════ | |
| -- Central cases table: single source of truth | |
| -- ════════════════════════════════════════════════════════════ | |
| local cases = { | |
| { method = "to_upper_case", label = "Upper", example = "LOREM IPSUM", word = "u", lsp = "U", op = "u" }, | |
| { method = "to_lower_case", label = "Lower", example = "lorem ipsum", word = "l", lsp = "L", op = "l" }, | |
| { method = "to_snake_case", label = "Snake", example = "lorem_ipsum", word = "s", lsp = "S", op = "s" }, | |
| { method = "to_dash_case", label = "Dash/Kebab", example = "lorem-ipsum", word = "d", lsp = "D", op = "d" }, | |
| { method = "to_title_dash_case", label = "Title-Dash", example = "Lorem-Ipsum", word = "k", lsp = "K", op = "k" }, | |
| { method = "to_constant_case", label = "Constant", example = "LOREM_IPSUM", word = "n", lsp = "N", op = "n" }, | |
| { method = "to_dot_case", label = "Dot", example = "lorem.ipsum", word = "o", lsp = "O", op = "o" }, | |
| { method = "to_comma_case", label = "Comma", example = "lorem,ipsum", word = ",", lsp = "<", op = "," }, | |
| { method = "to_camel_case", label = "Camel", example = "loremIpsum", word = "c", lsp = "C", op = "c" }, | |
| { method = "to_pascal_case", label = "Pascal", example = "LoremIpsum", word = "p", lsp = "P", op = "p" }, | |
| { method = "to_title_case", label = "Title", example = "Lorem Ipsum", word = "t", lsp = "T", op = "t" }, | |
| { method = "to_path_case", label = "Path", example = "lorem/ipsum", word = "f", lsp = "F", op = "f" }, | |
| { method = "to_phrase_case", label = "Phrase", example = "Lorem ipsum", word = "a", lsp = "A", op = "a" }, | |
| } | |
| -- ════════════════════════════════════════════════════════════ | |
| -- Keymap registration | |
| -- ════════════════════════════════════════════════════════════ | |
| -- Prefixes: | |
| -- ga<key> → current_word (changes the word under the cursor) | |
| -- ga<KEY> → lsp_rename (rename via LSP, uppercase letter) | |
| -- ge<key> → operator (awaits motion: gesw, gesiw, etc.) | |
| local map = vim.keymap.set | |
| for _, c in ipairs(cases) do | |
| map("n", "ga" .. c.word, function() | |
| textcase.current_word(c.method) | |
| end, { desc = "TextCase: " .. c.label .. " (word)" }) | |
| map("n", "ga" .. c.lsp, function() | |
| textcase.lsp_rename(c.method) | |
| end, { desc = "TextCase: " .. c.label .. " (LSP rename)" }) | |
| map("n", "ge" .. c.op, function() | |
| textcase.operator(c.method) | |
| end, { desc = "TextCase: " .. c.label .. " (operator)" }) | |
| end | |
| -- ════════════════════════════════════════════════════════════ | |
| -- Helpers for :TextCaseReplace and :TextCaseLspRename | |
| -- ════════════════════════════════════════════════════════════ | |
| local function pick_case(prompt, on_choice) | |
| vim.ui.select(cases, { | |
| prompt = prompt, | |
| format_item = function(c) | |
| return string.format("%-14s %s", c.label, c.example) | |
| end, | |
| }, function(choice) | |
| if choice then | |
| on_choice(choice) | |
| end | |
| end) | |
| end | |
| -- ════════════════════════════════════════════════════════════ | |
| -- :Case → case selector | |
| -- - In normal mode: acts on the word under the cursor | |
| -- - In visual mode: acts on the selection (:'<,'>TextCaseReplace) | |
| -- ════════════════════════════════════════════════════════════ | |
| vim.api.nvim_create_user_command("Case", function(opts) | |
| local is_visual = opts.range > 0 | |
| pick_case("Convert to:", function(choice) | |
| if is_visual then | |
| -- Reapply the last visual selection and call the operator | |
| -- Why gv instead of marks '< / '>? | |
| -- When you execute :TextCaseReplace from visual mode, Neovim exits visual mode before running the command | |
| -- marks '< and '> are set but the mode is already normal. | |
| -- gv reactivates the last visual selection (with the same type: charwise, linewise or blockwise), | |
| -- which is exactly what textcase.operator() expects to detect the range. | |
| vim.cmd("normal! gv") | |
| textcase.operator(choice.method) | |
| else | |
| textcase.current_word(choice.method) | |
| end | |
| end) | |
| end, { | |
| desc = "Select case and replace word/selection", | |
| range = true, | |
| }) | |
| -- :TextCaseLspRename → case selector via LSP rename | |
| vim.api.nvim_create_user_command("CaseLSP", function() | |
| pick_case("LSP rename to:", function(choice) | |
| textcase.lsp_rename(choice.method) | |
| end) | |
| end, { desc = "Select case and LSP rename symbol under cursor" }) | |
| -- ════════════════════════════════════════════════════════════ | |
| -- :CaseHelp → shows the table in a floating popup | |
| -- ════════════════════════════════════════════════════════════ | |
| local function show_mappings_popup() | |
| local lines = { | |
| " Change case", | |
| " ─────────────────", | |
| "", | |
| string.format(" %-14s %-18s %-8s %-8s %-8s", "Case", "Example", "Word", "LSP", "Operator"), | |
| string.format(" %-14s %-18s %-8s %-8s %-8s", string.rep("─", 14), string.rep("─", 18), string.rep("─", 8), string.rep("─", 8), string.rep("─", 8)), | |
| } | |
| for _, c in ipairs(cases) do | |
| table.insert(lines, string.format(" %-14s %-18s ga%-6s ga%-6s ge%-6s", c.label, c.example, c.word, c.lsp, c.op)) | |
| end | |
| table.insert(lines, "") | |
| table.insert(lines, " Prefix: ga<k>=word ga<K>=LSP ge<k>=operator (motion)") | |
| table.insert(lines, " Press q or <Esc> to close") | |
| -- Calculate dimensions | |
| local width = 0 | |
| for _, l in ipairs(lines) do | |
| width = math.max(width, vim.fn.strdisplaywidth(l)) | |
| end | |
| width = width + 2 | |
| local height = #lines | |
| local buf = vim.api.nvim_create_buf(false, true) | |
| vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) | |
| vim.bo[buf].modifiable = false | |
| vim.bo[buf].bufhidden = "wipe" | |
| vim.bo[buf].filetype = "textcase_help" | |
| local win = vim.api.nvim_open_win(buf, true, { | |
| relative = "editor", | |
| width = width, | |
| height = height, | |
| row = math.floor((vim.o.lines - height) / 2), | |
| col = math.floor((vim.o.columns - width) / 2), | |
| style = "minimal", | |
| border = "rounded", | |
| title = " TextCase ", | |
| title_pos = "center", | |
| }) | |
| vim.wo[win].cursorline = true | |
| -- Close with q or <Esc> | |
| local close = function() | |
| pcall(vim.api.nvim_win_close, win, true) | |
| end | |
| vim.keymap.set("n", "q", close, { buffer = buf, nowait = true, silent = true }) | |
| vim.keymap.set("n", "<Esc>", close, { buffer = buf, nowait = true, silent = true }) | |
| end | |
| vim.api.nvim_create_user_command("CaseHelp", show_mappings_popup, { desc = "Show TextCase help" }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment