commit 457ba5e3fcb0ac0ae8a7b7b4cd33ff20917ebc97
parent c62998822919401d876db53c956ef5233d4a852d
Author: Chris <chris@echoz.io>
Date: Fri, 28 Nov 2025 02:57:46 +0100
refactor(neovim): split up config, generate lsp config
Diffstat:
15 files changed, 372 insertions(+), 175 deletions(-)
diff --git a/modules/neovim/cheat-sheet.txt b/modules/neovim/cheat-sheet.txt
@@ -0,0 +1,10 @@
+i C-x C-o Omni completion
+n K Hover
+n,v C-w d Hover diagnostics
+n,v gra Code action
+n gri Go to implementation
+n grt Go to type definition
+n grr References
+n grn Rename
+n gO List symbols
+vb I Insert before every line in block
diff --git a/modules/neovim/default.nix b/modules/neovim/default.nix
@@ -3,16 +3,142 @@
pkgs,
...
}:
+let
+ runtime = luaRuntime // lspRuntime // ftpluginRuntime;
+
+ initLua = generateInitLua runtime {
+ cheat-sheet.file = ./cheat-sheet.txt;
+ init = { };
+ };
+
+ luaRuntime = importLuaDir "lua" ./lua;
+ ftpluginRuntime = importLuaDir "ftplugin" ./ftplugin;
+
+ lspRuntime = generateLspRuntime {
+ nixd = {
+ cmd = [ (lib.getExe pkgs.nixd) ];
+ filetypes = [ "nix" ];
+ root_markers = [
+ "flake.nix"
+ ".git"
+ ];
+ };
+
+ gopls = {
+ cmd = [ (lib.getExe pkgs.gopls) ];
+ filetypes = [
+ "go"
+ "gomod"
+ "gowork"
+ "gotmpl"
+ ];
+ root_markers = [
+ "go.work"
+ "go.mod"
+ ".git"
+ ];
+ };
+
+ typescript-language-server = {
+ cmd = [
+ (lib.getExe pkgs.typescript-language-server)
+ "--stdio"
+ ];
+ filetypes = [
+ "javascript"
+ "typescript"
+ ];
+ root_markers = [
+ [
+ "jsconfig.json"
+ "tsconfig.json"
+ ]
+ "package.json"
+ ".git"
+ ];
+ init_options.hostInfo = "neovim";
+ };
+
+ ruff = {
+ cmd = [
+ (lib.getExe pkgs.ruff)
+ "server"
+ ];
+ filetypes = [ "python" ];
+ root_markers = [
+ "pyproject.toml"
+ "ruff.toml"
+ ".ruff.toml"
+ ".git"
+ ];
+ };
+
+ lua-language-server = {
+ cmd = [ (lib.getExe pkgs.lua-language-server) ];
+ filetypes = [ "lua" ];
+ settings.Lua."diagnostics.globals" = [ "vim" ];
+ };
+ };
+
+ generateLspRuntime =
+ lsps:
+ (lib.mapAttrs' (name: config: {
+ name = "lsp/${name}.lua";
+ value.text = "return ${lib.generators.toLua { } config}";
+ }) lsps)
+ // {
+ "lua/lsp.lua".text = ''
+ local M = {}
+ function M.setup()
+ vim.lsp.enable(${lib.generators.toLua { indent=" "; } (builtins.attrNames lsps)})
+ end
+ return M
+ '';
+ };
+
+ importLuaDir =
+ prefix:
+ path:
+ lib.pipe (builtins.readDir path) [
+ (lib.mapAttrs' (
+ name: type: {
+ name = "${prefix}/${name}";
+ value =
+ if type == "regular" && builtins.match "^.*.lua$" name != null then
+ { source = path + "/${name}"; }
+ else
+ null;
+ }
+ ))
+ (lib.filterAttrs (_: value: value != null))
+ ];
+
+ generateInitLua =
+ runtime: args:
+ lib.pipe runtime [
+ builtins.attrNames
+ (builtins.map (builtins.match "^lua/(.*).lua$"))
+ (builtins.filter builtins.isList)
+ (builtins.map builtins.head)
+ (builtins.map (name: "require('${name}').setup(${lib.generators.toLua { } (args.${name} or { })})"))
+ (builtins.concatStringsSep "\n")
+ ];
+in
{
programs.neovim = {
enable = true;
+
+ inherit runtime;
+ configure.customLuaRC = initLua;
+
defaultEditor = true;
viAlias = true;
vimAlias = true;
- configure.customLuaRC = builtins.readFile ./neovim.lua;
+
withRuby = lib.mkDefault false;
withPython3 = lib.mkDefault false;
withNodeJs = lib.mkDefault false;
+
package = pkgs.neovim-unwrapped.overrideAttrs {
version = "v0.12.0-dev";
src = pkgs.fetchFromGitHub {
@@ -23,10 +149,4 @@
};
};
};
-
- environment.systemPackages = with pkgs; [
- nixd
- gopls
- typescript-language-server
- ];
}
diff --git a/modules/neovim/ftplugin/mail.lua b/modules/neovim/ftplugin/mail.lua
@@ -0,0 +1 @@
+vim.opt.textwidth = 72
diff --git a/modules/neovim/lua/cheat-sheet.lua b/modules/neovim/lua/cheat-sheet.lua
@@ -0,0 +1,32 @@
+local M = {}
+
+function M.setup(opts)
+ M.file = opts.file or "/dev/null"
+ M.lines = {}
+
+ for line in io.lines(M.file) do
+ table.insert(M.lines, line)
+ end
+
+ vim.keymap.set({'n','v'}, '<C-/>', function()
+ local buf = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_keymap(buf, 'n', 'q', ':q<CR>', {
+ silent = true,
+ nowait = true,
+ })
+ vim.api.nvim_buf_set_lines(buf, 0, -1, false, M.lines)
+ local win = vim.api.nvim_open_win(buf, true, {
+ title = 'Cheat sheet',
+ relative = 'editor',
+ row = math.floor(vim.o.lines * 0.10),
+ col = math.floor(vim.o.columns * 0.10),
+ height = math.floor(vim.o.lines * 0.80),
+ width = math.floor(vim.o.columns * 0.80),
+ style = 'minimal',
+ title_pos = 'center',
+ focusable = true,
+ })
+ end)
+end
+
+return M
diff --git a/modules/neovim/lua/code.lua b/modules/neovim/lua/code.lua
@@ -0,0 +1,17 @@
+local M = {}
+
+function M.setup()
+ vim.opt.signcolumn = "yes"
+ vim.opt.completeopt = { "fuzzy", "menu", "menuone", "noinsert", "popup" }
+
+ vim.diagnostic.config({ virtual_text = true })
+
+ vim.lsp.config('*', {
+ root_markers = { '.git' },
+ on_attach = function(client, bufnr)
+ vim.lsp.completion.enable(true, client.id, bufnr, {})
+ end,
+ })
+end
+
+return M
diff --git a/modules/neovim/lua/general.lua b/modules/neovim/lua/general.lua
@@ -0,0 +1,19 @@
+local M = {}
+
+function M.setup()
+ vim.opt.tabstop = 2
+ vim.opt.shiftwidth = 2
+ vim.opt.expandtab = true
+
+ vim.opt.number = true
+ vim.opt.relativenumber = true
+
+ vim.opt.scrolloff = 5
+
+ vim.opt.cursorline = true
+ vim.opt.textwidth = 100
+ vim.opt.colorcolumn = '+1'
+ vim.opt.formatoptions = 'cqj'
+end
+
+return M
diff --git a/modules/neovim/lua/listchars.lua b/modules/neovim/lua/listchars.lua
@@ -0,0 +1,31 @@
+local M = {}
+
+function M.setup()
+ vim.opt.list = true
+ vim.opt.listchars = {
+ trail = '~',
+ tab = '| ',
+ leadmultispace = ':' .. string.rep(' ', vim.opt.shiftwidth:get() - 1),
+ }
+
+ local function update()
+ local listchars = vim.opt_local.listchars:get()
+ listchars.leadmultispace = ':' .. string.rep(' ', vim.opt_local.shiftwidth:get() - 1)
+ vim.opt_local.listchars = listchars
+ end
+
+ local group = vim.api.nvim_create_augroup('ListcharsLeadmultispaceWidth', { clear = true }),
+
+ vim.api.nvim_create_autocmd('OptionSet', {
+ group = group,
+ pattern = 'shiftwidth',
+ callback = update,
+ })
+
+ vim.api.nvim_create_autocmd({ 'FileType', 'BufWinEnter' }, {
+ group = group,
+ callback = update,
+ })
+end
+
+return M
diff --git a/modules/neovim/lua/netrw.lua b/modules/neovim/lua/netrw.lua
@@ -0,0 +1,13 @@
+local M = {}
+
+function M.setup()
+ vim.g.netrw_banner = 0
+
+ -- absolute 30 cols/rows
+ vim.g.netrw_winsize = -30
+
+ -- tree style
+ vim.g.netrw_liststyle = 3
+end
+
+return M
diff --git a/modules/neovim/lua/return-to-last-line.lua b/modules/neovim/lua/return-to-last-line.lua
@@ -0,0 +1,15 @@
+local M = {}
+
+function M.setup()
+ vim.api.nvim_create_autocmd('BufReadPost', {
+ group = vim.api.nvim_create_augroup('ReturnToLastLine', { clear = true }),
+ callback = function()
+ local last = vim.fn.line([['"]])
+ if last > 1 and last < vim.fn.line("$") then
+ vim.cmd([[normal! g'"]])
+ end
+ end
+ })
+end
+
+return M
diff --git a/modules/neovim/lua/style.lua b/modules/neovim/lua/style.lua
@@ -0,0 +1,32 @@
+local M = {}
+
+function M.setup()
+ vim.api.nvim_set_hl(0, 'Normal', { bg='none' })
+ vim.api.nvim_set_hl(0, 'StatusLine', { bg='none' })
+ vim.api.nvim_set_hl(0, 'StatusLineNC', { bg='none', fg='gray' })
+ vim.api.nvim_set_hl(0, 'WinBar', { bg='none' })
+ vim.api.nvim_set_hl(0, 'WinBarNC', { bg='none', fg='gray' })
+ vim.api.nvim_set_hl(0, 'NormalFloat', { bg='none' })
+ vim.api.nvim_set_hl(0, 'FloatBorder', { bg='none' })
+ vim.api.nvim_set_hl(0, 'Pmenu', { bg='none' })
+ vim.api.nvim_set_hl(0, 'PmenuBorder', { bg='none' })
+
+ vim.opt.winborder = "rounded";
+ vim.opt.pumborder = "rounded";
+
+ vim.opt.cmdheight = 0
+
+ -- Raise cmdheight to make recording status visible
+ vim.api.nvim_create_autocmd("RecordingEnter", {
+ callback = function()
+ vim.opt.cmdheight = 1
+ end,
+ })
+ vim.api.nvim_create_autocmd("RecordingLeave", {
+ callback = function()
+ vim.opt.cmdheight = 0
+ end,
+ })
+end
+
+return M
diff --git a/modules/neovim/lua/system-clipboard.lua b/modules/neovim/lua/system-clipboard.lua
@@ -0,0 +1,12 @@
+local M = {}
+
+function M.setup()
+ vim.keymap.set({'n','v'}, '<C-c>', '"+y')
+ vim.keymap.set({'n','v'}, '<C-v>', '"+p')
+ vim.keymap.set({'n','v'}, '<C-x>', '"+d')
+
+ -- Use C-q for block visual mode
+ vim.keymap.set({'n','v'}, '<C-q>', '<C-v>', { noremap = true})
+end
+
+return M
diff --git a/modules/neovim/lua/undodir.lua b/modules/neovim/lua/undodir.lua
@@ -0,0 +1,8 @@
+local M = {}
+
+function M.setup()
+ vim.opt.undofile = true
+ vim.opt.undodir = vim.fn.stdpath('data') .. '/undodir'
+end
+
+return M
diff --git a/modules/neovim/lua/wildmenu.lua b/modules/neovim/lua/wildmenu.lua
@@ -0,0 +1,13 @@
+local M = {}
+
+function M.setup()
+ vim.opt.path = vim.o.path .. '**'
+ vim.opt.wildmenu = true
+ vim.opt.wildignore = {
+ "**/.direnv/**",
+ "**/node_modules/**",
+ "**/vendor/**",
+ };
+end
+
+return M
diff --git a/modules/neovim/lua/winbar.lua b/modules/neovim/lua/winbar.lua
@@ -0,0 +1,42 @@
+local M = {}
+
+function M.setup()
+ vim.api.nvim_create_autocmd({'BufEnter', 'BufAdd', 'BufDelete'}, {
+ group = vim.api.nvim_create_augroup("WinBarBuffers", { clear = true }),
+ pattern = "*",
+ callback = function()
+ local buffers = {}
+ local current = vim.api.nvim_win_get_buf(0)
+
+ for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+ if vim.bo[bufnr].buflisted then
+ local name = vim.fn.fnamemodify(vim.fn.bufname(bufnr), ":t")
+ if name == "" then name = "[No name]" end
+
+ local highlight = bufnr == current and "WinBar" or "WinBarNC"
+ local modified = vim.bo[bufnr].modified and "*" or ""
+
+ table.insert(buffers, string.format(
+ "%%#%s# %d:%s%s %%*",
+ highlight, bufnr, name, modified
+ ))
+ end
+ end
+
+ if #buffers > 1 and vim.bo[current].buflisted then
+ vim.opt_local.winbar = table.concat(buffers)
+ vim.keymap.set({'n','v'}, '<C-n>', ':bn<cr>', { buffer = current })
+ vim.keymap.set({'n','v'}, '<C-p>', ':bp<cr>', { buffer = current })
+
+ else
+ vim.opt_local.winbar = ""
+ pcall(function()
+ vim.keymap.del({'n','v'}, '<C-n>', { buffer = current })
+ vim.keymap.del({'n','v'}, '<C-p>', { buffer = current })
+ end)
+ end
+ end
+ })
+end
+
+return M
diff --git a/modules/neovim/neovim.lua b/modules/neovim/neovim.lua
@@ -1,168 +0,0 @@
-vim.opt.tabstop = 2
-vim.opt.shiftwidth = 0
-vim.opt.softtabstop = 0
-vim.opt.expandtab = true
-vim.opt.number = true
-vim.opt.relativenumber = true
-vim.opt.scrolloff = 5
-vim.opt.path = vim.o.path .. '**'
-vim.opt.wildmenu = true
-vim.opt.wildignore = {
- "**/.direnv/**",
- "**/node_modules/**",
- "**/vendor/**",
-};
-vim.opt.cursorline = true
-vim.opt.undofile = true
-vim.opt.undodir = vim.fn.stdpath('data') .. '/undodir'
-vim.opt.colorcolumn = '+1'
-vim.opt.textwidth = 72
-vim.opt.formatoptions = 'cqj'
-vim.keymap.set({'n','v'}, '<C-c>', '"+y')
-vim.keymap.set({'n','v'}, '<C-v>', '"+p')
-vim.keymap.set({'n','v'}, '<C-x>', '"+d')
-vim.keymap.set({'n','v'}, '<C-q>', '<C-v>', { noremap = true})
-vim.keymap.set({'n','v'}, '<C-n>', ':bn<cr>')
-vim.keymap.set({'n','v'}, '<C-p>', ':bp<cr>')
-vim.api.nvim_create_autocmd('BufReadPost', {
- group = vim.api.nvim_create_augroup('ReturnToLastLine', { clear = true }),
- callback = function()
- local last = vim.fn.line([['"]])
- if last > 1 and last < vim.fn.line("$") then
- vim.cmd([[normal! g'"]])
- end
- end
-})
-vim.opt.list = true
-vim.opt.listchars = {
- trail = '~',
- tab = '| ',
- leadmultispace = ':' .. string.rep(' ', vim.opt.tabstop:get() - 1),
-}
-vim.api.nvim_create_autocmd('OptionSet', {
- group = vim.api.nvim_create_augroup('ListcharsLeadmultispaceWidth', { clear = true }),
- pattern = 'tabstop',
- callback = function()
- local listchars = vim.opt.listchars:get()
- listchars.leadmultispace = ':' .. string.rep(' ', vim.opt.tabstop:get() - 1)
- vim.opt.listchars = listchars
- end
-})
-vim.opt.cmdheight = 0
-vim.api.nvim_create_autocmd("RecordingEnter", {
- callback = function()
- vim.opt.cmdheight = 1
- end,
-})
-vim.api.nvim_create_autocmd("RecordingLeave", {
- callback = function()
- vim.opt.cmdheight = 0
- end,
-})
-vim.api.nvim_set_hl(0, 'Normal', { bg='none' })
-vim.api.nvim_set_hl(0, 'StatusLine', { bg='none' })
-vim.api.nvim_set_hl(0, 'WinBar', { bg='none' })
-vim.api.nvim_set_hl(0, 'WinBarNC', { bg='none', fg='gray' })
-vim.api.nvim_set_hl(0, 'NormalFloat', { bg='none' })
-vim.api.nvim_set_hl(0, 'FloatBorder', { bg='none' })
-vim.api.nvim_set_hl(0, 'Pmenu', { bg='none' })
-vim.api.nvim_set_hl(0, 'PmenuBorder', { bg='none' })
-vim.opt.winborder = "rounded";
-vim.opt.pumborder = "rounded";
-function _G.WinBar()
- local buffers = {}
- local current = vim.api.nvim_win_get_buf(0)
- for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
- if vim.bo[bufnr].buflisted then
- local name = vim.fn.fnamemodify(vim.fn.bufname(bufnr), ":t")
- if name == "" then name = "[No name]" end
- local highlight = bufnr == current and "WinBar" or "WinBarNC"
- local modified = vim.bo[bufnr].modified and "*" or ""
- table.insert(buffers, string.format(
- "%%#%s# %d:%s%s %%*",
- highlight, bufnr, name, modified
- ))
- end
- end
- return table.concat(buffers)
-end
-vim.api.nvim_create_autocmd({'BufEnter', 'BufAdd', 'BufDelete'}, {
- group = vim.api.nvim_create_augroup("WinBarVisibility", { clear = true }),
- pattern = "*",
- callback = function()
- local buffers = 0
- for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
- if vim.bo[bufnr].buflisted then
- buffers = buffers + 1
- end
- end
- if buffers > 1 then
- vim.opt.winbar = "%!v:lua.WinBar()"
- else
- vim.opt.winbar = ""
- end
- end
-})
-vim.g.netrw_banner = 0
-vim.g.netrw_winsize = -30
-vim.g.netrw_liststyle = 3
-vim.diagnostic.config({ virtual_text = true })
-vim.opt.signcolumn = "yes"
-vim.opt.completeopt = { "fuzzy", "menu", "menuone", "noinsert", "popup" }
-vim.lsp.config('*', {
- root_markers = { '.git' },
- on_attach = function(client, bufnr)
- vim.lsp.completion.enable(true, client.id, bufnr, {})
- end,
-})
-vim.lsp.config('nixd', {
- cmd = { 'nixd' },
- filetypes = { 'nix' },
- root_markers = { 'flake.nix', '.git' },
-})
-vim.lsp.config('gopls', {
- cmd = { 'gopls' },
- filetypes = { 'go', 'gomod', 'gowork', 'gotmpl' },
- root_markers = { 'go.work', 'go.mod', '.git' },
-})
-vim.lsp.config('typescript-language-server', {
- cmd = { 'typescript-language-server', '--stdio' },
- filetypes = { 'javascript', 'typescript' },
- root_markers = { 'jsconfig.json', 'tsconfig.json', 'package.json', '.git' },
- init_options = { hostInfo = 'neovim' },
-})
-vim.lsp.enable({
- 'nixd',
- 'gopls',
- 'typescript-language-server',
-})
-vim.keymap.set({'n','v'}, '<C-/>', function()
- local buf = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_keymap(buf, 'n', 'q', ':q<CR>', {
- silent = true,
- nowait = true,
- })
- vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
- 'i C-x C-o Omni completion',
- 'n K Hover',
- 'n,v C-w d Hover diagnostics',
- 'n,v gra Code action',
- 'n gri Go to implementation',
- 'n grt Go to type definition',
- 'n grr References',
- 'n grn Rename',
- 'n gO List symbols',
- 'vb I Insert before every line in block',
- })
- local win = vim.api.nvim_open_win(buf, true, {
- title = 'Cheat sheet',
- relative = 'editor',
- row = math.floor(vim.o.lines * 0.10),
- col = math.floor(vim.o.columns * 0.10),
- height = math.floor(vim.o.lines * 0.80),
- width = math.floor(vim.o.columns * 0.80),
- style = 'minimal',
- title_pos = 'center',
- focusable = true,
- })
-end)