ACH-NEOVIM is my Neovim configuration for macOS. Neovim is a text editor that runs inside a terminal and is driven from the keyboard, and a configuration is the set of files that tells it which plugins to load, what each key does and how each language is handled. I wrote this one from scratch on lazy.nvim, the plugin manager, and it lives at github.com/anirbanchakraborty-dev/ACH-NEOVIM under the MIT licence.
Why I built it. I wanted one editor that already knows every language I work in: C and C++, Python, Rust, SystemVerilog and Verilog, VHDL, LaTeX and Markdown, with the rest of about ninety languages there when I need them. I did not want it to fill my disk with tools for languages I never open, so nothing for a language installs until I open a file in it. I wanted it to start fast, too. It starts within 10% of stock LazyVim's time, and its own test suite fails any change that makes it take more than one and a half times as long to start as it did when I first timed it. LazyVim showed me what a well-made Neovim setup looks like. I took its ideas and built my own, small enough that I can read every line of it and keep it working, and the few files that adapt a LazyVim function say so in their first lines and name LazyVim's Apache-2.0 licence.
Who it is for. It is for someone on a Mac who writes code and technical documents in many languages, works from the keyboard, and would rather set things up properly once than tinker forever.
My favourite part. I never install a language's tools by hand. I open a file in a language I have not touched in months, and while I read the first screen, its highlighting, completion and formatter install in the background and switch on. A close second is having Claude Code right inside the editor, on Space a c, looking at the same file I am.
Installing takes one Homebrew session, one clone and one first start. Every command below is typed in a terminal, in the order given.
Install Homebrew. Homebrew is the Mac's package manager, and everything else comes from it. If brew --version prints nothing, follow the one command on brew.sh.
Install the programs. Neovim itself, git, ripgrep and fd for searching, lazygit for git, the tree-sitter command-line program, ImageMagick for pictures, Ghostty, and the font:
ACH-NEOVIM needs Neovim 0.12.4 or later, and nvim --version prints the version on its first line. The tree-sitter program builds each language's parser, the part that reads its syntax, with the compiler from Apple's Command Line Tools, which is what the last line installs.
Back up what you have. The clone goes exactly where a Neovim configuration lives, so move yours aside first, with the folders Neovim keeps its data, its state and its cache in:
Bash
mv ~/.config/nvim ~/.config/nvim.bak
mv ~/.local/share/nvim ~/.local/share/nvim.bak
mv ~/.local/state/nvim ~/.local/state/nvim.bak
mv ~/.cache/nvim ~/.cache/nvim.bak
A line whose folder does not exist only prints an error, so run all four.
Link Ghostty's settings. The repository carries Ghostty's settings in ghostty/config.ghostty: the font at 15 points, the navy background, the left Option key working as Alt, and the sixteen colours Neovim's own terminal uses. Link the file to where Ghostty reads it:
The first start installs every plugin at the commit pinned in lazy-lock.json, so you get the versions I test. Let it finish before you open anything.
Check what is missing. Inside Neovim, type this and press Enter:
Plain Text
:checkhealth ach
Read what it says. It lists each program the configuration uses, installed or not, and gives each missing one the single command that installs it.
Open a file. The first time you open a file in a language, its parser, its language server, its formatter and its linter install by themselves in the background, without asking, and switch on when they are ready. A language that needs a compiler or runtime only Homebrew provides, such as Haskell's GHC or Julia, asks first, once, in one line that names it and its rough size. Say yes, and Homebrew installs it in the background while you keep editing. Say no, and it does not ask again until you next start Neovim.
Add the programs some keys use. A key whose program is missing prints the line that installs it instead of running, and :checkhealth ach lists the same programs:
Program
What it is for
Install
lazygit
Lazygit on Space g g and Space g G
brew install lazygit
gh
GitHub issues and pull requests on Space g i and Space g p
brew install gh, then gh auth login
claude
the Claude Code panel on Space a
brew install --cask claude-code
latexmk
building LaTeX documents on Space c b
brew install --cask mactex
displayline
showing LaTeX PDFs in Skim and jumping between them and the source
brew install --cask skim
magick
turning pictures and formulas into images the terminal can show
brew install imagemagick
pdflatex
typesetting the formulas in Markdown files
brew install --cask mactex
gs
letting ImageMagick read the PDF a formula is typeset into
brew install ghostscript
pdftotext
reading Intel's manual to find each instruction's page
Here is what ACH-NEOVIM does beyond editing text, area by area. Each area's keys are in the Keymaps section below.
Git. Change bars in the margin show what changed since the last commit, and ] h and [ h move between the changes. Lazygit opens on Space g g. Neogit, git's status page inside Neovim, opens on Space g n, and diffview shows every uncommitted change side by side on Space g D. GitHub's issues and pull requests are on Space g i and Space g p, through gh. A git key pressed on a file outside every repository says so and opens nothing, and a link copied to the current line never carries a user name or a token from the remote's address.
Claude Code.Space a c opens Claude Code in a panel on the right, 40 percent of the screen wide. It sees the file and the selection I am on, and a change it proposes opens side by side, to accept with Space a a or reject with Space a d. Its server listens only on this Mac, refuses a connection without the token in its lock file, and never starts when Neovim has no window.
LaTeX. VimTeX builds the document on Space c b, with latexmk or with a command the project sets, and shows the PDF in Skim, and I can jump from a line of the source to its place in the PDF and back. The build's own files are kept outside the repository.
Markdown. Headings, lists and tables are drawn in place while I write, formulas and pictures are drawn as images inside the editor, and Space c p opens a preview in the browser that keeps up with the file as it changes. The preview's server listens only on this Mac and serves nothing outside the folder it was started for.
Intel's manual.Space c x opens Intel's x86 manual in Skim at the page for the instruction under the cursor, and on any other word lists every instruction to pick one. The manual is Intel's to publish, so it is not in the repository.
News.Space N opens newsboat, a feed reader, in a window floating over the editor.
Ghostty. The repository carries Ghostty's settings: JetBrains Mono at 15 points, the same navy background as the editor, the left Option key working as Alt, and the sixteen colours Neovim's own terminal uses.
Startup. It starts within 10% of stock LazyVim's time, the two timed side by side over thirty starts each, and the test suite fails any change that makes it take more than one and a half times as long to start as it did when first timed.
Updates.scripts/update updates every plugin and Mason package, runs the full test suite, and commits both lock files when it passes, or puts everything back when it does not.
Upgrade check. The first time a new Neovim release starts on a screen, the fast test suite runs in the background, and the release is recorded once it passes.
Key card.docs/key-card.md is one page of keys to keep beside the keyboard, and a test fails whenever it disagrees with the keys the configuration sets.
This page.scripts/docs writes this page from the configuration's own source, so a plugin, a key or a language added to the configuration shows here the next time it runs.
Every key this configuration sets, with the description the menus show for it and the modes it works in, grouped the way the key card in docs/key-card.md groups them. A key is written the way it is pressed: Space f f is Space, then f, then f, and Ctrl-h is h pressed while Ctrl is held.
The Mode column uses Neovim's one-letter names. n is normal mode, where keys move and edit. x is visual mode, after selecting text with v. s is select mode, which snippets use for the parts to fill in. o is operator-pending mode, after an operator such as d and before the motion that says what it acts on. i is insert mode, for typing. c is the command line, after :. t is a terminal window.
Inside Neovim each key also shows an icon. This page leaves the icons out, because they come from the JetBrains Mono Nerd Font and this website does not load that font, so each would draw as an empty box. Keys a plugin sets only inside its own window, and text objects such as i ( in d i (, are not listed here. Inside Neovim, Space s k searches every key, those included.
Editor options live in lua/ach/options.lua, keys that belong to no plugin in lua/ach/keymaps.lua, and autocommands, the small actions Neovim runs when something happens, such as opening a file or saving one, in lua/ach/autocmds.lua. Keys that belong to a plugin live in that plugin's spec, under keys.
A plugin is a file in lua/ach/plugins/ that returns its spec, because lazy.nvim imports every file in that folder. The Full Spec tabs in the Plugins section below are real specs to copy from. Then:
Give every key a desc, and give that description a line in M.keys in lua/ach/icons.lua. The test suite fails a key without one, since that line is what draws its icon in the menus.
Give the plugin an entry in lua/ach/docs.lua, with its category and one sentence saying what it does. The test suite fails a plugin in lazy-lock.json without one.
Start Neovim once, so lazy.nvim installs the plugin and records its commit in lazy-lock.json.
If the plugin adds keys, run scripts/check --keymaps. Then run scripts/check, and commit the new file, lazy-lock.json, lua/ach/icons.lua, lua/ach/docs.lua and docs/site/keymaps.json together.
Every language is one entry in M.languages in lua/ach/languages.lua, under its name. The entry lists the file types it covers in ft, the Tree-sitter parsers in ts, the language servers in lsp, the formatters in fmt, the linters in lint, any other Mason packages it needs in mason, and harper = true when Harper should check its grammar. The Entry tab of each language in the Languages section below is that language's real entry, and CMake's uses every one of these fields. Settings for a server go in M.settings in lua/ach/servers.lua, under the server's name, and show in the language's Options tab.
Then run scripts/check, and scripts/check --full, which installs the new language's tools, tests them, adds their versions to mason-lock.json and rewrites the list of languages in docs/languages.md. Commit all of it together.
Neovim reads a file called .nvim.lua in the folder it starts in, because ACH-NEOVIM turns on Neovim's exrc option. Neovim asks before it runs such a file the first time, and again whenever the file changes, so a repository you clone cannot run code on you just by shipping one. A LaTeX project can use it to give Space c b its own build command:
Lua
require("ach.latex").build_with(function(main)
return{"make", "pdf" }, vim.fs.dirname(main)
end)
The function receives the main file's path and returns the command and the folder to run it in. Returning nothing falls back to latexmk.
lazy-lock.json holds the commit of every plugin and mason-lock.json the version of every Mason package, so two Macs on the same commit of this repository run the same code.
scripts/update --list shows what an update would move: the new commits of each plugin and the newer release of each Mason package. It changes nothing.
scripts/update updates every plugin and Mason package, runs scripts/check --full, and commits both lock files when it passes. When it fails, it puts the lock files back and makes what is installed match them again.
scripts/update --restore does only that second part. Run it on a second Mac after a git pull brings new lock files.
Inside Neovim, :AchMasonUpdate updates every installed Mason package and moves its pin, and :AchMasonLock pins a package installed by hand from the :Mason window.
Every plugin in lazy-lock.json, grouped by what it does, each with a link to its repository. The Options tab shows opts, the settings lazy.nvim hands to the plugin, and the Full Spec tab shows the whole spec: where the plugin comes from, when it loads, its keys and its options. Either one can be copied into a file in lua/ach/plugins/.
Some plugins are set up in pieces, one in each file that needs a part of them, and each piece is shown with the file it lives in. A plugin that only comes in for another plugin's sake shows the line that brings it in.
opts ={keymap ={ preset = "enter" },-- The kind icons are the icon database's, bare, since blink adds its own spacing.appearance ={ nerd_font_variant = "mono", kind_icons = require("ach.icons").kinds},completion ={accept ={ auto_brackets ={ enabled = true}},documentation ={ auto_show = true, auto_show_delay_ms = 200},ghost_text ={ enabled = true},menu ={ draw ={ treesitter ={"lsp" }}},},signature ={ enabled = true},sources ={default ={"lsp", "path", "snippets", "buffer" },providers ={-- The repository's own folder, so the snippets are found through the link and under nvim -u alike.snippets ={ opts ={ search_paths ={ vim.g.ach_root .. "/snippets" }}},},},fuzzy ={ implementation = "prefer_rust_with_warning" },cmdline ={ enabled = true},}
Full Spec
Lua
-- The completion menu, fed by the language server, file paths, snippets and words in open buffers.{"saghen/blink.cmp",-- Releases carry the compiled matcher; scripts/fetch-matcher downloads the one for this tag.version = "1.*",event ={"InsertEnter", "CmdlineEnter" },dependencies ={"rafamadriz/friendly-snippets" },opts_extend ={"sources.default" },opts ={keymap ={ preset = "enter" },-- The kind icons are the icon database's, bare, since blink adds its own spacing.appearance ={ nerd_font_variant = "mono", kind_icons = require("ach.icons").kinds},completion ={accept ={ auto_brackets ={ enabled = true}},documentation ={ auto_show = true, auto_show_delay_ms = 200},ghost_text ={ enabled = true},menu ={ draw ={ treesitter ={"lsp" }}},},signature ={ enabled = true},sources ={default ={"lsp", "path", "snippets", "buffer" },providers ={-- The repository's own folder, so the snippets are found through the link and under nvim -u alike.snippets ={ opts ={ search_paths ={ vim.g.ach_root .. "/snippets" }}},},},fuzzy ={ implementation = "prefer_rust_with_warning" },cmdline ={ enabled = true},},}
opts = function()local ai = require("mini.ai")return{-- Look this far for an object, so a long function still counts as one.n_lines = 500,custom_textobjects ={o = ai.gen_spec.treesitter({a ={"@block.outer", "@conditional.outer", "@loop.outer" },i ={"@block.inner", "@conditional.inner", "@loop.inner" },}),f = ai.gen_spec.treesitter({ a = "@function.outer", i = "@function.inner" }),c = ai.gen_spec.treesitter({ a = "@class.outer", i = "@class.inner" }),u = ai.gen_spec.function_call(),},}end
Full Spec
Lua
-- Text objects: af and if for a function, ac and ic for a class, ao and io for a block,-- au and iu for a call, on top of mini.ai's brackets, quotes and arguments.{"nvim-mini/mini.ai",event = "VeryLazy",opts = function()local ai = require("mini.ai")return{-- Look this far for an object, so a long function still counts as one.n_lines = 500,custom_textobjects ={o = ai.gen_spec.treesitter({a ={"@block.outer", "@conditional.outer", "@loop.outer" },i ={"@block.inner", "@conditional.inner", "@loop.inner" },}),f = ai.gen_spec.treesitter({ a = "@function.outer", i = "@function.inner" }),c = ai.gen_spec.treesitter({ a = "@class.outer", i = "@class.inner" }),u = ai.gen_spec.function_call(),},}end,}
-- Project-wide search and replace in a buffer you edit like any other.{"MagicDuck/grug-far.nvim",cmd = "GrugFar",opts ={ headerMaxWidth = 80},keys ={{"<leader>sr",function()local ext = vim.bo.buftype == "" and vim.fn.expand("%:e")require("grug-far").open({transient = true,prefills ={ filesFilter = ext and ext ~= "" and "*." .. ext or nil},})end,mode ={"n", "x" },desc = "Search and Replace",},},}
-- Several cursors that edit together.{"jake-stewart/multicursor.nvim",branch = "1.0",config = function()local m = mc()m.setup()-- These keys exist only while there is more than one cursor.m.addKeymapLayer(function(set)set({"n", "x" }, "<left>", m.prevCursor, { desc = "Previous Cursor" })set({"n", "x" }, "<right>", m.nextCursor, { desc = "Next Cursor" })set({"n", "x" }, "<leader>md", m.deleteCursor, { desc = "Delete This Cursor" })set("n", "<esc>", function()if not m.cursorsEnabled()thenm.enableCursors()elsem.clearCursors()endend, { desc = "Clear Cursors" })end)end,keys ={{"<leader>mn", function()mc().matchAddCursor(1)end, mode ={"n", "x" }, desc = "Add Cursor at Next Match" },{"<leader>mN",function()mc().matchAddCursor(-1)end,mode ={"n", "x" },desc = "Add Cursor at Previous Match",},{"<leader>ms", function()mc().matchSkipCursor(1)end, mode ={"n", "x" }, desc = "Skip to Next Match" },{"<leader>mS", function()mc().matchSkipCursor(-1)end, mode ={"n", "x" }, desc = "Skip to Previous Match" },{"<leader>mA",function()mc().matchAllAddCursors()end,mode ={"n", "x" },desc = "Add Cursors at Every Match",},{"<leader>mx", function()mc().toggleCursor()end, mode ={"n", "x" }, desc = "Add or Remove a Cursor Here" },{"<leader>ma", function()mc().alignCursors()end, desc = "Align Cursors" },-- Ctrl with the arrows resizes windows (keymaps.lua), so Option adds cursors.{"<A-Up>", function()mc().lineAddCursor(-1)end, mode ={"n", "x" }, desc = "Add Cursor Above" },{"<A-Down>", function()mc().lineAddCursor(1)end, mode ={"n", "x" }, desc = "Add Cursor Below" },},}
-- A folder as a text buffer: rename a line to rename the file, delete it to delete the file.{"stevearc/oil.nvim",cmd = "Oil",opts ={ default_file_explorer = false, view_options ={ show_hidden = true}},keys ={{"<leader>fo", function()require("oil").open()end, desc = "Open Folder as Text (oil)" },},}
opts ={preset = "helix",spec ={{mode ={"n", "x" },{"<leader><tab>", group = "tabs", icon = I.group("tabs")},{"<leader>a", group = "claude", icon = I.group("claude")},{"<leader>b",group = "buffer",icon = I.group("buffer"),expand = function()return require("which-key.extras").expand.buf()end,},{"<leader>c", group = "code", icon = I.group("code")},{"<leader>d", group = "debug", icon = I.group("debug")},{"<leader>f", group = "file/find", icon = I.group("file/find")},{"<leader>g", group = "git", icon = I.group("git")},{"<leader>gh", group = "hunks", icon = I.group("hunks")},{"<leader>m", group = "multicursor", icon = I.group("multicursor")},{"<leader>q", group = "quit/session", icon = I.group("quit/session")},{"<leader>s", group = "search", icon = I.group("search")},{"<leader>u", group = "toggles", icon = I.group("toggles")},{"<leader>w",group = "windows",icon = I.group("windows"),proxy = "<c-w>",expand = function()return require("which-key.extras").expand.win()end,},{"<leader>x", group = "diagnostics/quickfix", icon = I.group("diagnostics/quickfix")},{"[", group = "prev", icon = I.group("prev")},{"]", group = "next", icon = I.group("next")},{"g", group = "goto", icon = I.group("goto")},{"z", group = "fold", icon = I.group("fold")},},},}
Full Spec
Lua
{"folke/which-key.nvim",event = "VeryLazy",-- A later plugin file may add groups with its own which-key spec; this makes-- lazy.nvim join the lists instead of letting the later one replace this one.opts_extend ={"spec" },opts ={preset = "helix",spec ={{mode ={"n", "x" },{"<leader><tab>", group = "tabs", icon = I.group("tabs")},{"<leader>a", group = "claude", icon = I.group("claude")},{"<leader>b",group = "buffer",icon = I.group("buffer"),expand = function()return require("which-key.extras").expand.buf()end,},{"<leader>c", group = "code", icon = I.group("code")},{"<leader>d", group = "debug", icon = I.group("debug")},{"<leader>f", group = "file/find", icon = I.group("file/find")},{"<leader>g", group = "git", icon = I.group("git")},{"<leader>gh", group = "hunks", icon = I.group("hunks")},{"<leader>m", group = "multicursor", icon = I.group("multicursor")},{"<leader>q", group = "quit/session", icon = I.group("quit/session")},{"<leader>s", group = "search", icon = I.group("search")},{"<leader>u", group = "toggles", icon = I.group("toggles")},{"<leader>w",group = "windows",icon = I.group("windows"),proxy = "<c-w>",expand = function()return require("which-key.extras").expand.win()end,},{"<leader>x", group = "diagnostics/quickfix", icon = I.group("diagnostics/quickfix")},{"[", group = "prev", icon = I.group("prev")},{"]", group = "next", icon = I.group("next")},{"g", group = "goto", icon = I.group("goto")},{"z", group = "fold", icon = I.group("fold")},},},},-- The icon rules are built when which-key loads, after the first screen,-- rather than when this file is read during the start.config = function(_, opts)opts.icons = vim.tbl_extend("force", opts.icons or{}, { rules = I.which_key_rules()})require("which-key").setup(opts)end,keys ={{"<leader>?",function()require("which-key").show({ global = false})end,desc = "Buffer Keymaps (which-key)",},{"<c-w><space>",function()require("which-key").show({ keys = "<c-w>", loop = true})end,desc = "Window Hydra Mode (which-key)",},},}
{"stevearc/conform.nvim",-- Loaded when a file opens rather than at the first save, so the status-- bar can name the formatter from the start.event ={"BufReadPost", "BufNewFile", "BufWritePre" },cmd = "ConformInfo",keys ={{"<leader>cf",function()require("conform").format({ async = true})end,mode ={"n", "x" },desc = "Format",},{"<leader>cF",function()require("conform").format({ formatters ={"injected" }, async = true})end,mode ={"n", "x" },desc = "Format Injected Langs",},},init = function()require("ach.format").setup()end,opts = function()return require("ach.format").opts()end,}
{"nvim-treesitter/nvim-treesitter",branch = "main",-- The main branch says it does not support lazy loading, meaning its-- commands and parser folder must be there before a file needs them.-- LazyVim loads it lazily all the same, and so does this: at the start-- when a file is named, otherwise after the first screen, or sooner when-- lua/ach/treesitter.lua starts it on a buffer or one of its commands runs.lazy = vim.fn.argc(-1)== 0,event = "VeryLazy",cmd ={"TSUpdate", "TSInstall", "TSInstallFromGrammar", "TSUninstall", "TSLog" },build = ":TSUpdate",init = function()-- nvim-treesitter rereads its parser list on this event, so the table's-- own parsers are added back each time.vim.api.nvim_create_autocmd("User", {pattern = "TSUpdate",callback = function()require("ach.treesitter").register_custom()end,})end,config = function()require("nvim-treesitter").setup({})end,}
{"nvim-treesitter/nvim-treesitter-textobjects",branch = "main",event = "VeryLazy",config = function()require("nvim-treesitter-textobjects").setup({ move ={ set_jumps = true}})-- Buffers opened before VeryLazy were attached before these queries-- were on the runtime path, so they get their moves now.for _, buf in ipairs(vim.api.nvim_list_bufs())doif vim.api.nvim_buf_is_loaded(buf)thenrequire("ach.treesitter").attach(buf)endendend,}
opts = function()local opts ={bottom ={{ ft = "qf", title = "QuickFix" },{ft = "help",size ={ height = 20},-- A help file opened for editing is an ordinary file, not a docked page.filter = function(buf)return vim.bo[buf].buftype == "help" end,},},right ={-- The Claude Code panel's view comes first, so it claims the panel-- before the generic terminal view the loop below appends, which-- names no width and would leave the panel 30 columns wide.require("ach.claude").view(),{ title = "Grug Far", ft = "grug-far", size ={ width = 0.4}},},-- Inside a docked pane, Ctrl and the arrows resize the pane.keys ={["<c-Right>"]= function(win)win:resize("width", 2)end,["<c-Left>"]= function(win)win:resize("width", -2)end,["<c-Up>"]= function(win)win:resize("height", 2)end,["<c-Down>"]= function(win)win:resize("height", -2)end,},}for _, pos in ipairs({"top", "bottom", "left", "right" })doopts[pos]= opts[pos]or{}table.insert(opts[pos], {ft = "trouble",filter = function(_, win)local w = vim.w[win].troublereturn wand w.position == posand w.type == "split"and w.relative == "editor"and not vim.w[win].trouble_previewend,})table.insert(opts[pos], {ft = "snacks_terminal",size ={ height = 0.4},title = "%{b:snacks_terminal.id}: %{b:term_title}",filter = function(_, win)local w = vim.w[win].snacks_winreturn w and w.position == pos and w.relative == "editor" and not vim.w[win].trouble_previewend,})endreturn optsend
Full Spec
Lua
-- Help, the terminal, the problem list and search-and-replace dock into fixed panes.{"folke/edgy.nvim",event = "VeryLazy",keys ={{"<leader>ue", function()require("edgy").toggle()end, desc = "Toggle Docked Panes" },{"<leader>uE", function()require("edgy").select()end, desc = "Pick a Docked Pane" },},opts = function()local opts ={bottom ={{ ft = "qf", title = "QuickFix" },{ft = "help",size ={ height = 20},-- A help file opened for editing is an ordinary file, not a docked page.filter = function(buf)return vim.bo[buf].buftype == "help" end,},},right ={-- The Claude Code panel's view comes first, so it claims the panel-- before the generic terminal view the loop below appends, which-- names no width and would leave the panel 30 columns wide.require("ach.claude").view(),{ title = "Grug Far", ft = "grug-far", size ={ width = 0.4}},},-- Inside a docked pane, Ctrl and the arrows resize the pane.keys ={["<c-Right>"]= function(win)win:resize("width", 2)end,["<c-Left>"]= function(win)win:resize("width", -2)end,["<c-Up>"]= function(win)win:resize("height", 2)end,["<c-Down>"]= function(win)win:resize("height", -2)end,},}for _, pos in ipairs({"top", "bottom", "left", "right" })doopts[pos]= opts[pos]or{}table.insert(opts[pos], {ft = "trouble",filter = function(_, win)local w = vim.w[win].troublereturn wand w.position == posand w.type == "split"and w.relative == "editor"and not vim.w[win].trouble_previewend,})table.insert(opts[pos], {ft = "snacks_terminal",size ={ height = 0.4},title = "%{b:snacks_terminal.id}: %{b:term_title}",filter = function(_, win)local w = vim.w[win].snacks_winreturn w and w.position == pos and w.relative == "editor" and not vim.w[win].trouble_previewend,})endreturn optsend,}
{"nvim-mini/mini.icons",-- The icon database's overrides: the hardware languages get chips, gates and-- waveforms, and file types mini.icons does not know get an icon of their own.opts = function()return require("ach.icons").mini()end,init = function()-- Plugins that ask for nvim-web-devicons get mini.icons instead.package.preload["nvim-web-devicons"]= function()require("mini.icons").mock_nvim_web_devicons()return package.loaded["nvim-web-devicons"]endend,}
-- One session per folder, saved on quit under ~/.local/state/nvim/sessions.{"folke/persistence.nvim",event = "BufReadPre",opts ={},keys ={{"<leader>qs", function()require("persistence").load()end, desc = "Restore Session for This Folder" },{"<leader>qS", function()require("persistence").select()end, desc = "Pick a Session" },{"<leader>ql", function()require("persistence").load({ last = true})end, desc = "Restore Last Session" },{"<leader>qd", function()require("persistence").stop()end, desc = "Don't Save This Session" },},}
Many small tools in one plugin. Here it gives the file picker, the dashboard, notifications, toggles, the terminal, the file tree, indent guides, the git keys and the image previews.
-- Indent guides, the current scope, highlights on other uses of the word under the-- cursor, the line-number column and the input box.{"folke/snacks.nvim",opts ={indent ={ enabled = true},scope ={ enabled = true},words ={ enabled = true},statuscolumn ={ enabled = true},input ={ enabled = true},},}
{"lervag/vimtex",-- Loaded at start rather than on a LaTeX file, because the Neovim that-- Skim starts for an inverse search opens no LaTeX file and still needs-- the VimtexInverseSearch command.lazy = false,-- VimTeX reads its settings when it loads, so they are set first.init = function()for name, value in pairs(require("ach.latex").globals())dovim.g[name]= valueendend,config = function()vim.api.nvim_create_autocmd("User", {group = vim.api.nvim_create_augroup("ach_latex", { clear = true}),pattern = "VimtexEventInitPost",callback = function()require("ach.latex").attach(vim.api.nvim_get_current_buf())end,})end,}
{"brianhuster/live-preview.nvim",cmd = "LivePreview",opts = P.opts(),-- Runs at start, so Space c p is on every Markdown file before the plugin-- itself has loaded.init = function()vim.api.nvim_create_autocmd("FileType", {group = vim.api.nvim_create_augroup("ach_markdown", { clear = true}),pattern = P.file_types,callback = function(ev)require("ach.preview").attach(ev.buf)end,})end,-- The guard goes on as the plugin loads, before its server can start.config = function(_, opts)require("livepreview.config").set(opts)P.guard()end,}
{"MeanderingProgrammer/render-markdown.nvim",ft = P.file_types,opts ={file_types = P.file_types,debounce = debounce,completions ={ blink ={ enabled = true}},-- snacks draws formulas as pictures, and two drawings would overlap.latex ={ enabled = false},code ={ sign = false, width = "block", right_pad = 1},heading ={ sign = false},},config = function(_, opts)require("render-markdown").setup(opts)I.toggle(Snacks.toggle({id = "render_markdown",name = "Render Markdown",get = function()return require("render-markdown").get()end,set = function(state)require("render-markdown").set(state)-- render-markdown drops a drawing asked for within debounce of the-- last one, so a press straight after scrolling would change the-- setting and leave the old drawing up. Asking again once that-- window has passed makes the press land.vim.defer_fn(function()require("render-markdown").set(state)end, debounce + 50)end,})):map("<leader>um")end,}
One subsection for each entry in lua/ach/languages.lua. Nothing here is installed until a file of that language is opened. Then its Tree-sitter parser, its language servers, its formatters and its linters install in the background, without asking, and switch on when they are ready. A compiler or runtime that only Homebrew provides is asked about first, once, in one line that names it and its rough size.
Each table names the parts: the file types the entry covers, the parsers that read its syntax, the language servers that check the code as it is typed, the formatters that tidy it on save, the linters that report problems, the debugger, and whether Harper, a grammar checker, reads it. The Entry tab is the language's entry in lua/ach/languages.lua. The Options tab, where there is one, holds its servers' settings from lua/ach/servers.lua.
-- astro-ls will not start without a path to TypeScript's library. A project-- with TypeScript installed gives its own; a file outside one, or a project-- whose packages are not installed yet, gets the copy Mason installed with-- the server.astro ={before_init = function(_, config)local tsdk = require("lspconfig.util").get_typescript_server_path(config.root_dir)if tsdk == "" thentsdk = mason .. "/astro-language-server/node_modules/typescript/lib"endconfig.init_options = config.init_options or{}config.init_options.typescript = config.init_options.typescript or{}config.init_options.typescript.tsdk = tsdkend,}
-- SchemaStore's catalogue, read when the server starts rather than when-- this file loads, so opening a Lua file never pays for it.jsonls ={before_init = function(_, config)config.settings.json.schemas = config.settings.json.schemas or{}vim.list_extend(config.settings.json.schemas, require("schemastore").json.schemas())end,settings ={ json ={ format ={ enable = true}, validate ={ enable = true}}},}
-- Mason's julia-lsp takes the Julia environment to load as its one-- argument; nvim-lspconfig's command runs julia directly instead.julials ={cmd = function(dispatchers, config)local root =(config and config.root_dir)or vim.fn.getcwd()return vim.lsp.rpc.start({"julia-lsp", root }, dispatchers)end,}
-- nvim-lspconfig works the cache folder out once, from whichever buffer is-- current when its file loads, and outside a Gradle or Maven project leaves-- the options empty. Neovim sends an empty table as a JSON array, which the-- server refuses, so it never starts. Each start gets the project's root,-- or a folder of Neovim's own cache for a file outside a project.kotlin_language_server ={before_init = function(params, config)local storage = config.root_dirif not storage thenstorage = vim.fn.stdpath("cache").. "/kotlin-language-server"vim.fn.mkdir(storage, "p")endparams.initializationOptions ={ storagePath = storage }end,}
-- basedpyright's default mode reports far more than most code is written-- for; "standard" matches what pyright checks.basedpyright ={settings ={ basedpyright ={ analysis ={ typeCheckingMode = "standard" }}},}-- Ruff's hover only explains its own rule codes, and two hovers in one-- window bury basedpyright's, which documents the code itself.ruff ={init_options ={ settings ={ logLevel = "error" }},on_init = function(client) client.server_capabilities.hoverProvider = false end,}
r ={ft ={"r", "rmd", "quarto", "rnoweb" },ts ={"r", "rnoweb", "markdown" },lsp ={"r_language_server" },harper ={"quarto" },-- The server is an R package, which R builds from source on install.runtime ={name = "R",check ={"Rscript", "-e", "library(languageserver)" },brew ={"r" },after ={{"Rscript", "-e", "install.packages('languageserver', repos = 'https://cloud.r-project.org')" }},size = "about 1.5 GB",},}