From 3571520086988169991f0e9a180570391fd0c991 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Tue, 23 Jun 2026 19:31:17 -0600 Subject: [PATCH 01/18] refactor: trim redundant config and fix HashNewSyntax duplicate - Remove duplicate HashNewSyntax user command registration - Trim options.lua to personal overrides (drop LazyVim defaults) - Drop redundant nvin-web-devicons plugin spec - Remove unused sonokai plugin; keep tokyonight in lazy install list - Update README to match current plugin set Co-authored-by: Cursor --- README.md | 8 +---- lua/config/autocmds.lua | 2 -- lua/config/lazy.lua | 2 +- lua/config/options.lua | 51 +++---------------------------- lua/plugins/nvin-web-devicons.lua | 4 --- lua/plugins/sonokai.lua | 6 ---- 6 files changed, 7 insertions(+), 66 deletions(-) delete mode 100644 lua/plugins/nvin-web-devicons.lua delete mode 100644 lua/plugins/sonokai.lua diff --git a/README.md b/README.md index c34fd61..9e3186a 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,6 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def | [vim-bundler](https://github.com/tpope/vim-bundler) | `tpope.lua` | Jump to gems from `Gemfile` (`gf` in Ruby buffers) | | [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim) | `reprobado.lua` | Background jobs for `:Reprobado` / `:Reprobada` audio | | [vim-fetch](https://github.com/kopischke/vim-fetch) | `vim-fetch.lua` | Preserve file cursor position across reloads | -| [sonokai](https://github.com/sainnhe/sonokai) | `sonokai.lua` | Extra colorscheme (lazy-loaded; pick via Snacks or `:colorscheme`) | | [clipring.nvim](https://github.com/alexesba/clipring.nvim) | `clipring.lua` | Persistent yank/clipboard history ring | #### oil.nvim @@ -150,10 +149,6 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def - `yh` opens the yank history picker. - Ring is persisted across sessions (`persist = true`, up to 100 entries). -#### sonokai - -- Not loaded until selected; install list includes it in `lua/config/lazy.lua` for `:colorscheme sonokai` or the Snacks colorscheme picker. - ### LazyVim overrides (tweaks, not new plugins) | File | What it changes | @@ -163,7 +158,6 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def | `lsp.lua` | Disables diagnostic virtual text (float on `CursorHold` in `autocmds.lua`) | | `autosave-colorscheme.lua` | Saves/restores last colorscheme via [autosave-colorscheme.nvim](https://github.com/alexesba/autosave-colorscheme.nvim) | | `disabled.lua` | Turns off `bufferline.nvim`, `neo-tree.nvim`, `mini.files` | -| `nvin-web-devicons.lua` | Ensures devicons are lazy-loaded (dependency for Oil, luatab) | ### Disabled LazyVim plugins @@ -207,7 +201,7 @@ Press `` and wait for which-key for the full list. ### Colorschemes 1. `uC` or `:ColorScheme` — pick a theme in Snacks (live preview, works on empty buffers). -2. Or `:colorscheme ` (e.g. `sonokai`, `tokyonight`). +2. Or `:colorscheme ` (e.g. `tokyonight`). 3. Restart Neovim — your last theme is restored automatically. To reset persistence: diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 8d30ca9..6dd3dee 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -86,8 +86,6 @@ vim.api.nvim_create_user_command("UpdateRubyHashesByLines", FormatHashes, { desc vim.api.nvim_create_user_command("HashNewSyntax", HashNewSyntax, { desc = "update hash old to new syntax" }) vim.api.nvim_create_user_command("HashOldSyntax", HashOldSyntax, { desc = "update hash new to old syntax" }) -vim.api.nvim_create_user_command("HashNewSyntax", HashOldSyntax, { desc = "update hash syntax" }) - vim.api.nvim_create_user_command("CleanWhiteSpaces", CleanWhiteSpaces, { desc = "Clean White spaces" }) vim.api.nvim_create_user_command("UnscapeDoubleQuotes", UnscapeDoubleQuotes, { desc = "unscape double quotes" }) diff --git a/lua/config/lazy.lua b/lua/config/lazy.lua index f1c7fa5..87d3d33 100644 --- a/lua/config/lazy.lua +++ b/lua/config/lazy.lua @@ -30,7 +30,7 @@ require("lazy").setup({ version = false, -- always use the latest git commit -- version = "*", -- try installing the latest stable version for plugins that support semver }, - install = { colorscheme = { "tokyonight", "habamax", "sonokai" } }, + install = { colorscheme = { "tokyonight" } }, checker = { enabled = true, -- check for plugin updates periodically notify = false, -- notify on update diff --git a/lua/config/options.lua b/lua/config/options.lua index b68c87c..a402e4a 100644 --- a/lua/config/options.lua +++ b/lua/config/options.lua @@ -3,22 +3,9 @@ -- Personal overrides: lua/config/options.local.lua (gitignored; see options.local.lua.example) local load_local = require("utils.local").load local set = vim.opt + vim.o.winborder = "rounded" -set.number = true -set.relativenumber = true -set.cp = false -set.hidden = true -set.expandtab = true -set.backup = false -set.wrap = false -set.writebackup = false -set.autoindent = true -set.hlsearch = true -set.incsearch = true -set.showmatch = true -set.backspace = { "indent", "eol", "start" } -set.laststatus = 2 -set.ruler = true +set.colorcolumn = "81" set.listchars = { eol = "$", extends = ">", @@ -26,14 +13,11 @@ set.listchars = { tab = ">-", trail = "~", } -set.swapfile = false set.synmaxcol = 150 -set.cursorline = true -set.signcolumn = "yes" -set.termguicolors = true -set.colorcolumn = "81" -set.updatetime = 100 set.diffopt:append({ "vertical" }) +set.list = false +set.updatetime = 500 -- ms before CursorHold fires (affects diagnostic float speed) + vim.g.mapleader = "," vim.g.maplocalleader = "\\" @@ -43,29 +27,4 @@ vim.g.table_mode_corner = "+" vim.g.enable_italic_font = 1 vim.inccommand = "nosplit" -set.wildignore:append({ - "blue.vim", - "darkblue.vim", - "default.vim", - "delek.vim", - "desert.vim", - "elflord.vim", - "evening.vim", - "industry.vim", - "koehler.vim", - "morning.vim", - "murphy.vim", - "pablo.vim", - "peachpuff.vim", - "ron.vim", - "shine.vim", - "slate.vim", - "torte.vim", - "zellner.vim", -}) - -set.list = false -set.updatetime = 500 -- ms before CursorHold fires (affects diagnostic float speed) --- set.cmdheight=0 - load_local("options") diff --git a/lua/plugins/nvin-web-devicons.lua b/lua/plugins/nvin-web-devicons.lua deleted file mode 100644 index 267c9f1..0000000 --- a/lua/plugins/nvin-web-devicons.lua +++ /dev/null @@ -1,4 +0,0 @@ -return { - "kyazdani42/nvim-web-devicons", - lazy = true, -} diff --git a/lua/plugins/sonokai.lua b/lua/plugins/sonokai.lua deleted file mode 100644 index 4d41ac7..0000000 --- a/lua/plugins/sonokai.lua +++ /dev/null @@ -1,6 +0,0 @@ -return { - { - "sainnhe/sonokai", - lazy = true, - }, -} From ae5551a2cbb085a11906fd760d7595047dd96ac8 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Tue, 23 Jun 2026 19:35:02 -0600 Subject: [PATCH 02/18] refactor: lazy-load oil/reprobado and drop duplicate commands - Lazy-load oil.nvim (keys, cmd, nvim . still opens via Oil) - Lazy-load reprobado on commands and leader keymaps; merge vorbis-tools spec - Remove FormatJSONV2, FormatSQLV2, and :ColorScheme (LazyVim covers these) - Drop unused require_tool entries for underscore and sql-formatter-cli Co-authored-by: Cursor --- README.md | 11 +++++------ lua/config/autocmds.lua | 12 ------------ lua/config/keymaps.lua | 9 --------- lua/plugins/oil.lua | 5 ++--- lua/plugins/reprobado.lua | 31 ++++++++++++++++++++----------- lua/utils/functions.lua | 6 ------ lua/utils/require_tool.lua | 24 ------------------------ spec/README.md | 5 ++--- tests/autocmds_spec.lua | 2 -- tests/require_tool_spec.lua | 6 +++--- tests/utils_spec.lua | 7 ------- 11 files changed, 32 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 9e3186a..5853da7 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,8 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def #### reprobado (local audio commands) - `:Reprobado` / `:Reprobada` play Ogg files from `sounds/` via `ogg123` and [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim). -- Logic lives in `lua/reprobado/init.lua`; lazy spec in `lua/plugins/reprobado.lua`. -- Depends on [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim) and a local `vorbis-tools` meta-spec (same file) that installs `ogg123` when missing (Homebrew / apt / pacman / dnf / zypper). -- Keymaps (`no` / `na`) stay in `lua/config/keymaps.lua`. +- Logic lives in `lua/reprobado/init.lua`; lazy spec in `lua/plugins/reprobado.lua` (loads on command or `no` / `na`). +- Depends on [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim); installs `ogg123` when missing (Homebrew / apt / pacman / dnf / zypper). #### clipring.nvim @@ -172,7 +171,7 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def - Arrow keys disabled in Normal / Insert / Visual (use `hjkl`) - Search centering (`n` / `N` with `zz`), better `J` join, undo breakpoints in Insert - **Diagnostics** — virtual text off; float opens on `CursorHold` (see `lua/plugins/lsp.lua`, `lua/config/autocmds.lua`) -- **Colorschemes** — default Tokyo Night (moon); last pick restored on startup (`:ColorScheme`, `uC`, or `:colorscheme`); fallback Tokyo Night if saved theme fails +- **Colorschemes** — default Tokyo Night (moon); last pick restored on startup (`uC` or `:colorscheme`); fallback Tokyo Night if saved theme fails Saved theme path: `~/.local/state/nvim/last-colorscheme` @@ -200,7 +199,7 @@ Press `` and wait for which-key for the full list. ### Colorschemes -1. `uC` or `:ColorScheme` — pick a theme in Snacks (live preview, works on empty buffers). +1. `uC` — pick a theme in Snacks (live preview, works on empty buffers). 2. Or `:colorscheme ` (e.g. `tokyonight`). 3. Restart Neovim — your last theme is restored automatically. @@ -258,7 +257,7 @@ Restart Neovim after structural config changes (lazy.nvim does not fully reload ### Custom commands (`:command`) -Formatting and cleanup: `FormatJSON`, `FormatJSONV2`, `FormatCss`, `FormatSQL`, `FormatSQLV2`, `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` +Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` Quotes / Ruby: `DoubleQuotes`, `SingleQuotes`, `DoubleQuotesC`, `SingleQuotesC`, `HashNewSyntax`, `HashOldSyntax`, `UpdateRubyHashesByLines`, `UnscapeDoubleQuotes` diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 6dd3dee..b1f60b0 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -14,12 +14,6 @@ vim.api.nvim_create_user_command("FormatJSON", function() end end, { bang = true, desc = "Format json files with python" }) -vim.api.nvim_create_user_command("FormatJSONV2", function() - if require("utils.require_tool").ensure("underscore") then - vim.cmd("%!underscore print --outfmt json") - end -end, { bang = true, desc = "Format json files using underline-cli" }) - vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format css using regex" }) vim.api.nvim_create_user_command("RemoveExtraEmptyLines", RemoveExtraEmptyLines, { desc = "Remove extra empty lines" }) @@ -32,8 +26,6 @@ vim.api.nvim_create_user_command("RemoveEmptyLines", RemoveEmptyLines, { desc = vim.api.nvim_create_user_command("FormatSQL", FormatSQL, { desc = "Format SQL files using sqlformat" }) -vim.api.nvim_create_user_command("FormatSQLV2", FormatSQLV2, { desc = "Format SQL using sql-formatter-cli" }) - local function copy_to_clipboard(path, title) if path == "" then vim.notify("Not a file buffer", vim.log.levels.WARN) @@ -105,7 +97,3 @@ vim.api.nvim_create_autocmd("CursorHold", { vim.diagnostic.open_float(nil, { focus = false }) end, }) - -vim.api.nvim_create_user_command("ColorScheme", function() - require("snacks").picker.colorschemes() -end, { desc = "Pick colorscheme (Snacks picker with preview)" }) diff --git a/lua/config/keymaps.lua b/lua/config/keymaps.lua index 81db4b0..08feb06 100644 --- a/lua/config/keymaps.lua +++ b/lua/config/keymaps.lua @@ -16,15 +16,6 @@ map("n", "8", ":tabn 8", { noremap = true, silent = true }) map("n", "9", ":tabn 9", { noremap = true, silent = true }) map("n", "0", ":tabn 0", { noremap = true, silent = true }) -map("n", "no", [[lua require("reprobado").play("Reprobado.ogg")]], { - silent = true, - desc = "Play Reprobado", -}) -map("n", "na", [[lua require("reprobado").play("Reprobada.ogg")]], { - silent = true, - desc = "Play Reprobada", -}) - -- Remove newbie crutches in Normal Mode map("n", "", "") map("n", "", "") diff --git a/lua/plugins/oil.lua b/lua/plugins/oil.lua index bcf0677..18c963e 100644 --- a/lua/plugins/oil.lua +++ b/lua/plugins/oil.lua @@ -1,12 +1,11 @@ return { "stevearc/oil.nvim", - lazy = false, + lazy = true, opts = { default_file_explorer = true, }, - -- Optional dependencies dependencies = { "nvim-web-devicons" }, - cmd = { "Exp", "Explore", "Explorer" }, + cmd = { "Exp", "Explore", "Explorer", "Oil" }, keys = { { "-", diff --git a/lua/plugins/reprobado.lua b/lua/plugins/reprobado.lua index a7fa6e9..743e669 100644 --- a/lua/plugins/reprobado.lua +++ b/lua/plugins/reprobado.lua @@ -1,22 +1,31 @@ --- Local reprobado commands (:Reprobado / :Reprobada). Keymaps live in lua/config/keymaps.lua. +-- Local reprobado commands (:Reprobado / :Reprobada). return { - { - dir = vim.fn.stdpath("config"), - name = "vorbis-tools", - lazy = false, - config = function() - require("reprobado").ensure_player() - end, - }, { dir = vim.fn.stdpath("config"), name = "reprobado", - lazy = false, + lazy = true, + cmd = { "Reprobado", "Reprobada" }, + keys = { + { + "no", + function() + require("reprobado").play("Reprobado.ogg") + end, + desc = "Play Reprobado", + }, + { + "na", + function() + require("reprobado").play("Reprobada.ogg") + end, + desc = "Play Reprobada", + }, + }, dependencies = { "skywind3000/asyncrun.vim", - "vorbis-tools", }, config = function() + require("reprobado").ensure_player() require("reprobado").setup() require("reprobado").setup_commands() end, diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index a517ca4..1560561 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -46,12 +46,6 @@ function FormatSQL() end) end -function FormatSQLV2() - with_tool("sql-formatter-cli", function() - cmdPreserveCursorPosition([[%!sql-formatter-cli]]) - end) -end - function DoubleQuotes() cmdPreserveCursorPosition([[%s/'\([^']*\)'/"\1"/g]]) end diff --git a/lua/utils/require_tool.lua b/lua/utils/require_tool.lua index 69b963c..8fcc9c4 100644 --- a/lua/utils/require_tool.lua +++ b/lua/utils/require_tool.lua @@ -30,30 +30,6 @@ local tools = { fallback = "pip3 install sqlparse", }, }, - ["sql-formatter-cli"] = { - bin = "sql-formatter-cli", - label = "sql-formatter-cli", - install = { - Darwin = "npm install -g sql-formatter-cli", - Linux_apt = "npm install -g sql-formatter-cli", - Linux_pacman = "npm install -g sql-formatter-cli", - Linux_dnf = "npm install -g sql-formatter-cli", - Linux_zypper = "npm install -g sql-formatter-cli", - fallback = "npm install -g sql-formatter-cli", - }, - }, - underscore = { - bin = "underscore", - label = "underscore-cli", - install = { - Darwin = "npm install -g underscore-cli", - Linux_apt = "npm install -g underscore-cli", - Linux_pacman = "npm install -g underscore-cli", - Linux_dnf = "npm install -g underscore-cli", - Linux_zypper = "npm install -g underscore-cli", - fallback = "npm install -g underscore-cli", - }, - }, cat = { bin = "cat", label = "cat", diff --git a/spec/README.md b/spec/README.md index 8a06a64..5e24e0b 100644 --- a/spec/README.md +++ b/spec/README.md @@ -22,11 +22,10 @@ Format helpers that shell out to OS tools are covered by contract tests (mocked |--------------------|---------------| | `FormatXML` | `python3` (`xml.dom.minidom`) | | `FormatSQL` | `sqlformat` | -| `FormatSQLV2` | `sql-formatter-cli` | | `RemoveExtraEmptyLines` | `cat` | -| `FormatJSON` / `FormatJSONV2` (autocmds) | `python3`, `underscore` | +| `FormatJSON` (autocmds) | `python3` | -Install these locally when you use the commands. If a tool is missing, Neovim notifies you with the install command to run (e.g. `pip3 install sqlparse`, `npm install -g sql-formatter-cli`). CI verifies both the install hints and the expected filter wiring. +Install these locally when you use the commands. If a tool is missing, Neovim notifies you with the install command to run (e.g. `pip3 install sqlparse`). CI verifies both the install hints and the expected filter wiring. - `lua/utils/local.lua` — overlay load, missing file, and error notification - `lua/config/autocmds.lua` — command registration, `CopyFullPath`, `CopyRelativePath` diff --git a/tests/autocmds_spec.lua b/tests/autocmds_spec.lua index 5dc52c5..b045535 100644 --- a/tests/autocmds_spec.lua +++ b/tests/autocmds_spec.lua @@ -34,10 +34,8 @@ describe("config.autocmds", function() assert.is_not_nil(cmds.CopyRelativePath) assert.is_not_nil(cmds.FormatCss) assert.is_not_nil(cmds.FormatJSON) - assert.is_not_nil(cmds.FormatJSONV2) assert.equals("Copy file full path", helpers.command_desc(cmds.CopyFullPath)) assert.equals("Format json files with python", helpers.command_desc(cmds.FormatJSON)) - assert.equals("Format json files using underline-cli", helpers.command_desc(cmds.FormatJSONV2)) end) it("FormatJSON warns when python3 is missing", function() diff --git a/tests/require_tool_spec.lua b/tests/require_tool_spec.lua index eb03df1..d70a331 100644 --- a/tests/require_tool_spec.lua +++ b/tests/require_tool_spec.lua @@ -54,7 +54,7 @@ describe("utils.require_tool", function() assert.matches("sudo apt%-get install %-y python3", messages[1]) end) - it("falls back to npm install for sql-formatter-cli", function() + it("warns with cat fallback when cat is missing", function() local messages = helpers.capture_notify(function() helpers.with_mocked_fn({ executable = function() @@ -64,10 +64,10 @@ describe("utils.require_tool", function() return "Linux\n" end, }, function() - assert.is_false(require_tool.ensure("sql-formatter-cli")) + assert.is_false(require_tool.ensure("cat")) end) end) - assert.matches("npm install %-g sql%-formatter%-cli", messages[1]) + assert.matches("coreutils", messages[1]) end) end) diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index c189b62..0ce5a4d 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -162,13 +162,6 @@ describe("utils.functions", function() assert.matches("sqlformat", captured_cmds[1]) end) - it("FormatSQLV2 pipes the buffer through sql-formatter-cli", function() - with_tool_and_preserve("sql-formatter-cli", function() - FormatSQLV2() - end) - assert.matches("sql%-formatter%-cli", captured_cmds[1]) - end) - it("RemoveExtraEmptyLines pipes the buffer through cat -s", function() with_tool_and_preserve("cat", function() RemoveExtraEmptyLines() From 1f95893e7af6ab42dfbb92ffa919e34e0007f2ac Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Tue, 23 Jun 2026 19:37:30 -0600 Subject: [PATCH 03/18] feat: restore :ColorScheme command for Snacks picker Co-authored-by: Cursor --- README.md | 4 ++-- lua/config/autocmds.lua | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5853da7..0d63e24 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def - Arrow keys disabled in Normal / Insert / Visual (use `hjkl`) - Search centering (`n` / `N` with `zz`), better `J` join, undo breakpoints in Insert - **Diagnostics** — virtual text off; float opens on `CursorHold` (see `lua/plugins/lsp.lua`, `lua/config/autocmds.lua`) -- **Colorschemes** — default Tokyo Night (moon); last pick restored on startup (`uC` or `:colorscheme`); fallback Tokyo Night if saved theme fails +- **Colorschemes** — default Tokyo Night (moon); last pick restored on startup (`:ColorScheme`, `uC`, or `:colorscheme`); fallback Tokyo Night if saved theme fails Saved theme path: `~/.local/state/nvim/last-colorscheme` @@ -199,7 +199,7 @@ Press `` and wait for which-key for the full list. ### Colorschemes -1. `uC` — pick a theme in Snacks (live preview, works on empty buffers). +1. `uC` or `:ColorScheme` — pick a theme in Snacks (live preview, works on empty buffers). 2. Or `:colorscheme ` (e.g. `tokyonight`). 3. Restart Neovim — your last theme is restored automatically. diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index b1f60b0..bd55246 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -97,3 +97,7 @@ vim.api.nvim_create_autocmd("CursorHold", { vim.diagnostic.open_float(nil, { focus = false }) end, }) + +vim.api.nvim_create_user_command("ColorScheme", function() + require("snacks").picker.colorschemes() +end, { desc = "Pick colorscheme (Snacks picker with preview)" }) From 16bb0ead799cee2fd9aeca2cf149771ccb8a1012 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Tue, 23 Jun 2026 19:39:48 -0600 Subject: [PATCH 04/18] feat: restore sql-formatter-cli as FormatSQLFormatter Two SQL formatters use different tools: FormatSQL (sqlformat/sqlparse) and FormatSQLFormatter (sql-formatter-cli). Keep FormatSQLV2 as an alias. Co-authored-by: Cursor --- README.md | 2 +- lua/config/autocmds.lua | 9 ++++++++- lua/utils/functions.lua | 7 +++++++ lua/utils/require_tool.lua | 12 ++++++++++++ spec/README.md | 3 ++- tests/require_tool_spec.lua | 17 +++++++++++++++++ tests/utils_spec.lua | 7 +++++++ 7 files changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0d63e24..4d1011f 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Restart Neovim after structural config changes (lazy.nvim does not fully reload ### Custom commands (`:command`) -Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` +Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatSQLFormatter` (`FormatSQLV2` alias), `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` Quotes / Ruby: `DoubleQuotes`, `SingleQuotes`, `DoubleQuotesC`, `SingleQuotesC`, `HashNewSyntax`, `HashOldSyntax`, `UpdateRubyHashesByLines`, `UnscapeDoubleQuotes` diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index bd55246..4fa41dd 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -24,7 +24,14 @@ vim.api.nvim_create_user_command("ConvertTabToSpaces", ConvertTabToSpaces, { des vim.api.nvim_create_user_command("RemoveEmptyLines", RemoveEmptyLines, { desc = "Remove Empty Lines" }) -vim.api.nvim_create_user_command("FormatSQL", FormatSQL, { desc = "Format SQL files using sqlformat" }) +vim.api.nvim_create_user_command("FormatSQL", FormatSQL, { desc = "Format SQL with sqlformat (sqlparse)" }) + +vim.api.nvim_create_user_command("FormatSQLFormatter", FormatSQLFormatter, { + desc = "Format SQL with sql-formatter-cli (npm)", +}) +vim.api.nvim_create_user_command("FormatSQLV2", FormatSQLFormatter, { + desc = "Alias for FormatSQLFormatter (sql-formatter-cli)", +}) local function copy_to_clipboard(path, title) if path == "" then diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 1560561..1e42f78 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -46,6 +46,13 @@ function FormatSQL() end) end +--- sql-formatter-cli (npm); distinct from FormatSQL which uses sqlformat/sqlparse. +function FormatSQLFormatter() + with_tool("sql-formatter-cli", function() + cmdPreserveCursorPosition([[%!sql-formatter-cli]]) + end) +end + function DoubleQuotes() cmdPreserveCursorPosition([[%s/'\([^']*\)'/"\1"/g]]) end diff --git a/lua/utils/require_tool.lua b/lua/utils/require_tool.lua index 8fcc9c4..cb1e5b0 100644 --- a/lua/utils/require_tool.lua +++ b/lua/utils/require_tool.lua @@ -30,6 +30,18 @@ local tools = { fallback = "pip3 install sqlparse", }, }, + ["sql-formatter-cli"] = { + bin = "sql-formatter-cli", + label = "sql-formatter-cli", + install = { + Darwin = "npm install -g sql-formatter-cli", + Linux_apt = "npm install -g sql-formatter-cli", + Linux_pacman = "npm install -g sql-formatter-cli", + Linux_dnf = "npm install -g sql-formatter-cli", + Linux_zypper = "npm install -g sql-formatter-cli", + fallback = "npm install -g sql-formatter-cli", + }, + }, cat = { bin = "cat", label = "cat", diff --git a/spec/README.md b/spec/README.md index 5e24e0b..510dfcd 100644 --- a/spec/README.md +++ b/spec/README.md @@ -21,7 +21,8 @@ Format helpers that shell out to OS tools are covered by contract tests (mocked | Command / function | External tool | |--------------------|---------------| | `FormatXML` | `python3` (`xml.dom.minidom`) | -| `FormatSQL` | `sqlformat` | +| `FormatSQL` | `sqlformat` (sqlparse) | +| `FormatSQLFormatter` / `FormatSQLV2` | `sql-formatter-cli` | | `RemoveExtraEmptyLines` | `cat` | | `FormatJSON` (autocmds) | `python3` | diff --git a/tests/require_tool_spec.lua b/tests/require_tool_spec.lua index d70a331..9785d8d 100644 --- a/tests/require_tool_spec.lua +++ b/tests/require_tool_spec.lua @@ -54,6 +54,23 @@ describe("utils.require_tool", function() assert.matches("sudo apt%-get install %-y python3", messages[1]) end) + it("falls back to npm install for sql-formatter-cli", function() + local messages = helpers.capture_notify(function() + helpers.with_mocked_fn({ + executable = function() + return 0 + end, + system = function() + return "Linux\n" + end, + }, function() + assert.is_false(require_tool.ensure("sql-formatter-cli")) + end) + end) + + assert.matches("npm install %-g sql%-formatter%-cli", messages[1]) + end) + it("warns with cat fallback when cat is missing", function() local messages = helpers.capture_notify(function() helpers.with_mocked_fn({ diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 0ce5a4d..08abdfb 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -162,6 +162,13 @@ describe("utils.functions", function() assert.matches("sqlformat", captured_cmds[1]) end) + it("FormatSQLFormatter pipes the buffer through sql-formatter-cli", function() + with_tool_and_preserve("sql-formatter-cli", function() + FormatSQLFormatter() + end) + assert.matches("sql%-formatter%-cli", captured_cmds[1]) + end) + it("RemoveExtraEmptyLines pipes the buffer through cat -s", function() with_tool_and_preserve("cat", function() RemoveExtraEmptyLines() From 2a1a926c42a1acd8e6c9795842285876ad3eb3de Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Tue, 23 Jun 2026 19:44:21 -0600 Subject: [PATCH 05/18] feat: route SQL/JSON/XML format commands through conform Add conform formatter definitions and utils.format helper so :FormatSQL, :FormatSQLFormatter, :FormatJSON, and :FormatXML share the same engines as cf while keeping install hints from require_tool. Co-authored-by: Cursor --- README.md | 3 +- lua/config/autocmds.lua | 4 +-- lua/plugins/conform.lua | 32 +++++++++++++++++++ lua/utils/format.lua | 45 +++++++++++++++++++++++++++ lua/utils/functions.lua | 31 ++++++++---------- spec/README.md | 6 ++-- tests/format_spec.lua | 69 +++++++++++++++++++++++++++++++++++++++++ tests/utils_spec.lua | 38 ++++++++--------------- 8 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 lua/plugins/conform.lua create mode 100644 lua/utils/format.lua create mode 100644 tests/format_spec.lua diff --git a/README.md b/README.md index 4d1011f..a457e77 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def | --- | --- | | `snaks.lua` | Disables Snacks file explorer (Oil is used instead); custom Snacks picker keys (`fg`, `fk`) | | `noice.lua` | Rounded LSP hover borders only; cmdline/messages/popupmenu stay on Vim defaults | +| `conform.lua` | Custom SQL/JSON/XML formatters; `cf` uses sqlformat by default for SQL | | `lsp.lua` | Disables diagnostic virtual text (float on `CursorHold` in `autocmds.lua`) | | `autosave-colorscheme.lua` | Saves/restores last colorscheme via [autosave-colorscheme.nvim](https://github.com/alexesba/autosave-colorscheme.nvim) | | `disabled.lua` | Turns off `bufferline.nvim`, `neo-tree.nvim`, `mini.files` | @@ -257,7 +258,7 @@ Restart Neovim after structural config changes (lazy.nvim does not fully reload ### Custom commands (`:command`) -Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatSQLFormatter` (`FormatSQLV2` alias), `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` +Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatSQLFormatter` (`FormatSQLV2` alias), `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` — SQL/JSON/XML formatters use [conform.nvim](https://github.com/stevearc/conform.nvim) (same engines as `cf` for those filetypes) Quotes / Ruby: `DoubleQuotes`, `SingleQuotes`, `DoubleQuotesC`, `SingleQuotesC`, `HashNewSyntax`, `HashOldSyntax`, `UpdateRubyHashesByLines`, `UnscapeDoubleQuotes` diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 4fa41dd..68967e0 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -9,9 +9,7 @@ require("utils.functions") vim.api.nvim_create_user_command("FormatJSON", function() - if require("utils.require_tool").ensure("python3") then - vim.cmd("%!python3 -m json.tool") - end + require("utils.format").run({ "json_tool" }, "Format JSON") end, { bang = true, desc = "Format json files with python" }) vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format css using regex" }) diff --git a/lua/plugins/conform.lua b/lua/plugins/conform.lua new file mode 100644 index 0000000..6239af4 --- /dev/null +++ b/lua/plugins/conform.lua @@ -0,0 +1,32 @@ +return { + { + "stevearc/conform.nvim", + opts = { + formatters_by_ft = { + sql = { "sqlformat", stop_after_first = true }, + json = { "json_tool" }, + xml = { "xml_minidom" }, + }, + formatters = { + sqlformat = { + command = "sqlformat", + args = { "--reindent", "--keywords", "upper", "--identifiers", "lower", "-" }, + }, + sql_formatter_cli = { + command = "sql-formatter-cli", + }, + json_tool = { + command = "python3", + args = { "-m", "json.tool" }, + }, + xml_minidom = { + command = "python3", + args = { + "-c", + "import xml.dom.minidom, sys; print(xml.dom.minidom.parse(sys.stdin).toprettyxml())", + }, + }, + }, + }, + }, +} diff --git a/lua/utils/format.lua b/lua/utils/format.lua new file mode 100644 index 0000000..72fbbf7 --- /dev/null +++ b/lua/utils/format.lua @@ -0,0 +1,45 @@ +local M = {} + +local TOOL_BY_FORMATTER = { + sqlformat = "sqlformat", + sql_formatter_cli = "sql-formatter-cli", + json_tool = "python3", + xml_minidom = "python3", +} + +--- Run conform formatters by name (used by :FormatSQL, :FormatJSON, etc.). +---@param formatters string[] +---@param label? string +---@return boolean +function M.run(formatters, label) + local require_tool = require("utils.require_tool") + + for _, name in ipairs(formatters) do + local tool = TOOL_BY_FORMATTER[name] + if tool and not require_tool.ensure(tool) then + return false + end + end + + local ok, conform = pcall(require, "conform") + if not ok then + vim.notify("conform.nvim is not available", vim.log.levels.ERROR) + return false + end + + local err = conform.format({ + async = false, + timeout_ms = 5000, + lsp_format = "never", + formatters = formatters, + }) + + if err then + vim.notify(("%s: %s"):format(label or "Format", err), vim.log.levels.ERROR) + return false + end + + return true +end + +return M diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 1e42f78..fe61f44 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -1,5 +1,6 @@ local cmdPreserveCursorPosition = require("utils.cmdPreservePosition") local require_tool = require("utils.require_tool") +local format = require("utils.format") local function with_tool(tool, fn) if not require_tool.ensure(tool) then @@ -13,15 +14,16 @@ function FormatCss() end function FormatXML() - with_tool("python3", function() - local save_cursor = vim.fn.getpos(".") - vim.cmd([[ - silent! %s/\\"/"/g | - silent! %s/\\n//g | - silent! %!python3 -c "import xml.dom.minidom, sys; print(xml.dom.minidom.parse(sys.stdin).toprettyxml())" - ]]) - vim.fn.setpos(".", save_cursor) - end) + if not require_tool.ensure("python3") then + return + end + local save_cursor = vim.fn.getpos(".") + vim.cmd([[ + silent! %s/\\"/"/g | + silent! %s/\\n//g + ]]) + format.run({ "xml_minidom" }, "Format XML") + vim.fn.setpos(".", save_cursor) end function RemoveExtraEmptyLines() @@ -39,18 +41,11 @@ function RemoveEmptyLines() end function FormatSQL() - with_tool("sqlformat", function() - cmdPreserveCursorPosition([[ - '%!sqlformat --reindent --keywords upper --identifiers lower -' - ]]) - end) + format.run({ "sqlformat" }, "Format SQL") end ---- sql-formatter-cli (npm); distinct from FormatSQL which uses sqlformat/sqlparse. function FormatSQLFormatter() - with_tool("sql-formatter-cli", function() - cmdPreserveCursorPosition([[%!sql-formatter-cli]]) - end) + format.run({ "sql_formatter_cli" }, "Format SQL") end function DoubleQuotes() diff --git a/spec/README.md b/spec/README.md index 510dfcd..fffafa1 100644 --- a/spec/README.md +++ b/spec/README.md @@ -14,6 +14,7 @@ Current coverage: - `lua/utils/cmdPreservePosition.lua` — cursor preserved after commands - `lua/utils/map.lua` — default `noremap` and merged caller options - `lua/utils/require_tool.lua` — missing-tool detection with OS-specific install hints +- `lua/utils/format.lua` — conform formatter dispatch for SQL/JSON/XML commands - `lua/utils/functions.lua` — in-buffer transforms and external-tool command wiring Format helpers that shell out to OS tools are covered by contract tests (mocked `vim.cmd` / `cmdPreservePosition`), not by running the binaries in CI: @@ -21,10 +22,9 @@ Format helpers that shell out to OS tools are covered by contract tests (mocked | Command / function | External tool | |--------------------|---------------| | `FormatXML` | `python3` (`xml.dom.minidom`) | -| `FormatSQL` | `sqlformat` (sqlparse) | -| `FormatSQLFormatter` / `FormatSQLV2` | `sql-formatter-cli` | +| `FormatSQL` / `FormatSQLFormatter` | `sqlformat`, `sql-formatter-cli` (via conform) | +| `FormatXML` | `python3` (via conform) | | `RemoveExtraEmptyLines` | `cat` | -| `FormatJSON` (autocmds) | `python3` | Install these locally when you use the commands. If a tool is missing, Neovim notifies you with the install command to run (e.g. `pip3 install sqlparse`). CI verifies both the install hints and the expected filter wiring. diff --git a/tests/format_spec.lua b/tests/format_spec.lua new file mode 100644 index 0000000..676372f --- /dev/null +++ b/tests/format_spec.lua @@ -0,0 +1,69 @@ +local helpers = require("tests.helpers") + +describe("utils.format", function() + local format + + before_each(function() + package.loaded["conform"] = nil + helpers.reload_module("utils.require_tool") + helpers.reload_module("utils.format") + format = require("utils.format") + end) + + it("warns when a required tool is missing", function() + local messages = helpers.capture_notify(function() + helpers.with_mocked_fn({ + executable = function() + return 0 + end, + }, function() + format.run({ "sqlformat" }, "Format SQL") + end) + end) + + assert.matches("sqlformat", messages[1]) + assert.matches("install with:", messages[1]) + end) + + it("calls conform.format with the requested formatters", function() + local called + package.loaded["conform"] = { + format = function(opts) + called = opts + return nil + end, + } + + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + assert.is_true(format.run({ "sql_formatter_cli" }, "Format SQL")) + end) + + assert.same({ "sql_formatter_cli" }, called.formatters) + assert.is_false(called.async) + assert.equals("never", called.lsp_format) + end) + + it("notifies when conform reports an error", function() + package.loaded["conform"] = { + format = function() + return "formatter failed" + end, + } + + local messages = helpers.capture_notify(function() + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + assert.is_false(format.run({ "json_tool" }, "Format JSON")) + end) + end) + + assert.matches("Format JSON: formatter failed", messages[1]) + end) +end) diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 08abdfb..6d09e2c 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -139,6 +139,7 @@ describe("utils.functions", function() end) it("FormatSQL warns and skips when sqlformat is missing", function() + helpers.reload_module("utils.format") helpers.reload_module("utils.functions") local messages = helpers.capture_notify(function() helpers.with_mocked_fn({ @@ -155,20 +156,6 @@ describe("utils.functions", function() assert.matches("install with:", messages[1]) end) - it("FormatSQL pipes the buffer through sqlformat", function() - with_tool_and_preserve("sqlformat", function() - FormatSQL() - end) - assert.matches("sqlformat", captured_cmds[1]) - end) - - it("FormatSQLFormatter pipes the buffer through sql-formatter-cli", function() - with_tool_and_preserve("sql-formatter-cli", function() - FormatSQLFormatter() - end) - assert.matches("sql%-formatter%-cli", captured_cmds[1]) - end) - it("RemoveExtraEmptyLines pipes the buffer through cat -s", function() with_tool_and_preserve("cat", function() RemoveExtraEmptyLines() @@ -187,13 +174,16 @@ describe("utils.functions", function() assert.matches("%%s/%[{;}", captured_cmds[1]) end) - it("FormatXML runs python minidom via vim.cmd", function() - local cmd_calls = {} - local original_cmd = vim.cmd + it("FormatXML preprocesses escapes then runs conform xml formatter", function() + local formatters_run + package.loaded["utils.format"] = { + run = function(names) + formatters_run = names + return true + end, + } - vim.cmd = function(command) - cmd_calls[#cmd_calls + 1] = command - end + vim.api.nvim_buf_set_lines(0, 0, -1, true, { '{\\"a\\": 1}' }) helpers.with_mocked_fn({ executable = function(name) @@ -205,11 +195,9 @@ describe("utils.functions", function() FormatXML() end) - vim.cmd = original_cmd - - local joined = table.concat(cmd_calls, "\n") - assert.matches("python3", joined) - assert.matches("xml%.dom%.minidom", joined) + package.loaded["utils.format"] = nil + assert.same({ "xml_minidom" }, formatters_run) + assert.same({ '{"a": 1}' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) end) end) end) From c0e7d2a481456123ce37e1de58294058bd79198d Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 14:35:04 -0600 Subject: [PATCH 06/18] feat: format CSS/SCSS via conform Prettier in FormatCss Route :FormatCss through utils.format with prettier, and register css/scss/less/sass filetypes for cf. Co-authored-by: Cursor --- README.md | 4 ++-- lua/config/autocmds.lua | 2 +- lua/plugins/conform.lua | 4 ++++ lua/utils/format.lua | 1 + lua/utils/functions.lua | 2 +- lua/utils/require_tool.lua | 12 ++++++++++++ spec/README.md | 1 + tests/utils_spec.lua | 31 ++++++++++++++++++++++--------- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a457e77..13b3def 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ These are added under `lua/plugins/` and are **not** shipped with LazyVim by def | --- | --- | | `snaks.lua` | Disables Snacks file explorer (Oil is used instead); custom Snacks picker keys (`fg`, `fk`) | | `noice.lua` | Rounded LSP hover borders only; cmdline/messages/popupmenu stay on Vim defaults | -| `conform.lua` | Custom SQL/JSON/XML formatters; `cf` uses sqlformat by default for SQL | +| `conform.lua` | Custom SQL/JSON/XML formatters; Prettier for CSS/SCSS; `cf` uses defaults per filetype | | `lsp.lua` | Disables diagnostic virtual text (float on `CursorHold` in `autocmds.lua`) | | `autosave-colorscheme.lua` | Saves/restores last colorscheme via [autosave-colorscheme.nvim](https://github.com/alexesba/autosave-colorscheme.nvim) | | `disabled.lua` | Turns off `bufferline.nvim`, `neo-tree.nvim`, `mini.files` | @@ -258,7 +258,7 @@ Restart Neovim after structural config changes (lazy.nvim does not fully reload ### Custom commands (`:command`) -Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatSQLFormatter` (`FormatSQLV2` alias), `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` — SQL/JSON/XML formatters use [conform.nvim](https://github.com/stevearc/conform.nvim) (same engines as `cf` for those filetypes) +Formatting and cleanup: `FormatJSON`, `FormatCss`, `FormatSQL`, `FormatSQLFormatter` (`FormatSQLV2` alias), `FormatXML`, `RemoveEmptyLines`, `RemoveExtraEmptyLines`, `CleanWhiteSpaces`, `ConvertTabToSpaces`, `AddLineNumbers`, `RemoveLineBreak` — SQL/JSON/XML/CSS use [conform.nvim](https://github.com/stevearc/conform.nvim) (same engines as `cf` for those filetypes) Quotes / Ruby: `DoubleQuotes`, `SingleQuotes`, `DoubleQuotesC`, `SingleQuotesC`, `HashNewSyntax`, `HashOldSyntax`, `UpdateRubyHashesByLines`, `UnscapeDoubleQuotes` diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 68967e0..7b695b5 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -12,7 +12,7 @@ vim.api.nvim_create_user_command("FormatJSON", function() require("utils.format").run({ "json_tool" }, "Format JSON") end, { bang = true, desc = "Format json files with python" }) -vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format css using regex" }) +vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format CSS/SCSS with Prettier" }) vim.api.nvim_create_user_command("RemoveExtraEmptyLines", RemoveExtraEmptyLines, { desc = "Remove extra empty lines" }) diff --git a/lua/plugins/conform.lua b/lua/plugins/conform.lua index 6239af4..791006a 100644 --- a/lua/plugins/conform.lua +++ b/lua/plugins/conform.lua @@ -6,6 +6,10 @@ return { sql = { "sqlformat", stop_after_first = true }, json = { "json_tool" }, xml = { "xml_minidom" }, + css = { "prettier", stop_after_first = true }, + scss = { "prettier", stop_after_first = true }, + less = { "prettier" }, + sass = { "prettier" }, }, formatters = { sqlformat = { diff --git a/lua/utils/format.lua b/lua/utils/format.lua index 72fbbf7..23cb3ac 100644 --- a/lua/utils/format.lua +++ b/lua/utils/format.lua @@ -5,6 +5,7 @@ local TOOL_BY_FORMATTER = { sql_formatter_cli = "sql-formatter-cli", json_tool = "python3", xml_minidom = "python3", + prettier = "prettier", } --- Run conform formatters by name (used by :FormatSQL, :FormatJSON, etc.). diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index fe61f44..185239c 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -10,7 +10,7 @@ local function with_tool(tool, fn) end function FormatCss() - cmdPreserveCursorPosition([[silent! :%s/[{;}]/&\r/g|norm! =gg]]) + format.run({ "prettier" }, "Format CSS") end function FormatXML() diff --git a/lua/utils/require_tool.lua b/lua/utils/require_tool.lua index cb1e5b0..4a293ce 100644 --- a/lua/utils/require_tool.lua +++ b/lua/utils/require_tool.lua @@ -42,6 +42,18 @@ local tools = { fallback = "npm install -g sql-formatter-cli", }, }, + prettier = { + bin = { "prettier", "prettierd" }, + label = "Prettier", + install = { + Darwin = "npm install -g prettier", + Linux_apt = "npm install -g prettier", + Linux_pacman = "npm install -g prettier", + Linux_dnf = "npm install -g prettier", + Linux_zypper = "npm install -g prettier", + fallback = "npm install -g prettier", + }, + }, cat = { bin = "cat", label = "cat", diff --git a/spec/README.md b/spec/README.md index fffafa1..b4b70a6 100644 --- a/spec/README.md +++ b/spec/README.md @@ -23,6 +23,7 @@ Format helpers that shell out to OS tools are covered by contract tests (mocked |--------------------|---------------| | `FormatXML` | `python3` (`xml.dom.minidom`) | | `FormatSQL` / `FormatSQLFormatter` | `sqlformat`, `sql-formatter-cli` (via conform) | +| `FormatCss` | `prettier` (via conform) | | `FormatXML` | `python3` (via conform) | | `RemoveExtraEmptyLines` | `cat` | diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 6d09e2c..ee7b258 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -163,15 +163,28 @@ describe("utils.functions", function() assert.matches("cat %-s", captured_cmds[1]) end) - it("FormatCss runs a brace-aware substitute command", function() - captured_cmds = {} - package.loaded["utils.cmdPreservePosition"] = function(cmd) - captured_cmds[#captured_cmds + 1] = cmd - end - helpers.reload_module("utils.functions") - require("utils.functions") - FormatCss() - assert.matches("%%s/%[{;}", captured_cmds[1]) + it("FormatCss runs conform prettier formatter", function() + local formatters_run + package.loaded["utils.format"] = { + run = function(names, label) + formatters_run = names + assert.equals("Format CSS", label) + return true + end, + } + + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + helpers.reload_module("utils.functions") + require("utils.functions") + FormatCss() + end) + + package.loaded["utils.format"] = nil + assert.same({ "prettier" }, formatters_run) end) it("FormatXML preprocesses escapes then runs conform xml formatter", function() From 2ff581abf954e538a517d603641eb5702338563b Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 16:52:27 -0600 Subject: [PATCH 07/18] refactor: add FormatJSON helper and drop unused bang flag Wire: FormatJSON through utils.functions like other conform formatters. Co-authored-by: Cursor --- lua/config/autocmds.lua | 4 +--- lua/utils/functions.lua | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 7b695b5..3d6c393 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -8,9 +8,7 @@ -- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell") require("utils.functions") -vim.api.nvim_create_user_command("FormatJSON", function() - require("utils.format").run({ "json_tool" }, "Format JSON") -end, { bang = true, desc = "Format json files with python" }) +vim.api.nvim_create_user_command("FormatJSON", FormatJSON, { desc = "Format json files with python" }) vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format CSS/SCSS with Prettier" }) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 185239c..70ef3cc 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -44,6 +44,10 @@ function FormatSQL() format.run({ "sqlformat" }, "Format SQL") end +function FormatJSON() + format.run({ "json_tool" }, "Format JSON") +end + function FormatSQLFormatter() format.run({ "sql_formatter_cli" }, "Format SQL") end From 1e839f067c445ea7844570469cf774f5ec20d5f5 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 16:54:21 -0600 Subject: [PATCH 08/18] refactor: use cmdPreserveCursorPosition in FormatXML preprocess Replace manual getpos/setpos around escape unwrapping; conform handles cursor for the format step like other format commands. Co-authored-by: Cursor --- lua/utils/functions.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 70ef3cc..2e371e3 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -17,13 +17,11 @@ function FormatXML() if not require_tool.ensure("python3") then return end - local save_cursor = vim.fn.getpos(".") - vim.cmd([[ + cmdPreserveCursorPosition([[ silent! %s/\\"/"/g | silent! %s/\\n//g ]]) format.run({ "xml_minidom" }, "Format XML") - vim.fn.setpos(".", save_cursor) end function RemoveExtraEmptyLines() From 21aa2da6a7c52e2f3a16ff50d3821bf891124363 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 16:56:04 -0600 Subject: [PATCH 09/18] refactor: wrap FormatXML in with_tool for python3 check Co-authored-by: Cursor --- lua/utils/functions.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 2e371e3..ba0c646 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -14,14 +14,13 @@ function FormatCss() end function FormatXML() - if not require_tool.ensure("python3") then - return - end - cmdPreserveCursorPosition([[ - silent! %s/\\"/"/g | - silent! %s/\\n//g - ]]) - format.run({ "xml_minidom" }, "Format XML") + with_tool("python3", function() + cmdPreserveCursorPosition([[ + silent! %s/\\"/"/g | + silent! %s/\\n//g + ]]) + format.run({ "xml_minidom" }, "Format XML") + end) end function RemoveExtraEmptyLines() From 9544a5008d453e5787c022bea460c67066eb49c6 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 17:01:40 -0600 Subject: [PATCH 10/18] refactor: clarify RemoveExtraEmptyLines as blank-line collapse Replace cat -s with a Vim substitute that keeps one blank line between functions/blocks; update command description and behavior test. Co-authored-by: Cursor --- lua/config/autocmds.lua | 4 +++- lua/utils/functions.lua | 5 ++--- spec/README.md | 2 +- tests/utils_spec.lua | 12 +++++------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 3d6c393..8d8ecdc 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -12,7 +12,9 @@ vim.api.nvim_create_user_command("FormatJSON", FormatJSON, { desc = "Format json vim.api.nvim_create_user_command("FormatCss", FormatCss, { desc = "Format CSS/SCSS with Prettier" }) -vim.api.nvim_create_user_command("RemoveExtraEmptyLines", RemoveExtraEmptyLines, { desc = "Remove extra empty lines" }) +vim.api.nvim_create_user_command("RemoveExtraEmptyLines", RemoveExtraEmptyLines, { + desc = "Collapse multiple blank lines to one between functions/blocks", +}) vim.api.nvim_create_user_command("AddLineNumbers", AddLineNumbers, { desc = "Add number for the entire file" }) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index ba0c646..6457da0 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -23,10 +23,9 @@ function FormatXML() end) end +--- Collapse consecutive blank lines to a single blank line between blocks. function RemoveExtraEmptyLines() - with_tool("cat", function() - cmdPreserveCursorPosition([[%!cat -s]]) - end) + cmdPreserveCursorPosition([[silent! %s/\n\n\zs\n\+//ge]]) end function ConvertTabToSpaces() diff --git a/spec/README.md b/spec/README.md index b4b70a6..ce917b3 100644 --- a/spec/README.md +++ b/spec/README.md @@ -25,7 +25,7 @@ Format helpers that shell out to OS tools are covered by contract tests (mocked | `FormatSQL` / `FormatSQLFormatter` | `sqlformat`, `sql-formatter-cli` (via conform) | | `FormatCss` | `prettier` (via conform) | | `FormatXML` | `python3` (via conform) | -| `RemoveExtraEmptyLines` | `cat` | +| `RemoveExtraEmptyLines` | (built-in Vim substitute) | Install these locally when you use the commands. If a tool is missing, Neovim notifies you with the install command to run (e.g. `pip3 install sqlparse`). CI verifies both the install hints and the expected filter wiring. diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index ee7b258..c27c5cc 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -70,6 +70,11 @@ describe("utils.functions", function() assert.same({ "keep", "also" }, lines) end) + it("RemoveExtraEmptyLines keeps one blank line between blocks", function() + local lines = with_buffer({ "def foo", "", "", "end", "", "", "", "def bar" }, RemoveExtraEmptyLines) + assert.same({ "def foo", "", "end", "", "def bar" }, lines) + end) + it("DoubleQuotes converts single-quoted strings to double quotes", function() local lines = with_buffer({ "x = 'value'" }, DoubleQuotes) assert.same({ 'x = "value"' }, lines) @@ -156,13 +161,6 @@ describe("utils.functions", function() assert.matches("install with:", messages[1]) end) - it("RemoveExtraEmptyLines pipes the buffer through cat -s", function() - with_tool_and_preserve("cat", function() - RemoveExtraEmptyLines() - end) - assert.matches("cat %-s", captured_cmds[1]) - end) - it("FormatCss runs conform prettier formatter", function() local formatters_run package.loaded["utils.format"] = { From 0bb86863b8eabae19ae34775ae5276f770c3f4fc Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Wed, 24 Jun 2026 17:04:10 -0600 Subject: [PATCH 11/18] fix: RemoveExtraEmptyLines collapse across buffer lines Vim %s cannot match newlines between lines; rewrite in Lua and treat whitespace-only lines as blank so demo.xml-style spacing collapses. Co-authored-by: Cursor --- lua/utils/functions.lua | 22 ++++++++++++++++++++-- tests/utils_spec.lua | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 6457da0..d0b263a 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -23,9 +23,27 @@ function FormatXML() end) end ---- Collapse consecutive blank lines to a single blank line between blocks. +--- Collapse consecutive blank lines (including whitespace-only) to one empty line. function RemoveExtraEmptyLines() - cmdPreserveCursorPosition([[silent! %s/\n\n\zs\n\+//ge]]) + local save_cursor = vim.fn.getpos(".") + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local result = {} + local in_blank_run = false + + for _, line in ipairs(lines) do + if line:match("^%s*$") then + if not in_blank_run then + result[#result + 1] = "" + in_blank_run = true + end + else + in_blank_run = false + result[#result + 1] = line + end + end + + vim.api.nvim_buf_set_lines(0, 0, -1, true, result) + vim.fn.setpos(".", save_cursor) end function ConvertTabToSpaces() diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index c27c5cc..64274b5 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -75,6 +75,11 @@ describe("utils.functions", function() assert.same({ "def foo", "", "end", "", "def bar" }, lines) end) + it("RemoveExtraEmptyLines treats whitespace-only lines as blank", function() + local lines = with_buffer({ "", " ", " ", " ", " ", " <author/>" }, RemoveExtraEmptyLines) + assert.same({ "<book>", "", " <title/>", "", " <author/>" }, lines) + end) + it("DoubleQuotes converts single-quoted strings to double quotes", function() local lines = with_buffer({ "x = 'value'" }, DoubleQuotes) assert.same({ 'x = "value"' }, lines) From 3d3c5ac22565c77925402e5076542d8cb855cb2e Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza <alejandro.espinoza@fabrichealth.com> Date: Wed, 24 Jun 2026 17:06:01 -0600 Subject: [PATCH 12/18] fix: RemoveEmptyLines without g/^$/ pattern-not-found error Rewrite in Lua like RemoveExtraEmptyLines; treat whitespace-only lines as blank so demo.xml and similar files do not error. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/config/autocmds.lua | 4 +++- lua/utils/functions.lua | 14 +++++++++++++- tests/utils_spec.lua | 5 +++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 8d8ecdc..07951d6 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -20,7 +20,9 @@ vim.api.nvim_create_user_command("AddLineNumbers", AddLineNumbers, { desc = "Add vim.api.nvim_create_user_command("ConvertTabToSpaces", ConvertTabToSpaces, { desc = "Convert Tab to spaces" }) -vim.api.nvim_create_user_command("RemoveEmptyLines", RemoveEmptyLines, { desc = "Remove Empty Lines" }) +vim.api.nvim_create_user_command("RemoveEmptyLines", RemoveEmptyLines, { + desc = "Remove all blank lines (including whitespace-only)", +}) vim.api.nvim_create_user_command("FormatSQL", FormatSQL, { desc = "Format SQL with sqlformat (sqlparse)" }) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index d0b263a..41513c7 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -50,8 +50,20 @@ function ConvertTabToSpaces() cmdPreserveCursorPosition([[%s/\t/ /g]]) end +--- Remove all blank lines (including whitespace-only). function RemoveEmptyLines() - cmdPreserveCursorPosition([[g/^$/d]]) + local save_cursor = vim.fn.getpos(".") + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local result = {} + + for _, line in ipairs(lines) do + if not line:match("^%s*$") then + result[#result + 1] = line + end + end + + vim.api.nvim_buf_set_lines(0, 0, -1, true, result) + vim.fn.setpos(".", save_cursor) end function FormatSQL() diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 64274b5..8581a31 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -70,6 +70,11 @@ describe("utils.functions", function() assert.same({ "keep", "also" }, lines) end) + it("RemoveEmptyLines deletes whitespace-only lines", function() + local lines = with_buffer({ "keep", " ", "also" }, RemoveEmptyLines) + assert.same({ "keep", "also" }, lines) + end) + it("RemoveExtraEmptyLines keeps one blank line between blocks", function() local lines = with_buffer({ "def foo", "", "", "end", "", "", "", "def bar" }, RemoveExtraEmptyLines) assert.same({ "def foo", "", "end", "", "def bar" }, lines) From ed0a5ef2efbe171cb4f4b833e5faac2a9946a5df Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza <alejandro.espinoza@fabrichealth.com> Date: Wed, 24 Jun 2026 17:25:54 -0600 Subject: [PATCH 13/18] docs: document FormatHashes and add FormatHashes command alias Clarify what UpdateRubyHashesByLines does (syntax normalize, split }, {, single quotes, re-indent) without changing the original behavior. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/config/autocmds.lua | 7 ++++++- lua/utils/functions.lua | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 07951d6..1613884 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -80,7 +80,12 @@ vim.api.nvim_create_user_command( { desc = "Replace double quotes with single quotes with confirmation" } ) -vim.api.nvim_create_user_command("UpdateRubyHashesByLines", FormatHashes, { desc = "Update ruby hashes by lines" }) +vim.api.nvim_create_user_command("UpdateRubyHashesByLines", FormatHashes, { + desc = "Normalize Ruby hashes: rocket→new syntax, split }, {, single quotes, re-indent", +}) +vim.api.nvim_create_user_command("FormatHashes", FormatHashes, { + desc = "Alias for UpdateRubyHashesByLines", +}) vim.api.nvim_create_user_command("HashNewSyntax", HashNewSyntax, { desc = "update hash old to new syntax" }) vim.api.nvim_create_user_command("HashOldSyntax", HashOldSyntax, { desc = "update hash new to old syntax" }) diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 41513c7..0947210 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -86,6 +86,15 @@ function SingleQuotes() cmdPreserveCursorPosition([[%s/"\([^"]*\)"/'\1'/g]]) end +--- Normalize multi-line Ruby hashes in the current buffer. +--- +--- Steps (order preserved; no sorting or column alignment): +--- 1. `:key =>` → `key: ` (old rocket syntax to new, with space after `:`) +--- 2. `}, {` → `},\n {` (split two inline hashes onto separate lines) +--- 3. `"…"` → `'…'` (double quotes to single quotes) +--- 4. `ggVG=` — re-indent the buffer (Ruby indent when `filetype=ruby`) +--- +--- Commands: `:UpdateRubyHashesByLines`, `:FormatHashes` (alias). function FormatHashes() local save_cursor = vim.fn.getpos(".") vim.cmd([[ From 7f394d8adbf137288fb7a7d48edab0f6216652a8 Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza <alejandro.espinoza@fabrichealth.com> Date: Wed, 24 Jun 2026 17:32:54 -0600 Subject: [PATCH 14/18] refactor: preserve_cursor accepts Vim cmds and Lua callbacks Extend cmdPreservePosition to wrap functions as well as :commands and use it throughout functions.lua. Add specs for AddLineNumbers, FormatHashes, FormatJSON, and FormatSQLFormatter. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/utils/cmdPreservePosition.lua | 27 ++++++-- lua/utils/functions.lua | 88 ++++++++++++------------- tests/utils_spec.lua | 103 +++++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 52 deletions(-) diff --git a/lua/utils/cmdPreservePosition.lua b/lua/utils/cmdPreservePosition.lua index 6a28694..3610288 100644 --- a/lua/utils/cmdPreservePosition.lua +++ b/lua/utils/cmdPreservePosition.lua @@ -1,7 +1,24 @@ -local function cmdPreserveCursorPosition(cmd) - local save_cursor = vim.fn.getpos(".") - vim.cmd(cmd) - vim.fn.setpos(".", save_cursor) +local M = {} + +--- Run a Vim command or Lua function, then restore the cursor position. +---@param action string|function +---@param ... any Arguments passed when action is a function +---@return ... Return values from action when it is a function +function M.with_cursor(action, ...) + local pos = vim.fn.getpos(".") + if type(action) == "function" then + local ret = { action(...) } + vim.fn.setpos(".", pos) + return unpack(ret, 1, ret.n) + end + vim.cmd(action) + vim.fn.setpos(".", pos) end -return cmdPreserveCursorPosition +setmetatable(M, { + __call = function(_, action, ...) + return M.with_cursor(action, ...) + end, +}) + +return M diff --git a/lua/utils/functions.lua b/lua/utils/functions.lua index 0947210..b73ecc2 100644 --- a/lua/utils/functions.lua +++ b/lua/utils/functions.lua @@ -1,4 +1,4 @@ -local cmdPreserveCursorPosition = require("utils.cmdPreservePosition") +local preserve_cursor = require("utils.cmdPreservePosition") local require_tool = require("utils.require_tool") local format = require("utils.format") @@ -15,7 +15,7 @@ end function FormatXML() with_tool("python3", function() - cmdPreserveCursorPosition([[ + preserve_cursor([[ silent! %s/\\"/"/g | silent! %s/\\n//g ]]) @@ -25,45 +25,45 @@ end --- Collapse consecutive blank lines (including whitespace-only) to one empty line. function RemoveExtraEmptyLines() - local save_cursor = vim.fn.getpos(".") - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) - local result = {} - local in_blank_run = false - - for _, line in ipairs(lines) do - if line:match("^%s*$") then - if not in_blank_run then - result[#result + 1] = "" - in_blank_run = true + preserve_cursor(function() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local result = {} + local in_blank_run = false + + for _, line in ipairs(lines) do + if line:match("^%s*$") then + if not in_blank_run then + result[#result + 1] = "" + in_blank_run = true + end + else + in_blank_run = false + result[#result + 1] = line end - else - in_blank_run = false - result[#result + 1] = line end - end - vim.api.nvim_buf_set_lines(0, 0, -1, true, result) - vim.fn.setpos(".", save_cursor) + vim.api.nvim_buf_set_lines(0, 0, -1, true, result) + end) end function ConvertTabToSpaces() - cmdPreserveCursorPosition([[%s/\t/ /g]]) + preserve_cursor([[%s/\t/ /g]]) end --- Remove all blank lines (including whitespace-only). function RemoveEmptyLines() - local save_cursor = vim.fn.getpos(".") - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) - local result = {} + preserve_cursor(function() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local result = {} - for _, line in ipairs(lines) do - if not line:match("^%s*$") then - result[#result + 1] = line + for _, line in ipairs(lines) do + if not line:match("^%s*$") then + result[#result + 1] = line + end end - end - vim.api.nvim_buf_set_lines(0, 0, -1, true, result) - vim.fn.setpos(".", save_cursor) + vim.api.nvim_buf_set_lines(0, 0, -1, true, result) + end) end function FormatSQL() @@ -79,11 +79,11 @@ function FormatSQLFormatter() end function DoubleQuotes() - cmdPreserveCursorPosition([[%s/'\([^']*\)'/"\1"/g]]) + preserve_cursor([[%s/'\([^']*\)'/"\1"/g]]) end function SingleQuotes() - cmdPreserveCursorPosition([[%s/"\([^"]*\)"/'\1'/g]]) + preserve_cursor([[%s/"\([^"]*\)"/'\1'/g]]) end --- Normalize multi-line Ruby hashes in the current buffer. @@ -96,36 +96,36 @@ end --- --- Commands: `:UpdateRubyHashesByLines`, `:FormatHashes` (alias). function FormatHashes() - local save_cursor = vim.fn.getpos(".") - vim.cmd([[ - silent! %s/:\([^ ]*\)\(\s*\)=>/\1: /g | - silent! %s/}, {/},\r {/g | - silent! %s/"\([^"]*\)"/'\1'/g - ]]) - vim.cmd.normal(vim.api.nvim_replace_termcodes("gg<S-v><S-g>=", true, true, true)) - vim.fn.setpos(".", save_cursor) + preserve_cursor(function() + vim.cmd([[ + silent! %s/:\([^ ]*\)\(\s*\)=>/\1: /g | + silent! %s/}, {/},\r {/g | + silent! %s/"\([^"]*\)"/'\1'/g + ]]) + vim.cmd.normal(vim.api.nvim_replace_termcodes("gg<S-v><S-g>=", true, true, true)) + end) end function HashNewSyntax() - cmdPreserveCursorPosition([[:%s/:\([^ ]*\)\(\s*\)=>/\1:/g]]) + preserve_cursor([[:%s/:\([^ ]*\)\(\s*\)=>/\1:/g]]) end function HashOldSyntax() - cmdPreserveCursorPosition([[:%s/\(\w*\): \([':]\)/:\1 => \2/g]]) + preserve_cursor([[:%s/\(\w*\): \([':]\)/:\1 => \2/g]]) end function UnscapeDoubleQuotes() - cmdPreserveCursorPosition([[%s/\\"//g]]) + preserve_cursor([[%s/\\"//g]]) end function RemoveLineBreak() - cmdPreserveCursorPosition([[%s/\\n//g]]) + preserve_cursor([[%s/\\n//g]]) end function CleanWhiteSpaces() - cmdPreserveCursorPosition([[%s/\s\+$//e]]) + preserve_cursor([[%s/\s\+$//e]]) end function AddLineNumbers() - cmdPreserveCursorPosition([[%s/^/\=printf('%-2d', line('.'))]]) + preserve_cursor([[%s/^/\=printf('%-2d', line('.'))]]) end diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 8581a31..df51272 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -10,6 +10,18 @@ describe("utils.cmdPreservePosition", function() assert.same({ 2, 0 }, vim.api.nvim_win_get_cursor(0)) end) + + it("restores the cursor after running a function", function() + vim.api.nvim_buf_set_lines(0, 0, -1, true, { "one", "two", "three" }) + vim.api.nvim_win_set_cursor(0, { 2, 0 }) + + local preserve = require("utils.cmdPreservePosition") + preserve(function() + vim.api.nvim_buf_set_lines(0, 1, 2, true, { "TWO" }) + end) + + assert.same({ 2, 0 }, vim.api.nvim_win_get_cursor(0)) + end) end) describe("utils.map", function() @@ -120,6 +132,32 @@ describe("utils.functions", function() assert.same({ ":foo => 'bar'" }, lines) end) + it("AddLineNumbers prefixes each line with its line number", function() + local lines = with_buffer({ "first", "second" }, AddLineNumbers) + assert.same({ "1 first", "2 second" }, lines) + end) + + it("FormatHashes converts rocket syntax and double quotes", function() + local lines = with_buffer({ + " :zebra => 1,", + ' :apple => "foo",', + }, FormatHashes) + + local text = table.concat(lines, "\n") + assert.not_matches("=>", text) + assert.not_matches('"foo"', text) + assert.matches("zebra:%s+1,", text) + assert.matches("apple:%s+'foo',", text) + end) + + it("FormatHashes splits }, { onto separate lines", function() + local lines = with_buffer({ "{ :b => 2 }, { :a => 1 }" }, FormatHashes) + + assert.equals(2, #lines) + assert.matches("{ b:%s+2 },", lines[1]) + assert.matches("{ a:%s+1 }", lines[2]) + end) + it("preserves the cursor when transforming buffer text", function() vim.api.nvim_buf_set_lines(0, 0, -1, true, { " :foo => 1", "unchanged" }) vim.api.nvim_win_set_cursor(0, { 2, 0 }) @@ -134,9 +172,20 @@ describe("utils.functions", function() local function with_tool_and_preserve(tool_bin, fn) captured_cmds = {} - package.loaded["utils.cmdPreservePosition"] = function(cmd) - captured_cmds[#captured_cmds + 1] = cmd - end + package.loaded["utils.cmdPreservePosition"] = setmetatable({ + with_cursor = function(action, ...) + if type(action) == "string" then + captured_cmds[#captured_cmds + 1] = action + end + if type(action) == "function" then + action(...) + end + end, + }, { + __call = function(_, action, ...) + return package.loaded["utils.cmdPreservePosition"].with_cursor(action, ...) + end, + }) helpers.reload_module("utils.require_tool") helpers.reload_module("utils.functions") helpers.with_mocked_fn({ @@ -195,6 +244,54 @@ describe("utils.functions", function() assert.same({ "prettier" }, formatters_run) end) + it("FormatSQLFormatter runs conform sql_formatter_cli formatter", function() + local formatters_run + package.loaded["utils.format"] = { + run = function(names, label) + formatters_run = names + assert.equals("Format SQL", label) + return true + end, + } + + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + helpers.reload_module("utils.functions") + require("utils.functions") + FormatSQLFormatter() + end) + + package.loaded["utils.format"] = nil + assert.same({ "sql_formatter_cli" }, formatters_run) + end) + + it("FormatJSON runs conform json_tool formatter", function() + local formatters_run + package.loaded["utils.format"] = { + run = function(names, label) + formatters_run = names + assert.equals("Format JSON", label) + return true + end, + } + + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + helpers.reload_module("utils.functions") + require("utils.functions") + FormatJSON() + end) + + package.loaded["utils.format"] = nil + assert.same({ "json_tool" }, formatters_run) + end) + it("FormatXML preprocesses escapes then runs conform xml formatter", function() local formatters_run package.loaded["utils.format"] = { From c62959a708b395ca635ba1b5ec9a9291dc85144d Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza <alejandro.espinoza@fabrichealth.com> Date: Thu, 25 Jun 2026 08:37:56 -0600 Subject: [PATCH 15/18] fix: read conform format errors from callback, not return value conform.format returns true when formatters run; errors come via the callback. Fixes false "Format JSON: true" notifications for all commands using utils.format.run. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/utils/format.lua | 11 +++++++---- tests/format_spec.lua | 14 ++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lua/utils/format.lua b/lua/utils/format.lua index 23cb3ac..5fa9e32 100644 --- a/lua/utils/format.lua +++ b/lua/utils/format.lua @@ -28,15 +28,18 @@ function M.run(formatters, label) return false end - local err = conform.format({ + local format_err + conform.format({ async = false, timeout_ms = 5000, lsp_format = "never", formatters = formatters, - }) + }, function(err) + format_err = err + end) - if err then - vim.notify(("%s: %s"):format(label or "Format", err), vim.log.levels.ERROR) + if format_err then + vim.notify(("%s: %s"):format(label or "Format", format_err), vim.log.levels.ERROR) return false end diff --git a/tests/format_spec.lua b/tests/format_spec.lua index 676372f..a9ba675 100644 --- a/tests/format_spec.lua +++ b/tests/format_spec.lua @@ -28,9 +28,12 @@ describe("utils.format", function() it("calls conform.format with the requested formatters", function() local called package.loaded["conform"] = { - format = function(opts) + format = function(opts, callback) called = opts - return nil + if callback then + callback(nil, true) + end + return true end, } @@ -49,8 +52,11 @@ describe("utils.format", function() it("notifies when conform reports an error", function() package.loaded["conform"] = { - format = function() - return "formatter failed" + format = function(_, callback) + if callback then + callback("formatter failed", false) + end + return true end, } From 21da64f55bc372c35d994d6ec48ae90cb9c779d4 Mon Sep 17 00:00:00 2001 From: alexesba <alexesba@gmail.com> Date: Tue, 14 Jul 2026 09:05:19 -0600 Subject: [PATCH 16/18] feat: map Ctrl-j/Ctrl-k for blink.cmp suggestion navigation Keep LazyVim's enter preset (arrows, C-n/C-p) and add Vim-friendlier completion movement keys. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/plugins/blink.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lua/plugins/blink.lua diff --git a/lua/plugins/blink.lua b/lua/plugins/blink.lua new file mode 100644 index 0000000..1e49d6f --- /dev/null +++ b/lua/plugins/blink.lua @@ -0,0 +1,12 @@ +return { + { + "saghen/blink.cmp", + opts = { + keymap = { + -- Keep LazyVim preset ("enter"): <Up>/<Down>, <C-n>/<C-p>, <CR>, etc. + ["<C-j>"] = { "select_next", "fallback" }, + ["<C-k>"] = { "select_prev", "fallback" }, + }, + }, + }, +} From 9e8e59742bed2c5a740ed38e331345950548ac2b Mon Sep 17 00:00:00 2001 From: alexesba <alexesba@gmail.com> Date: Tue, 14 Jul 2026 09:06:05 -0600 Subject: [PATCH 17/18] chore: track LazyVim sidekick extra in lazyvim.json Co-authored-by: Cursor <cursoragent@cursor.com> --- lazyvim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/lazyvim.json b/lazyvim.json index 1ca8e66..13b143a 100644 --- a/lazyvim.json +++ b/lazyvim.json @@ -2,6 +2,7 @@ "extras": [ "lazyvim.plugins.extras.ai.copilot", "lazyvim.plugins.extras.ai.copilot-chat", + "lazyvim.plugins.extras.ai.sidekick", "lazyvim.plugins.extras.coding.mini-surround" ], "install_version": 8, From e6af176ba6b4be96006b02a5fa18c789119c74d7 Mon Sep 17 00:00:00 2001 From: alexesba <alexesba@gmail.com> Date: Tue, 14 Jul 2026 09:12:39 -0600 Subject: [PATCH 18/18] fix: allow conform format when buffer is nomodifiable Temporarily enable modifiable for normal file buffers, refuse readonly and special buftypes with clear errors, and pcall format to avoid stack dumps. Co-authored-by: Cursor <cursoragent@cursor.com> --- lua/utils/format.lua | 65 +++++++++++++++++++++++++++++++++++++------ tests/format_spec.lua | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/lua/utils/format.lua b/lua/utils/format.lua index 5fa9e32..ff4eb1c 100644 --- a/lua/utils/format.lua +++ b/lua/utils/format.lua @@ -8,11 +8,44 @@ local TOOL_BY_FORMATTER = { prettier = "prettier", } +--- Ensure the current buffer can receive formatter edits. +---@param label string +---@return boolean, table|nil restore options to apply after formatting +local function prepare_buffer(label) + local bufnr = vim.api.nvim_get_current_buf() + local buftype = vim.bo[bufnr].buftype + local readonly = vim.bo[bufnr].readonly + local modifiable = vim.bo[bufnr].modifiable + + if buftype ~= "" then + vim.notify( + ("%s: buffer is not a normal file (buftype=%s)"):format(label, buftype), + vim.log.levels.ERROR + ) + return false + end + + if readonly then + vim.notify(("%s: buffer is readonly"):format(label), vim.log.levels.ERROR) + return false + end + + if modifiable then + return true, nil + end + + -- Some plugins leave 'nomodifiable' on an otherwise editable file buffer. + -- Conform applies edits via nvim_buf_set_text, which requires modifiable. + vim.bo[bufnr].modifiable = true + return true, { bufnr = bufnr, modifiable = false } +end + --- Run conform formatters by name (used by :FormatSQL, :FormatJSON, etc.). ---@param formatters string[] ---@param label? string ---@return boolean function M.run(formatters, label) + label = label or "Format" local require_tool = require("utils.require_tool") for _, name in ipairs(formatters) do @@ -28,18 +61,34 @@ function M.run(formatters, label) return false end + local prepared, restore = prepare_buffer(label) + if not prepared then + return false + end + local format_err - conform.format({ - async = false, - timeout_ms = 5000, - lsp_format = "never", - formatters = formatters, - }, function(err) - format_err = err + local ran, err = pcall(function() + conform.format({ + async = false, + timeout_ms = 5000, + lsp_format = "never", + formatters = formatters, + }, function(cb_err) + format_err = cb_err + end) end) + if restore then + vim.bo[restore.bufnr].modifiable = restore.modifiable + end + + if not ran then + vim.notify(("%s: %s"):format(label, err), vim.log.levels.ERROR) + return false + end + if format_err then - vim.notify(("%s: %s"):format(label or "Format", format_err), vim.log.levels.ERROR) + vim.notify(("%s: %s"):format(label, format_err), vim.log.levels.ERROR) return false end diff --git a/tests/format_spec.lua b/tests/format_spec.lua index a9ba675..1891e5b 100644 --- a/tests/format_spec.lua +++ b/tests/format_spec.lua @@ -8,6 +8,9 @@ describe("utils.format", function() helpers.reload_module("utils.require_tool") helpers.reload_module("utils.format") format = require("utils.format") + vim.bo.buftype = "" + vim.bo.modifiable = true + vim.bo.readonly = false end) it("warns when a required tool is missing", function() @@ -72,4 +75,54 @@ describe("utils.format", function() assert.matches("Format JSON: formatter failed", messages[1]) end) + + it("refuses to format special buftype buffers", function() + vim.bo.buftype = "nofile" + + package.loaded["conform"] = { + format = function() + error("should not format") + end, + } + + local messages = helpers.capture_notify(function() + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + assert.is_false(format.run({ "sql_formatter_cli" }, "Format SQL")) + end) + end) + + vim.bo.buftype = "" + assert.matches("buftype=nofile", messages[1]) + end) + + it("temporarily enables modifiable when needed", function() + vim.bo.buftype = "" + vim.bo.modifiable = false + + local saw_modifiable + package.loaded["conform"] = { + format = function(_, callback) + saw_modifiable = vim.bo.modifiable + if callback then + callback(nil, true) + end + return true + end, + } + + helpers.with_mocked_fn({ + executable = function() + return 1 + end, + }, function() + assert.is_true(format.run({ "sql_formatter_cli" }, "Format SQL")) + end) + + assert.is_true(saw_modifiable) + assert.is_false(vim.bo.modifiable) + end) end)