Skip to content

Instantly share code, notes, and snippets.

@eduardoarandah
Created April 22, 2026 17:42
Show Gist options
  • Select an option

  • Save eduardoarandah/0820b469b2921461558862730578a393 to your computer and use it in GitHub Desktop.

Select an option

Save eduardoarandah/0820b469b2921461558862730578a393 to your computer and use it in GitHub Desktop.
neovim text-case improved
----------------------------------------------------
-- 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