Personal Neovim configuration for macOS and Ubuntu Linux.
The setup script installs or verifies Neovim and the command-line tools, creates a safe configuration symlink, installs the locked plugins, and is safe to run again.
It supports macOS and Ubuntu Linux. On macOS it installs Homebrew if needed. On
Ubuntu it uses sudo apt-get for system prerequisites and installs Neovim and
user tools without requiring root for the configuration.
The setup pins Node.js 24.20.0 and Go 1.27.1 on both platforms. It also
enables the pnpm and yarn Corepack shims next to the user-local tools, so
they continue to point at the pinned Node.js installation. Treesitter uses
Neovim's 0.12-compatible nvim-treesitter main branch; setup installs the
required Tree-sitter CLI automatically.
Go is installed under ${XDG_DATA_HOME:-$HOME/.local/share}/go-1.27.1.
Setup reuses that installation on subsequent runs and updates your shell's PATH
to prefer it. An older system Go can remain installed; open a new shell after
setup completes to use the pinned version.
git clone https://github.com/oliverxchen/nvim-config.git ~/nvim-config
cd ~/nvim-config
./setup_nvim.shOpen a new terminal after setup so the user-local tool paths take effect. Run
./setup_nvim.sh again to update tools and reinstall the locked plugins. The
script installs lazy.nvim at the commit recorded in nvim/lazy-lock.json.
Normal Neovim startup only verifies and loads that local checkout; it does not
clone, fetch, or check out a Git revision. Use :Lazy update when you
deliberately want newer plugin revisions, then review the lockfile and run
setup again.
<C-p>finds files with fuzzy matching across file and directory names, including hidden paths while respecting.gitignore;:Fgsearches text;:Fblists buffers;:Feopens Oil for buffer-style directory editing;<leader>eor:Etoggles the Neo-tree sidebar, and<leader>Eor:Eereveals the current file in it.<leader>is Space in this configuration:<leader>emeans Space thene, while<leader>Emeans Space then Shift+e.- Starting
nvimwithout a path opens Neo-tree automatically; closing all non-Neo-tree windows exits Neovim. :Termopens a terminal in a 10-line-high split at the bottom of the window. Press<Esc>in terminal mode to enter Normal mode (equivalent to<C-\> C-n).:Ztoggles folds. Split navigation uses<C-w>followed byh,j,k, orl.gcandgbcomment lines or blocks.- Fugitive provides Git commands. Gitsigns and Lualine show the current branch and changed-line counts.
- Diffview provides a single-tab review of Git changes. Use
:DiffviewOpento compare the working tree with the index (:DOis a shortcut),:DiffviewOpen HEAD~2to compare against another revision, and:DiffviewFileHistoryor:DiffviewFileHistory %for branch or current-file history. Close the view with:DiffviewCloseor:DC;<Tab>and<S-Tab>cycle through changed files. - The TypeScript language server,
ty,gopls, and Taplo provide language intelligence when their tools apply. - Conform formats supported files on save when the appropriate project-local formatter is available. Ruff, ESLint, and other linters report diagnostics separately.
- Swap files and persistent undo are enabled for recovery.
For troubleshooting, use :checkhealth, :checkhealth vim.lsp,
:checkhealth neo-tree, and :Lazy inside Neovim. The configuration expects
Neovim 0.12.0 or newer.
Ruff and ty are not installed globally by the setup script. Prettier is installed in the user-local Node.js tool directory as a fallback for Markdown, while repository-local versions take precedence:
- For Markdown, Prettier is resolved from the nearest
node_modules/.bin/prettierbetween the current file and the repository root, then from the user-local global Prettier installation. - Other Prettier-supported filetypes require the nearest repository-local
node_modules/.bin/prettier. - Ruff and ty are resolved from the active
$VIRTUAL_ENV, or from a.venv,venv, orenvdirectory in the current repository. - Repository detection follows the current file; for an unnamed buffer it uses Neovim's current working directory, so starting Neovim from a repository also works.
- Autoformatting and repository linters are disabled when the current file is outside a Git repository. Markdown can use the global fallback inside a repository; other Prettier-supported filetypes still need a project-local formatter.
For example, install the tools in the project rather than globally:
# JavaScript/TypeScript/JSON/YAML/Markdown projects
pnpm add --save-dev prettier
# or: npm install --save-dev prettier
# Python projects using uv
uv add --dev ruff ty
uv run ruff --version
uv run ty --versionThe setup still installs uv itself as an environment manager, and keeps
ESLint, eslint_d, Prettier, TypeScript, and typescript-language-server in
the user-local Node.js tool directory. Use :ConformInfo to see which
formatter was selected for the current buffer.
- Python: project-environment
tyfor type checking and LSP features; project- environment Ruff for diagnostics and formatting; four-space indentation. - JavaScript and TypeScript:
ts_ls, ESLint, and project-local Prettier; Markdown also has a user-local global fallback; two-space indentation. The setup uses TypeScript 6 becausetypescript-language-serverneedstsserver.js. - Go: pinned
goplsv0.23.0and pinned Go1.27.1'sgofmt; tabs with a width of two. - TOML: Treesitter highlighting and Taplo for validation, completion, and formatting, with two-space indentation for nested tables and entries.
- JSON, YAML, and Markdown: Treesitter and Prettier. Terraform uses
terraform fmtwhen Terraform is installed. SQL has highlighting only. CSV usescsv.vim. - Git commit buffers: spell checking and two-space indentation.
Normal y and p use the system clipboard through unnamedplus. macOS uses
its native clipboard; Ubuntu desktop uses Wayland or X11 tools; direct SSH uses
OSC 52; SSH inside tmux uses Neovim's tmux provider. Check
:checkhealth provider if clipboard behavior is unexpected. In tmux,
tmux info | grep 'Ms:' should show an Ms capability.
Swap files and persistent undo are enabled for recovery. Formatting runs on save for supported file types inside repositories when a formatter is available. The global trailing-whitespace hook from the Vim configuration was intentionally removed.
Run setup twice on both macOS and Ubuntu:
./setup_nvim.sh
./setup_nvim.shOpen a new shell afterward so user-local paths are loaded. Confirm the main tools:
nvim --version
node --version # v24.20.0
go version # go1.27.1
tsc --version
typescript-language-server --version
gopls version
corepack --version
tree-sitter --version
taplo lsp --help
taplo fmt --helpNeovim should start without errors, use the onedark_dark theme, show Git
branch and change counts, open Neo-tree as a left sidebar with <leader>e or
:E, and keep the lockfile unchanged during normal setup. Test nvim with no
path, Oil with :Fe, Neo-tree with <leader>e, :E, :Ee, and <leader>E,
and one file of each commonly used type: Python, TypeScript, JavaScript, Go,
TOML, Markdown, JSON, YAML, Terraform, SQL, and CSV. For language buffers,
:checkhealth vim.lsp should show the expected client; :checkhealth neo-tree
should pass; :ConformInfo shows the selected formatter.
The detailed migration plan is kept in
neovim-migration.md. It records the old Vim plugin
audit, rejected alternatives, and the reasons for the current choices; it is not
needed for normal installation.
The setup script adds these settings to the relevant shell profile:
export EDITOR=nvim
export VISUAL=nvim
export GIT_EDITOR=nvim
alias vim=nvim
alias vi=nvimExecute the following command in a terminal to allow holding a direction to scroll:
defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false
For cursor, find the app address:
cd /Applications
mdls -name kMDItemCFBundleIdentifier Cursor.app
And then use the output with the defaults write command.
Add the following to VSCode's settings.json to be able to yank into the system
clipboard:
"vim.useSystemClipboard": true
Go to the command pallete cmd+shift+P and select: "Preferences: Open Keyboard Shortcuts (JSON)" add to the keybindings.json file
// Place your key bindings in this file to override the defaults
[
{
"key": "cmd+j",
"command": "workbench.action.terminal.focus"
},
{
"key": "cmd+j",
"command": "workbench.action.focusActiveEditorGroup",
"when": "terminalFocus"
},
{
"key": "cmd+r",
"command": "workbench.action.terminal.runSelectedText"
},
{
"key": "shift+enter",
"command": "-python.execInREPL",
"when": "config.python.REPL.sendToNativeREPL && editorTextFocus && !accessibilityModeEnabled && !isCompositeNotebook && !jupyter.ownsSelection && !notebookEditorFocused && editorLangId == 'python'"
},
{
"key": "shift+enter",
"command": "-python.execSelectionInTerminal",
"when": "editorTextFocus && !findInputFocussed && !isCompositeNotebook && !jupyter.ownsSelection && !notebookEditorFocused && !replaceInputFocussed && editorLangId == 'python'"
},
{
"key": "cmd+i",
"command": "composerMode.agent",
},
{
"key": "shift+cmd+0",
"command": "workbench.action.terminal.focusPrevious",
},
{
"key": "shift+cmd+backspace",
"command": "workbench.action.terminal.focusNext",
},
]
User settings in VS Code:
{
"dart.flutterSdkPath": "/Users/oliverchen/src/flutter",
"editor.minimap.enabled": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"vim.useSystemClipboard": true,
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[javascript]": {
"editor.tabSize": 2
},
"[javascriptreact]": {
"editor.tabSize": 2
},
"[typescript]": {
"editor.tabSize": 2
},
"[typescriptreact]": {
"editor.tabSize": 2
},
"files.associations": {
".cursorrules": "plaintext"
},
"git.openRepositoryInParentFolders": "never",
"editor.accessibilitySupport": "off",
"git.confirmSync": false
}