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.

Revisions

  1. eduardoarandah created this gist Apr 22, 2026.
    169 changes: 169 additions & 0 deletions textcase.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,169 @@
    ----------------------------------------------------
    -- 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" })