diff --git a/flake.lock b/flake.lock index a89f66c..405babb 100644 --- a/flake.lock +++ b/flake.lock @@ -6,17 +6,14 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1757895736, - "narHash": "sha256-BBjs+YCOzgb6N2lew4vEmyS6s70y0z5xStKjGQaf55g=", - "owner": "iofq", - "repo": "dart.nvim", - "rev": "f059335a22811374d5a7e22c97889ea712db58d7", - "type": "github" + "lastModified": 1758080529, + "narHash": "sha256-Sup4+HacL6Xe6mTk23N6sD4uXoU9dcoqRgc9Mu0oQ5E=", + "path": "/home/e/dev/dart.nvim", + "type": "path" }, "original": { - "owner": "iofq", - "repo": "dart.nvim", - "type": "github" + "path": "/home/e/dev/dart.nvim", + "type": "path" } }, "flake-compat": { @@ -413,11 +410,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1759381078, - "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "lastModified": 1757745802, + "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", "owner": "nixos", "repo": "nixpkgs", - "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", + "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", "type": "github" }, "original": { @@ -430,11 +427,11 @@ "nvim-treesitter": { "flake": false, "locked": { - "lastModified": 1759376029, - "narHash": "sha256-Cu6Wg9SKJpYAkp8DPAXe4Rf9OSSWW2wNdmCkYtl//fw=", + "lastModified": 1757840001, + "narHash": "sha256-9LcVSwWfOvp+1fLWqQtSohGax40gD9sGz9t0amySyDk=", "owner": "nvim-treesitter", "repo": "nvim-treesitter", - "rev": "99bd52ba56a4b7c9a8cc50a6140180755e76fac6", + "rev": "7aa24acae3a288e442e06928171f360bbdf75ba4", "type": "github" }, "original": { @@ -447,21 +444,17 @@ "nvim-treesitter-main": { "inputs": { "nixpkgs": "nixpkgs_4", - "nvim-treesitter": "nvim-treesitter", - "nvim-treesitter-textobjects": "nvim-treesitter-textobjects" + "nvim-treesitter": "nvim-treesitter" }, "locked": { - "lastModified": 1759559567, - "narHash": "sha256-cNu8MIRumBeW1HJK8c8IM6ge1yalz4xS5j5RCDbhf0Y=", - "owner": "iofq", - "repo": "nvim-treesitter-main", - "rev": "1f19cc0a907328d8dbf467ee9fe216277628d366", - "type": "github" + "lastModified": 1759471274, + "narHash": "sha256-jZxiszCE/g7oRLHmR8+aNqzP5w53wnfJ84D+AWhIwUk=", + "path": "/home/e/dev/nvim-treesitter-main", + "type": "path" }, "original": { - "owner": "iofq", - "repo": "nvim-treesitter-main", - "type": "github" + "path": "/home/e/dev/nvim-treesitter-main", + "type": "path" } }, "nvim-treesitter-textobjects": { @@ -488,7 +481,8 @@ "gen-luarc": "gen-luarc", "neovim-nightly-overlay": "neovim-nightly-overlay", "nixpkgs": "nixpkgs_3", - "nvim-treesitter-main": "nvim-treesitter-main" + "nvim-treesitter-main": "nvim-treesitter-main", + "nvim-treesitter-textobjects": "nvim-treesitter-textobjects" } }, "systems": { diff --git a/flake.nix b/flake.nix index a2231e4..310e0ab 100644 --- a/flake.nix +++ b/flake.nix @@ -10,10 +10,14 @@ inputs.nixpkgs.follows = "nixpkgs"; }; dart = { - url = "github:iofq/dart.nvim"; + url = "path:/home/e/dev/dart.nvim"; }; nvim-treesitter-main = { - url = "github:iofq/nvim-treesitter-main"; + url = "path:/home/e/dev/nvim-treesitter-main"; + }; + nvim-treesitter-textobjects = { + url = "github:nvim-treesitter/nvim-treesitter-textobjects/main"; + flake = false; }; # Add bleeding-edge plugins here. # They can be updated with `nix flake update` (make sure to commit the generated flake.lock) @@ -33,6 +37,7 @@ systems = builtins.attrNames nixpkgs.legacyPackages; # This is where the Neovim derivation is built. + plugin-overlay = import ./nix/plugin-overlay.nix { inherit inputs; }; neovim-overlay = import ./nix/neovim-overlay.nix { inherit inputs; }; in flake-utils.lib.eachSystem systems ( @@ -44,6 +49,7 @@ overlays = [ inputs.neovim-nightly-overlay.overlays.default inputs.nvim-treesitter-main.overlays.default + plugin-overlay neovim-overlay # This adds a function can be used to generate a .luarc.json # containing the Neovim API all plugins in the workspace directory. @@ -81,6 +87,6 @@ } ) // { - overlays.default = final: prev: (neovim-overlay final prev); + overlays.default = final: prev: (plugin-overlay final prev) // (neovim-overlay final prev); }; } diff --git a/nix/mkNeovim.nix b/nix/mkNeovim.nix index a60263b..7d2e3a6 100644 --- a/nix/mkNeovim.nix +++ b/nix/mkNeovim.nix @@ -1,40 +1,197 @@ +# Function for creating a Neovim derivation { pkgs, lib, + stdenv, + # Set by the overlay to ensure we use a compatible version of `wrapNeovimUnstable` + pkgs-wrapNeovim ? pkgs, }: +with lib; { - name ? "nvim", - plugins ? [ ], - packages ? [ ], + # NVIM_APPNAME - Defaults to 'nvim' if not set. + # If set to something else, this will also rename the binary. + appName ? "nvim", + # The Neovim package to wrap + neovim-unwrapped ? pkgs-wrapNeovim.neovim-unwrapped, + plugins ? [ ], # List of plugins + # List of dev plugins (will be bootstrapped) - useful for plugin developers + # { name = ; url = ; } + devPlugins ? [ ], + # Regexes for config files to ignore, relative to the nvim directory. + # e.g. [ "^plugin/neogit.lua" "^ftplugin/.*.lua" ] + ignoreConfigRegexes ? [ ], + extraPackages ? [ ], # Extra runtime dependencies (e.g. ripgrep, ...) + # The below arguments can typically be left as their defaults + # Additional lua packages (not plugins), e.g. from luarocks.org. + # e.g. p: [p.jsregexp] + extraLuaPackages ? p: [ ], + extraPython3Packages ? p: [ ], # Additional python 3 packages + withPython3 ? false, # Build Neovim with Python 3 support? + withRuby ? false, # Build Neovim with Ruby support? + withNodeJs ? false, # Build Neovim with NodeJS support? + withSqlite ? true, # Add sqlite? This is a dependency for some plugins + # You probably don't want to create vi or vim aliases + # if the appName is something different than "nvim" + viAlias ? appName == "nvim", # Add a "vi" binary to the build output as an alias? + vimAlias ? appName == "nvim", # Add a "vim" binary to the build output as an alias? wrapRc ? true, }: let - isNvim = (name == "nvim"); - nvimRtp = pkgs.stdenv.mkDerivation { + # This is the structure of a plugin definition. + # Each plugin in the `plugins` argument list can also be defined as this attrset + defaultPlugin = { + plugin = null; # e.g. nvim-lspconfig + config = null; # plugin config + # If `optional` is set to `false`, the plugin is installed in the 'start' packpath + # set to `true`, it is installed in the 'opt' packpath, and can be lazy loaded with + # ':packadd! {plugin-name} + optional = false; + runtime = { }; + }; + + externalPackages = extraPackages ++ (optionals withSqlite [ pkgs.sqlite ]); + + # Map all plugins to an attrset { plugin = ; config = ; optional = ; ... } + normalizedPlugins = map (x: defaultPlugin // (if x ? plugin then x else { plugin = x; })) plugins; + + # This nixpkgs util function creates an attrset + # that pkgs.wrapNeovimUnstable uses to configure the Neovim build. + neovimConfig = pkgs-wrapNeovim.neovimUtils.makeNeovimConfig { + inherit + extraPython3Packages + withPython3 + withRuby + withNodeJs + viAlias + vimAlias + ; + plugins = normalizedPlugins; + }; + + packDir = pkgs.neovimUtils.packDir { + myNeovimPackages = pkgs.neovimUtils.normalizedPluginsToVimPackage normalizedPlugins; + }; + + # This uses the ignoreConfigRegexes list to filter + # the nvim directory + nvimRtpSrc = + let + src = ../nvim; + in + lib.cleanSourceWith { + inherit src; + name = "nvim-rtp-src"; + filter = + path: tyoe: + let + srcPrefix = toString src + "/"; + relPath = lib.removePrefix srcPrefix (toString path); + in + lib.all (regex: builtins.match regex relPath == null) ignoreConfigRegexes; + }; + + # Split runtimepath into 3 directories: + # - lua, to be prepended to the rtp at the beginning of init.lua + # - nvim, containing plugin, ftplugin, ... subdirectories + # - after, to be sourced last in the startup initialization + # See also: https://neovim.io/doc/user/starting.html + nvimRtp = stdenv.mkDerivation { name = "nvim-rtp"; - src = ../nvim; - installPhase = '' + src = nvimRtpSrc; + + buildPhase = '' mkdir -p $out/ + ''; + + installPhase = '' cp -r . $out/ ''; }; - wrapperArgs = '' - --set NVIM_APPNAME "${name}" - --prefix PATH : "${lib.makeBinPath packages}" - --set LIBSQLITE_CLIB_PATH "${pkgs.sqlite.out}/lib/libsqlite3.so" - --set LIBSQLITE "${pkgs.sqlite.out}/lib/libsqlite3.so" - ''; + # The final init.lua content that we pass to the Neovim wrapper. + # It wraps the user init.lua, prepends the lua lib directory to the RTP + # and prepends the nvim and after directory to the RTP + initLua = '' + LAZY_OPTS = { + performance = { + reset_packpath = false, + rtp = { + reset = false, + disabled_plugins = { + "netrwPlugin", + "tutor", + }, + }, + }, + dev = { + path = "${packDir}/pack/myNeovimPackages/start", + patterns = {""}, + }, + checker = { + enabled = false, + }, + install = { missing = false, }, + spec = {{ import = "plugins" }}, + } + vim.opt.rtp:prepend('${nvimRtp}') + '' + + (builtins.readFile ../nvim/init.lua); - neovimConfig = pkgs.neovimUtils.makeNeovimConfig { - wrapRc = wrapRc; - withPython3 = false; - vimAlias = isNvim; - plugins = (pkgs.neovimUtils.normalizePlugins plugins); - customLuaRC = ''vim.opt.rtp:prepend('${nvimRtp}')'' + builtins.readFile ../nvim/init.lua; - extraMakeWrapperArgs = wrapperArgs; - }; + # Add arguments to the Neovim wrapper script + extraMakeWrapperArgs = builtins.concatStringsSep " " ( + # Set the NVIM_APPNAME environment variable + (optional ( + appName != "nvim" && appName != null && appName != "" + ) ''--set NVIM_APPNAME "${appName}"'') + # Add external packages to the PATH + ++ (optional (externalPackages != [ ]) ''--prefix PATH : "${makeBinPath externalPackages}"'') + # Set the LIBSQLITE_CLIB_PATH if sqlite is enabled + ++ (optional withSqlite ''--set LIBSQLITE_CLIB_PATH "${pkgs.sqlite.out}/lib/libsqlite3.so"'') + # Set the LIBSQLITE environment variable if sqlite is enabled + ++ (optional withSqlite ''--set LIBSQLITE "${pkgs.sqlite.out}/lib/libsqlite3.so"'') + ); + + luaPackages = neovim-unwrapped.lua.pkgs; + resolvedExtraLuaPackages = extraLuaPackages luaPackages; + + # Native Lua libraries + extraMakeWrapperLuaCArgs = + optionalString (resolvedExtraLuaPackages != [ ]) + ''--suffix LUA_CPATH ";" "${ + concatMapStringsSep ";" luaPackages.getLuaCPath resolvedExtraLuaPackages + }"''; + + # Lua libraries + extraMakeWrapperLuaArgs = + optionalString (resolvedExtraLuaPackages != [ ]) + ''--suffix LUA_PATH ";" "${ + concatMapStringsSep ";" luaPackages.getLuaPath resolvedExtraLuaPackages + }"''; + + # wrapNeovimUnstable is the nixpkgs utility function for building a Neovim derivation. + neovim-wrapped = pkgs-wrapNeovim.wrapNeovimUnstable neovim-unwrapped ( + neovimConfig + // { + luaRcContent = initLua; + wrapperArgs = + escapeShellArgs neovimConfig.wrapperArgs + + " " + + extraMakeWrapperArgs + + " " + + extraMakeWrapperLuaCArgs + + " " + + extraMakeWrapperLuaArgs; + wrapRc = wrapRc; + } + ); + + isCustomAppName = appName != null && appName != "nvim"; in -(pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped (neovimConfig)).overrideAttrs (oa: { - buildPhase = oa.buildPhase + lib.optionalString (!isNvim) ''mv $out/bin/nvim $out/bin/${name}''; +neovim-wrapped.overrideAttrs (oa: { + buildPhase = + oa.buildPhase + # If a custom NVIM_APPNAME has been set, rename the `nvim` binary + + lib.optionalString isCustomAppName '' + mv $out/bin/nvim $out/bin/${lib.escapeShellArg appName} + ''; }) diff --git a/nix/neovim-overlay.nix b/nix/neovim-overlay.nix index 3bb808e..204e118 100644 --- a/nix/neovim-overlay.nix +++ b/nix/neovim-overlay.nix @@ -1,20 +1,25 @@ # This overlay, when applied to nixpkgs, adds the final neovim derivation to nixpkgs. { inputs }: final: prev: +with final.pkgs.lib; let - mkNeovim = prev.callPackage ./mkNeovim.nix { pkgs = prev; }; - dart-nvim = inputs.dart.packages.x86_64-linux.default; + mkNeovim = prev.callPackage ./mkNeovim.nix { pkgs-wrapNeovim = prev; }; - plugins = with prev.vimPlugins; [ + plugins = with final.vimPlugins; [ blink-cmp blink-ripgrep-nvim conform-nvim dart-nvim diffview-nvim + eyeliner-nvim + friendly-snippets + lazy-nvim mini-nvim + nvim-autopairs nvim-lint nvim-lspconfig nvim-treesitter.withAllGrammars + nvim-treesitter-context nvim-treesitter-textobjects quicker-nvim refactoring-nvim @@ -22,13 +27,12 @@ let snacks-nvim ]; - basePackages = with prev; [ - sqlite + basePackages = with final; [ ripgrep fd ]; # Extra packages that should be included on nixos but don't need to be bundled - extraPackages = with prev; [ + extraPackages = with final; [ # linters yamllint jq @@ -40,28 +44,35 @@ let # LSPs gopls lua-language-server - nixd + nil basedpyright + + #other + jujutsu ]; in { nvim-pkg = mkNeovim { inherit plugins; - packages = basePackages ++ extraPackages; + extraPackages = basePackages ++ extraPackages; }; nvim-min-pkg = mkNeovim { inherit plugins; - packages = basePackages; + extraPackages = basePackages; }; + # This is meant to be used within a devshell. + # Instead of loading the lua Neovim configuration from + # the Nix store, it is loaded from $XDG_CONFIG_HOME/nvim-dev nvim-dev = mkNeovim { inherit plugins; - packages = basePackages ++ extraPackages; - name = "nvim-dev"; + extraPackages = basePackages ++ extraPackages; + appName = "nvim-dev"; wrapRc = false; }; + # This can be symlinked in the devShell's shellHook nvim-luarc-json = final.mk-luarc-json { inherit plugins; }; diff --git a/nix/plugin-overlay.nix b/nix/plugin-overlay.nix new file mode 100644 index 0000000..938ba67 --- /dev/null +++ b/nix/plugin-overlay.nix @@ -0,0 +1,18 @@ +{ inputs, ... }: +final: prev: +let + mkNvimPlugin = + src: pname: + prev.vimUtils.buildVimPlugin { + inherit pname src; + version = src.lastModifiedDate; + }; +in +{ + vimPlugins = prev.vimPlugins.extend ( + final': prev': { + dart-nvim = inputs.dart.packages.${prev.system}.default; + nvim-treesitter-textobjects = mkNvimPlugin inputs.nvim-treesitter-textobjects "nvim-treesitter-textobjects"; + } + ); +} diff --git a/nvim/init.lua b/nvim/init.lua index bc4155b..622287b 100644 --- a/nvim/init.lua +++ b/nvim/init.lua @@ -1,65 +1,34 @@ -vim.cmd('colorscheme iofq') - vim.g.mapleader = ' ' -vim.opt.autowrite = true -vim.opt.backspace = 'indent,eol,start' -vim.opt.confirm = true -vim.opt.completeopt = 'menuone,popup,noselect,fuzzy' -vim.opt.diffopt = 'internal,filler,closeoff,inline:char' -vim.opt.expandtab = true -- insert tabs as spaces -vim.opt.inccommand = 'split' -- incremental live completion -vim.opt.laststatus = 1 -- statusline only if split -vim.opt.nrformats:append('alpha') -- let Ctrl-a do letters as well -vim.opt.path:append('**') -- enable fuzzy :find ing -vim.opt.relativenumber = true -vim.opt.shadafile = 'NONE' -- disable shada (unless session) -vim.opt.shiftwidth = 0 -- >> shifts by tabstop -vim.opt.showmatch = true -- highlight matching brackets -vim.opt.showmode = true -vim.opt.signcolumn = 'no' -vim.opt.softtabstop = -1 -- backspace removes tabstop -vim.opt.swapfile = false -vim.opt.tabstop = 2 -- 2 space tabs are based -vim.opt.updatetime = 250 -- decrease update time -vim.opt.virtualedit = 'onemore' -vim.opt.winborder = 'rounded' - --- Configure Neovim diagnostic messages -vim.diagnostic.config { - virtual_text = true, - underline = true, - severity_sort = true, - float = { - focusable = false, - style = 'minimal', - source = 'if_many', - }, -} - -vim.lsp.enable { - 'nixd', - 'phpactor', - 'gopls', - 'lua_ls', - 'basedpyright', -} - -local map = vim.keymap.set -map('n', '\\t', function() -- Switch tab length on the fly - vim.o.tabstop = vim.o.tabstop == 8 and 2 or 2 * vim.o.tabstop - vim.notify('tabstop: ' .. vim.o.tabstop) -end) -map({ 'v', 'i' }, 'wq', 'l') -map('v', '<', '', '>gv') -map('n', 'n', 'nzz', { noremap = true }) -map('n', 'N', 'Nzz', { noremap = true }) -map('n', '', 'zz', { noremap = true }) -map('n', '', 'zz', { noremap = true }) -map('n', 'gq', vim.cmd.bdelete, { noremap = true }) -map('n', 'gQ', function() - vim.cmd('bufdo bdelete') -end, { noremap = true }) - -require('autocmd') -require('plugins') +-- If lazy_opts is set, we're running in wrapped neovim via nix +if not LAZY_OPTS then + -- Bootstrapping lazy.nvim + local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' + if not vim.loop.fs_stat(lazypath) then + vim.fn.system { + 'git', + 'clone', + '--filter=blob:none', + 'https://github.com/folke/lazy.nvim.git', + '--branch=stable', -- latest stable release + lazypath, + } + end + vim.opt.rtp:prepend(lazypath) + LAZY_OPTS = { + spec = { { import = 'plugins' } }, + performance = { + reset_packpath = false, + rtp = { + reset = false, + disabled_plugins = { + 'netrwPlugin', + 'tutor', + }, + }, + }, + } +end +vim.cmd('packadd cfilter') +vim.cmd('colorscheme iofq') +require('lazy').setup(LAZY_OPTS) +require('config') diff --git a/nvim/lsp/lua_ls.lua b/nvim/lsp/lua_ls.lua index f71f2bd..4be10e8 100644 --- a/nvim/lsp/lua_ls.lua +++ b/nvim/lsp/lua_ls.lua @@ -19,6 +19,9 @@ return { end, settings = { Lua = { + codeLens = { + enable = true, + }, hint = { enable = true, arrayIndex = 'Enable', diff --git a/nvim/lua/autocmd.lua b/nvim/lua/autocmd.lua deleted file mode 100644 index 41dd900..0000000 --- a/nvim/lua/autocmd.lua +++ /dev/null @@ -1,92 +0,0 @@ -local cmd = vim.api.nvim_create_autocmd --- open :h in buffers -cmd('FileType', { - group = vim.api.nvim_create_augroup('help', { clear = true }), - pattern = 'help', - callback = function(_) - vim.cmd.only() - vim.keymap.set('n', 'q', vim.cmd.bdelete, { noremap = true }) - vim.bo.buflisted = false - end, -}) - --- resize splits if window got resized -cmd({ 'VimResized' }, { - group = vim.api.nvim_create_augroup('resize_splits', { clear = true }), - callback = function() - vim.cmd('tabdo wincmd =') - vim.cmd('tabnext ' .. vim.fn.tabpagenr()) - end, -}) - --- Check if we need to reload the file when it changed -cmd({ 'FocusGained', 'TermClose', 'TermLeave' }, { - group = vim.api.nvim_create_augroup('check_reload', { clear = true }), - callback = function() - if vim.o.buftype ~= 'nofile' then - vim.cmd('checktime') - end - end, -}) - --- Init treesitter -cmd('FileType', { - callback = function(event) - local bufnr = event.buf - - vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()" - pcall(vim.treesitter.start, bufnr) - - vim.keymap.set({ 'v', 'n' }, ']]', function() - require('nvim-treesitter-textobjects.move').goto_next_start('@function.outer', 'textobjects') - end, { buffer = bufnr }) - vim.keymap.set({ 'v', 'n' }, '[[', function() - require('nvim-treesitter-textobjects.move').goto_previous_start('@function.outer', 'textobjects') - end, { buffer = bufnr }) - vim.keymap.set({ 'v', 'n' }, ']a', function() - require('nvim-treesitter-textobjects.move').goto_next_start('@parameter.inner', 'textobjects') - end, { buffer = bufnr }) - vim.keymap.set({ 'v', 'n' }, '[a', function() - require('nvim-treesitter-textobjects.move').goto_previous_start('@parameter.inner', 'textobjects') - end, { buffer = bufnr }) - vim.keymap.set({ 'v', 'n' }, ']A', function() - require('nvim-treesitter-textobjects.swap').swap_next('@parameter.inner') - end, { buffer = bufnr }) - vim.keymap.set({ 'v', 'n' }, '[A', function() - require('nvim-treesitter-textobjects.swap').swap_previous('@parameter.inner') - end, { buffer = bufnr }) - end, -}) - --- Init LSP -cmd('LspAttach', { - group = vim.api.nvim_create_augroup('UserLspConfig', {}), - callback = function(ev) - local client = vim.lsp.get_client_by_id(ev.data.client_id) - if not client then - return - end - vim.keymap.set('n', 'gO', function() - Snacks.picker.lsp_symbols { focus = 'list' } - end, { buffer = ev.buf }) - - vim.keymap.set('n', 'grh', function() - vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) - end, { buffer = ev.buf }) - vim.keymap.set('n', 'grl', vim.lsp.codelens.run, { buffer = ev.buf }) - - vim.keymap.set('n', 'gre', vim.diagnostic.setloclist, { buffer = ev.buf }) - vim.keymap.set('n', 'grE', vim.diagnostic.setqflist, { buffer = ev.buf }) - - -- Auto-refresh code lenses - if client:supports_method('textDocument/codeLens') or client.server_capabilities.codeLensProvider then - vim.lsp.codelens.refresh { bufnr = ev.buf } - cmd({ 'InsertLeave', 'TextChanged' }, { - callback = function() - vim.lsp.codelens.refresh { bufnr = ev.buf } - end, - buffer = ev.buf, - }) - end - end, -}) diff --git a/nvim/lua/config/autocmd.lua b/nvim/lua/config/autocmd.lua new file mode 100644 index 0000000..0ccfabd --- /dev/null +++ b/nvim/lua/config/autocmd.lua @@ -0,0 +1,112 @@ +-- create undopath +local undopath = vim.fn.stdpath('data') .. 'undo' +vim.api.nvim_create_autocmd('VimEnter', { + command = 'silent !mkdir -p ' .. undopath, + group = vim.api.nvim_create_augroup('Init', {}), +}) + +-- open :h in buffers +vim.api.nvim_create_autocmd('BufWinEnter', { + pattern = '*', + callback = function(event) + if vim.bo[event.buf].filetype == 'help' then + vim.cmd.only() + vim.keymap.set('n', 'q', vim.cmd.bdelete, { noremap = true, silent = true }) + vim.bo.buflisted = false + end + end, +}) + +-- Allow basic deletion in qflist +vim.api.nvim_create_autocmd({ 'FileType' }, { + pattern = 'qf', + callback = function() + vim.keymap.set({ 'n', 'i' }, 'dd', function() + local ln = vim.fn.line('.') + local qf = vim.fn.getqflist() + if #qf == 0 then + return + end + table.remove(qf, ln) + vim.fn.setqflist(qf, 'r') + vim.cmd('copen') + -- move cursor to stay at same index (or up one if at EOF) + vim.api.nvim_win_set_cursor(vim.fn.win_getid(), { ln < #qf and ln or math.max(ln - 1, 1), 0 }) + require('quicker').refresh() + end, { buffer = true }) + end, +}) + +-- resize splits if window got resized +vim.api.nvim_create_autocmd({ 'VimResized' }, { + group = vim.api.nvim_create_augroup('resize_splits', { clear = true }), + callback = function() + local current_tab = vim.fn.tabpagenr() + vim.cmd('tabdo wincmd =') + vim.cmd('tabnext ' .. current_tab) + end, +}) + +-- Check if we need to reload the file when it changed +vim.api.nvim_create_autocmd({ 'FocusGained', 'TermClose', 'TermLeave' }, { + group = vim.api.nvim_create_augroup('check_reload', { clear = true }), + callback = function() + if vim.o.buftype ~= 'nofile' then + vim.cmd('checktime') + end + end, +}) + +-- Init treesitter +vim.api.nvim_create_autocmd('FileType', { + callback = function(event) + local bufnr = event.buf + local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) + + if filetype == '' then + return + end + + local parser_name = vim.treesitter.language.get_lang(filetype) + if not parser_name then + return + end + local parser_installed = pcall(vim.treesitter.get_parser, bufnr, parser_name) + if not parser_installed then + return + end + + local function map(lhs, rhs, opts) + if lhs == '' then + return + end + opts = vim.tbl_deep_extend('force', { silent = true }, opts or {}) + vim.keymap.set({ 'v', 'n' }, lhs, rhs, opts) + end + + vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()" + vim.treesitter.start() + + map('[c', function() + require('treesitter-context').go_to_context(vim.v.count1) + end, { buffer = bufnr, desc = 'jump to TS context' }) + map(']f', function() + require('nvim-treesitter-textobjects.move').goto_next_start('@function.outer', 'textobjects') + end, { buffer = bufnr, desc = 'next function def' }) + map('[f', function() + require('nvim-treesitter-textobjects.move').goto_previous_start('@function.outer', 'textobjects') + end, { buffer = bufnr, desc = 'prev function def' }) + map(']a', function() + require('nvim-treesitter-textobjects.move').goto_next_start('@parameter.inner', 'textobjects') + end, { buffer = bufnr, desc = 'next param def' }) + map('[a', function() + require('nvim-treesitter-textobjects.move').goto_previous_start('@parameter.inner', 'textobjects') + end, { buffer = bufnr, desc = 'prev param def' }) + map('a]', function() + require('nvim-treesitter-textobjects.swap').swap_next('@parameter.inner') + end, { buffer = bufnr, desc = 'swap next arg' }) + map('a[', function() + require('nvim-treesitter-textobjects.swap').swap_previous('@parameter.inner') + end, { buffer = bufnr, desc = 'swap prev arg' }) + end, +}) diff --git a/nvim/lua/config/init.lua b/nvim/lua/config/init.lua new file mode 100644 index 0000000..f9ba85f --- /dev/null +++ b/nvim/lua/config/init.lua @@ -0,0 +1,37 @@ +vim.opt.autowrite = true +vim.opt.backspace = 'indent,eol,start' +vim.opt.confirm = true +vim.opt.diffopt = 'internal,filler,closeoff,inline:char' +vim.opt.expandtab = true -- insert tabs as spaces +vim.opt.inccommand = 'split' -- incremental live completion +vim.opt.laststatus = 1 -- statusline only if split +vim.opt.nrformats:append('alpha') -- let Ctrl-a do letters as well +vim.opt.path:append('**') -- enable fuzzy :find ing +vim.opt.relativenumber = true +vim.opt.shadafile = 'NONE' -- disable shada (unless session) +vim.opt.shiftwidth = 0 -- >> shifts by tabstop +vim.opt.showmatch = true -- highlight matching brackets +vim.opt.showmode = true +vim.opt.signcolumn = 'no' +vim.opt.softtabstop = -1 -- backspace removes tabstop +vim.opt.swapfile = false +vim.opt.tabstop = 2 -- 2 space tabs are based +vim.opt.updatetime = 250 -- decrease update time +vim.opt.virtualedit = 'onemore' +vim.opt.winborder = 'rounded' + +-- Configure Neovim diagnostic messages +vim.diagnostic.config { + virtual_text = true, + underline = true, + severity_sort = true, + float = { + focusable = false, + style = 'minimal', + source = 'if_many', + }, +} +vim.schedule(function() + require('config.autocmd') + require('config.keymaps') +end) diff --git a/nvim/lua/config/keymaps.lua b/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..62c630c --- /dev/null +++ b/nvim/lua/config/keymaps.lua @@ -0,0 +1,18 @@ +-- Switch tab length on the fly +vim.keymap.set('n', '\\t', function() + vim.o.tabstop = vim.o.tabstop == 8 and 2 or 2 * vim.o.tabstop + vim.notify('tabstop: ' .. vim.o.tabstop) +end, { silent = true, desc = 'toggle tabstop' }) +vim.keymap.set({ 'v', 'i' }, 'wq', 'l', { noremap = true, silent = true }) +vim.keymap.set('v', '<', '', '>gv') +vim.keymap.set('n', 'n', 'nzz', { noremap = true }) +vim.keymap.set('n', 'N', 'Nzz', { noremap = true }) +vim.keymap.set('n', '', 'zz', { noremap = true }) +vim.keymap.set('n', '', 'zz', { noremap = true }) +vim.keymap.set('v', '', ":m '>+1gv=gv", { desc = 'move selection down' }) +vim.keymap.set('v', '', ":m '<-2gv=gv", { desc = 'move selection up' }) +vim.keymap.set('n', 'gq', vim.cmd.bdelete, { noremap = true, silent = true, desc = 'close buffer' }) +vim.keymap.set('n', 'gQ', function() + vim.cmd('bufdo bdelete') +end, { noremap = true, silent = true, desc = 'close all buffers' }) diff --git a/nvim/lua/lib/minidiff_jj.lua b/nvim/lua/lib/minidiff_jj.lua deleted file mode 100644 index 0294773..0000000 --- a/nvim/lua/lib/minidiff_jj.lua +++ /dev/null @@ -1,103 +0,0 @@ -local diff = require('mini.diff') -local M = { - cache = {}, -} - -M.get_buf_realpath = function(buf_id) - local path = vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' - local cwd, basename = vim.fn.fnamemodify(path, ':h'), vim.fn.fnamemodify(path, ':t') - return path, cwd, basename -end - -M.jj_start_watching_tree_state = function(buf_id, path) - local on_not_in_jj = vim.schedule_wrap(function() - if not vim.api.nvim_buf_is_valid(buf_id) then - M.cache[buf_id] = nil - return false - end - diff.fail_attach(buf_id) - M.cache[buf_id] = {} - end) - - vim.system( - { 'jj', 'workspace', 'root', '--ignore-working-copy' }, - {cwd = vim.fn.fnamemodify(path, ':h')}, - function(obj) - if obj.code ~= 0 then - return on_not_in_jj() - end - - -- Set up index watching - local root = obj.stdout:gsub('\n+$', '') .. '/.jj/working_copy/tree_state' - local buf_fs_event = vim.loop.new_fs_event() - - buf_fs_event:start(root, { stat = true }, function() - M.jj_set_ref_text(buf_id) - end) - M.cache[buf_id] = { fs_event = buf_fs_event } - - -- Set reference text immediately - M.jj_set_ref_text(buf_id) - end - ) -end - -M.jj_set_ref_text = vim.schedule_wrap(function(buf_id) - if not vim.api.nvim_buf_is_valid(buf_id) then - return - end - - local buf_set_ref_text = function(text) - pcall(diff.set_ref_text, buf_id, text) - end - - -- react to possible rename - local path, cwd, basename = M.get_buf_realpath(buf_id) - if path == '' then - return buf_set_ref_text {} - end - - vim.system( - { 'jj', 'file', 'show', '--no-pager', '--ignore-working-copy', '-r', '@-', './' .. basename }, - { cwd = cwd }, - vim.schedule_wrap(function(obj) - if obj.code ~= 0 then return buf_set_ref_text {} end - buf_set_ref_text(obj.stdout:gsub('\r\n', '\n')) - end) - ) -end) - -M.jj_invalidate_cache = function(buf_id) - pcall(vim.loop.fs_event_stop, M.cache[buf_id].fs_event) - M.cache[buf_id] = nil -end - -M.gen_source = function() - local attach = function(buf_id) - -- Try attaching to a buffer only once - if M.cache[buf_id] ~= nil then - return false - end - -- - Possibly resolve symlinks to get data from the original repo - local path = M.get_buf_realpath(buf_id) - if path == '' then - return false - end - - M.cache[buf_id] = {} - M.jj_start_watching_tree_state(buf_id, path) - end - - local detach = function(buf_id) - M.jj_invalidate_cache(buf_id) - end - - - return { - name = 'jj', - attach = attach, - detach = detach, - apply_hunks = function(_, _) end -- staging does not apply for jj - } -end -return M diff --git a/nvim/lua/lib/snacks_jj.lua b/nvim/lua/lib/snacks_jj.lua deleted file mode 100644 index 639ecfd..0000000 --- a/nvim/lua/lib/snacks_jj.lua +++ /dev/null @@ -1,52 +0,0 @@ -local M = {} - -function M.status() - local function get_files() - local status_raw = vim.fn.system('jj diff --no-pager --quiet --summary') - local files = {} - - for status in status_raw:gmatch('[^\r\n]+') do - local state, file = string.match(status, '^(%a)%s(.+)$') - - if state and file then - local hl = '' - if state == 'A' then - hl = 'SnacksPickerGitStatusAdded' - elseif state == 'M' then - hl = 'SnacksPickerGitStatusModified' - elseif state == 'D' then - hl = 'SnacksPickerGitStatusDeleted' - elseif state == 'R' then - hl = 'SnacksPickerGitStatusRenamed' - file = string.match(file, '{.-=>%s*(.-)}') - end - - local diff = vim.fn.system('jj diff ' .. file .. ' --no-pager --stat --git') - table.insert(files, { - file = file, - filename_hl = hl, - diff = diff, - }) - end - end - - return files - end - - Snacks.picker.pick { - source = 'jj_status', - items = get_files(), - format = 'file', - title = 'jj status', - preview = function(ctx) - if ctx.item.file then - Snacks.picker.preview.diff(ctx) - else - ctx.preview:reset() - ctx.preview:set_title('No preview') - end - end, - } -end - -return M diff --git a/nvim/lua/plugins.lua b/nvim/lua/plugins.lua deleted file mode 100644 index f3e62b8..0000000 --- a/nvim/lua/plugins.lua +++ /dev/null @@ -1,256 +0,0 @@ -local map = vim.keymap.set - -require('mini.basics').setup { mappings = { windows = true } } -require('mini.icons').setup() - -require('dart').setup { - tabline = { - label_marked_fg = 'cyan', - }, -} - -require('snacks').setup { - bigfile = { enabled = true }, - terminal = { enabled = true }, - indent = { enabled = true }, - input = { enabled = true }, - notifier = { enabled = true }, - styles = { - notification = { - wo = { wrap = true }, - }, - }, - picker = { - enabled = true, - matcher = { - frecency = true, - cwd_bonus = true, - }, - layout = 'ivy_split', - sources = { - grep = { hidden = true }, - lsp_symbols = { - filter = { default = true }, - layout = 'left', - }, - smart = { - multi = { - 'buffers', - { source = 'files', hidden = true }, - { source = 'git_files', untracked = true }, - }, - }, - }, - }, -} - -map({ 'n', 't' }, '', Snacks.terminal.toggle) -map('n', 'ff', Snacks.picker.smart) -map('n', '', Snacks.picker.smart) -map('n', 'fa', Snacks.picker.grep) -map('n', 'f8', Snacks.picker.grep_word) -map('n', 'f?', Snacks.picker.pickers) -map('n', 'fu', Snacks.picker.undo) -map('n', 'fj', Snacks.picker.jumps) -map('n', 'f.', Snacks.picker.resume) -map('n', 'fb', Snacks.picker.buffers) -map('n', 'fq', Snacks.picker.qflist) -map('n', 'jf', require('lib.snacks_jj').status) - -vim.schedule(function() - require('nvim-treesitter').setup() - require('nvim-treesitter-textobjects').setup() - require('render-markdown').setup() - - require('refactoring').setup() - map('n', 'rr', require('refactoring').select_refactor) - map('n', 'rv', function() - require('refactoring').refactor('Inline Variable') - end) - - require('quicker').setup() - map('n', 'qf', function() - require('quicker').toggle { max_height = 20 } - end) - - local close = { { { 'n' }, 'q', vim.cmd.DiffviewClose, { desc = 'Close Diffview' } } } - require('diffview').setup { - enhanced_diff_hl = true, - default_args = { DiffviewOpen = { '--imply-local' } }, - view = { - merge_tool = { - layout = 'diff4_mixed', - disable_diagnostics = true, - }, - }, - keymaps = { - view = close, - file_panel = close, - file_history_panel = close, - }, - } - - map('n', 'nb', vim.cmd.DiffviewOpen) - map('n', 'nH', vim.cmd.DiffviewFileHistory) - map('n', 'nh', 'DiffviewFileHistory %') - map('n', 'go', function() - local id = vim.fn.expand('') - vim.cmd('DiffviewOpen ' .. id .. '~1' .. '..' .. id) - end) - - require('conform').setup { - notify_no_formatters = false, - formatters_by_ft = { - json = { 'jq' }, - lua = { 'stylua' }, - python = { 'ruff' }, - nix = { 'nixfmt' }, - fish = { 'fish_indent' }, - ['*'] = { 'trim_whitespace' }, - }, - format_on_save = function(bufnr) - -- Disable with a global or buffer-local variable - if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then - return - end - return { timeout_ms = 1500, lsp_format = 'fallback' } - end, - } - map('n', '\\f', function() - vim.b.disable_autoformat = not vim.b.disable_autoformat - vim.notify(string.format('Buffer formatting disabled: %s', vim.b.disable_autoformat)) - end) - map('n', '\\F', function() - vim.g.disable_autoformat = not vim.g.disable_autoformat - vim.notify(string.format('Global formatting disabled: %s', vim.g.disable_autoformat)) - end) - - require('lint').linters_by_ft = { - docker = { 'hadolint' }, - yaml = { 'yamllint' }, - sh = { 'shellcheck' }, - go = { 'golangcilint' }, - ruby = { 'rubocop' }, - fish = { 'fish' }, - bash = { 'bash' }, - nix = { 'nix' }, - php = { 'php' }, - } - vim.api.nvim_create_autocmd({ 'BufWritePost', 'InsertLeave' }, { - group = vim.api.nvim_create_augroup('lint', { clear = true }), - callback = function() - if vim.bo.modifiable then - require('lint').try_lint(nil, { ignore_errors = true }) - end - end, - }) - - vim.treesitter.language.register('markdown', 'blink-cmp-documentation') - require('blink.cmp').setup { - enabled = function() - return not vim.tbl_contains({ 'snacks_picker_input' }, vim.bo.filetype) - end, - sources = { - default = { 'lsp', 'snippets', 'ripgrep', 'buffer' }, - providers = { - lsp = { fallbacks = {} }, -- include buffer even when LSP is active - ripgrep = { - module = 'blink-ripgrep', - name = 'rg', - score_offset = -10, - async = true, - }, - }, - }, - cmdline = { - completion = { menu = { auto_show = true } }, - }, - completion = { - documentation = { auto_show = true }, - menu = { - draw = { - columns = { - { 'label', 'label_description', gap = 1 }, - { 'source_name', 'kind', gap = 1 }, - }, - }, - }, - }, - signature = { - enabled = true, - trigger = { show_on_insert = true }, - }, - } -end) - -vim.schedule(function() - require('mini.align').setup() - require('mini.pairs').setup() - require('mini.surround').setup() - require('mini.splitjoin').setup { detect = { separator = '[,;\n]' } } - - local ai = require('mini.ai') - ai.setup { - n_lines = 300, - custom_textobjects = { - i = require('mini.extra').gen_ai_spec.indent(), - a = ai.gen_spec.treesitter { a = '@parameter.outer', i = '@parameter.inner' }, - f = ai.gen_spec.treesitter { a = '@function.outer', i = '@function.inner' }, - }, - } - - require('mini.git').setup() - map('n', 'gb', 'Git blame -- %') - - local jump = require('mini.jump2d') - jump.setup { - view = { n_steps_ahead = 1, dim = true }, - spotter = jump.gen_spotter.vimpattern(), - } - - local diff = require('mini.diff') - diff.setup { - source = { - require('lib.minidiff_jj').gen_source(), - diff.gen_source.git(), - }, - } - map('n', 'gp', MiniDiff.toggle_overlay) - - require('mini.files').setup { - mappings = { go_in_plus = '' }, - windows = { - preview = true, - width_preview = 50, - }, - } - map('n', 'nc', function() - MiniFiles.open(vim.api.nvim_buf_get_name(0), false) -- open current buffer's dir - MiniFiles.reveal_cwd() - end) - vim.api.nvim_create_autocmd('User', { - pattern = 'MiniFilesBufferCreate', - callback = function(args) - map('n', 'nc', function() - MiniFiles.synchronize() - MiniFiles.close() - end, { buffer = args.data.buf_id }) - map('n', '`', function() - local _, cur_entry_path = pcall(MiniFiles.get_fs_entry().path) - local cur_directory = vim.fs.dirname(cur_entry_path) - if cur_directory ~= '' then - vim.fn.chdir(cur_directory) - end - end, { buffer = args.data.buf_id }) - end, - }) - - -- pass file rename events to LSP - vim.api.nvim_create_autocmd('User', { - group = vim.api.nvim_create_augroup('snacks_rename', { clear = true }), - pattern = 'MiniFilesActionRename', - callback = function(event) - Snacks.rename.on_rename_file(event.data.from, event.data.to) - end, - }) -end) diff --git a/nvim/lua/plugins/completion.lua b/nvim/lua/plugins/completion.lua new file mode 100644 index 0000000..20480dc --- /dev/null +++ b/nvim/lua/plugins/completion.lua @@ -0,0 +1,87 @@ +return { + { + 'saghen/blink.cmp', + event = 'VeryLazy', + dependencies = { + 'mikavilpas/blink-ripgrep.nvim', + }, + opts = { + enabled = function() + return not vim.tbl_contains({ 'snacks_picker_input' }, vim.bo.filetype) + end, + fuzzy = { + sorts = { + 'exact', + 'score', + 'sort_text', + }, + }, + sources = { + default = { + 'lsp', + 'path', + 'snippets', + 'ripgrep', + 'buffer', + }, + providers = { + lsp = { + fallbacks = {}, -- include buffer even when LSP is active + score_offset = 10, + }, + snippets = { + score_offset = -10, + }, + path = { + opts = { + get_cwd = function(_) + return vim.fn.getcwd() -- use nvim pwd instead of current file pwd + end, + }, + }, + ripgrep = { + module = 'blink-ripgrep', + name = 'rg', + score_offset = -10, + async = true, + }, + }, + }, + cmdline = { + completion = { + menu = { + auto_show = true, + }, + }, + }, + completion = { + documentation = { + auto_show = true, + auto_show_delay_ms = 500, + }, + menu = { + draw = { + treesitter = { 'lsp' }, + columns = { + { 'label', 'label_description', gap = 1 }, + { 'source_name', 'kind', gap = 1 }, + }, + }, + }, + trigger = { + show_on_keyword = true, + }, + }, + signature = { + enabled = true, + trigger = { + show_on_insert = true, + }, + }, + }, + config = function(_, opts) + require('blink.cmp').setup(opts) + vim.treesitter.language.register('markdown', 'blink-cmp-documentation') + end, + }, +} diff --git a/nvim/lua/plugins/lib/minidiff_jj.lua b/nvim/lua/plugins/lib/minidiff_jj.lua new file mode 100644 index 0000000..d5b93d8 --- /dev/null +++ b/nvim/lua/plugins/lib/minidiff_jj.lua @@ -0,0 +1,162 @@ +local diff = require('mini.diff') +local M = { + cache = {}, +} + +M.get_buf_realpath = function(buf_id) + return vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' +end + +M.jj_start_watching_tree_state = function(buf_id, path) + local stdout = vim.loop.new_pipe() + local args = { 'workspace', 'root', '--ignore-working-copy' } + local spawn_opts = { + args = args, + cwd = vim.fn.fnamemodify(path, ':h'), + stdio = { nil, stdout, nil }, + } + + local on_not_in_jj = vim.schedule_wrap(function() + if not vim.api.nvim_buf_is_valid(buf_id) then + M.cache[buf_id] = nil + return false + end + diff.fail_attach(buf_id) + M.cache[buf_id] = {} + end) + + local process, stdout_feed = nil, {} + local on_exit = function(exit_code) + process:close() + + -- Watch index only if there was no error retrieving path to it + if exit_code ~= 0 or stdout_feed[1] == nil then + return on_not_in_jj() + end + + -- Set up index watching + local jj_dir_path = table.concat(stdout_feed, ''):gsub('\n+$', '') .. '/.jj/working_copy' + M.jj_setup_tree_state_watch(buf_id, jj_dir_path) + + -- Set reference text immediately + M.jj_set_ref_text(buf_id) + end + + process = vim.loop.spawn('jj', spawn_opts, on_exit) + M.jj_read_stream(stdout, stdout_feed) +end + +M.jj_setup_tree_state_watch = function(buf_id, jj_dir_path) + local buf_fs_event, timer = vim.loop.new_fs_event(), vim.loop.new_timer() + local buf_jj_set_ref_text = function() + M.jj_set_ref_text(buf_id) + end + + local watch_tree_state = function(_, filename, _) + if filename ~= 'tree_state' then + return + end + -- Debounce to not overload during incremental staging (like in script) + timer:stop() + timer:start(50, 0, buf_jj_set_ref_text) + end + buf_fs_event:start(jj_dir_path, { stat = true }, watch_tree_state) + + M.jj_invalidate_cache(M.cache[buf_id]) + M.cache[buf_id] = { fs_event = buf_fs_event, timer = timer } +end +M.jj_set_ref_text = vim.schedule_wrap(function(buf_id) + if not vim.api.nvim_buf_is_valid(buf_id) then + return + end + + local buf_set_ref_text = vim.schedule_wrap(function(text) + pcall(diff.set_ref_text, buf_id, text) + end) + + -- NOTE: Do not cache buffer's name to react to its possible rename + local path = M.get_buf_realpath(buf_id) + if path == '' then + return buf_set_ref_text {} + end + local cwd, basename = vim.fn.fnamemodify(path, ':h'), vim.fn.fnamemodify(path, ':t') + + -- Set + local stdout = vim.loop.new_pipe() + local spawn_opts = { + args = { 'file', 'show', '--no-pager', '--ignore-working-copy', '-r', '@-', './' .. basename }, + cwd = cwd, + stdio = { nil, stdout, nil }, + } + + local process, stdout_feed = nil, {} + process = vim.loop.spawn('jj', spawn_opts, function(exit_code) + process:close() + + if exit_code ~= 0 or stdout_feed[1] == nil then + return buf_set_ref_text {} + end + + -- Set reference text accounting for possible 'crlf' end of line in index + local text = table.concat(stdout_feed, ''):gsub('\r\n', '\n') + buf_set_ref_text(text) + end) + + M.jj_read_stream(stdout, stdout_feed) +end) + +M.jj_read_stream = function(stream, feed) + local callback = function(err, data) + if data ~= nil then + return table.insert(feed, data) + end + if err then + feed[1] = nil + end + stream:close() + end + stream:read_start(callback) +end + +M.jj_invalidate_cache = function(cache) + if cache == nil then + return + end + pcall(vim.loop.fs_event_stop, cache.fs_event) + pcall(vim.loop.timer_stop, cache.timer) +end + +M.gen_source = function() + local attach = function(buf_id) + -- Try attaching to a buffer only once + if M.cache[buf_id] ~= nil then + return false + end + -- - Possibly resolve symlinks to get data from the original repo + local path = M.get_buf_realpath(buf_id) + if path == '' then + return false + end + + M.cache[buf_id] = {} + M.jj_start_watching_tree_state(buf_id, path) + end + + local detach = function(buf_id) + local cache = M.cache[buf_id] + M.cache[buf_id] = nil + M.jj_invalidate_cache(cache) + end + + local apply_hunks = function(_, _) + -- staging does not apply for jj + end + + return { + name = 'jj', + attach = attach, + detach = detach, + apply_hunks = apply_hunks, + } +end +return M diff --git a/nvim/lua/plugins/lib/session_jj.lua b/nvim/lua/plugins/lib/session_jj.lua new file mode 100644 index 0000000..5e81803 --- /dev/null +++ b/nvim/lua/plugins/lib/session_jj.lua @@ -0,0 +1,71 @@ +local M = {} + +M.setup = function() + local id = M.get_id() + if M.check_exists(id) then + vim.notify('Existing session for ' .. id) + end + + vim.keymap.set('n', 'fs', function() + require('plugins.lib.session_jj').load() + end, { noremap = true, desc = 'mini session select' }) +end + +M.get_id = function() + local jj_root = vim.system({ 'jj', 'workspace', 'root' }):wait() + + if jj_root.code ~= 0 then + return + end + + local result = vim + .system({ + 'jj', + 'log', + '-r', + 'latest(heads(::@ & bookmarks()))', + '--template', + 'bookmarks', + '--no-pager', + '--no-graph', + }) + :wait() + local branch = vim.trim(string.gsub(result.stdout, '[\n*]', '')) -- trim newlines and unpushed indicator + local root = vim.trim(string.gsub(jj_root.stdout, '\n', '')) + local id = string.gsub(string.format('jj:%s:%s', root, branch), '[./]', '-') -- slugify + return id +end + +M.check_exists = function(id) + for name, _ in pairs(MiniSessions.detected) do + if name == id then + return true + end + end + return false +end + +M.load = function() + local id = M.get_id() + if id == '' then + return + end + vim.opt.shadafile = vim.fn.stdpath('data') .. '/myshada/' .. id .. '.shada' + if M.check_exists(id) then + vim.ui.select({ + 'Yes', + 'No', + }, { prompt = 'Session found at ' .. id .. ', load it?' }, function(c) + if c == 'Yes' then + -- load session (buffers, etc) as well as shada (marks) + MiniSessions.read(id) + vim.notify('loaded jj session: ' .. id) + end + end) + else + MiniSessions.write(id) + end +end + +_G.M = M +return M diff --git a/nvim/lua/plugins/lib/snacks_jj.lua b/nvim/lua/plugins/lib/snacks_jj.lua new file mode 100644 index 0000000..da442eb --- /dev/null +++ b/nvim/lua/plugins/lib/snacks_jj.lua @@ -0,0 +1,121 @@ +local M = {} + +function M.status() + local function get_files() + local status_raw = vim.fn.system('jj diff --no-pager --quiet --summary') + local files = {} + + for status in status_raw:gmatch('[^\r\n]+') do + local state, text = string.match(status, '^(%a)%s(.+)$') + + if state and text then + local file = text + + local hl = '' + if state == 'A' then + hl = 'SnacksPickerGitStatusAdded' + elseif state == 'M' then + hl = 'SnacksPickerGitStatusModified' + elseif state == 'D' then + hl = 'SnacksPickerGitStatusDeleted' + elseif state == 'R' then + hl = 'SnacksPickerGitStatusRenamed' + file = string.match(text, '{.-=>%s*(.-)}') + end + + local diff = vim.fn.system('jj diff ' .. file .. ' --ignore-working-copy --no-pager --stat --git') + table.insert(files, { + text = text, + file = file, + filename_hl = hl, + state = state, + diff = diff, + }) + end + end + + return files + end + + local files = get_files() + + Snacks.picker.pick { + source = 'jj_status', + items = files, + format = 'file', + title = 'jj status', + preview = function(ctx) + if ctx.item.file then + Snacks.picker.preview.diff(ctx) + else + ctx.preview:reset() + ctx.preview:set_title('No preview') + end + end, + } +end + +function M.revs() + local function jj_new(picker, item) + picker:close() + if item then + if not item.rev then + vim.notify.warn('No branch or commit found', { title = 'Snacks Picker' }) + return + end + local cmd = { 'jj', 'new', '-r', item.rev } + Snacks.picker.util.cmd(cmd, function() + vim.notify('Checking out revision: ' .. item.rev, { title = 'Snacks Picker' }) + vim.cmd.checktime() + require('plugins.lib.session_jj').load() + end, { cwd = item.cwd }) + end + end + + local function jj_rev_cmd(ctx) + if ctx.item.rev then + Snacks.picker.preview.cmd({ 'jj', 'show', '--ignore-working-copy', '--git', '-r', ctx.item.rev }, ctx) + else + ctx.preview:reset() + return 'No preview available.' + end + end + + local function jj_log(revset) + if revset == nil then + revset = '-r "ancestors(@,25)"' + else + revset = '-r ' .. revset + end + local status_raw = vim.fn.system( + 'jj log --ignore-working-copy ' + .. revset + .. + ' --template \'if(root, format_root_commit(self), label(if(current_working_copy, "working_copy"), concat(separate(" ", self.change_id().shortest(8), self.bookmarks()), " | ", if(empty, label("empty", "(empty)")), if(description, description.first_line(), label(if(empty, "empty"), description_placeholder),),) ++ "\n",),)\'' + ) + local lines = {} + + for line in status_raw:gmatch('[^\r\n]+') do + local sign, rev = string.match(line, '(.)%s(%a+)%s.*') + table.insert(lines, { + text = line, + sign = sign, + rev = rev, + }) + end + + return lines + end + + Snacks.picker.pick { + source = 'jj_revs', + layout = 'ivy', + format = 'text', + title = 'jj log', + items = jj_log(), + confirm = jj_new, + preview = jj_rev_cmd, + } +end + +return M diff --git a/nvim/lua/plugins/lsp.lua b/nvim/lua/plugins/lsp.lua new file mode 100644 index 0000000..432aac4 --- /dev/null +++ b/nvim/lua/plugins/lsp.lua @@ -0,0 +1,131 @@ +return { + { + 'neovim/nvim-lspconfig', + event = 'VeryLazy', + config = function() + vim.lsp.enable { + 'nil_ls', + 'phpactor', + 'gopls', + 'lua_ls', + 'basedpyright', + } + + vim.api.nvim_create_autocmd('LspAttach', { + group = vim.api.nvim_create_augroup('UserLspConfig', {}), + callback = function(ev) + local client = vim.lsp.get_client_by_id(ev.data.client_id) + if not client then + return + end + vim.keymap.set('n', 'gO', function() + Snacks.picker.lsp_symbols { focus = 'list' } + end, { buffer = ev.buf, desc = 'LSP symbols' }) + + vim.keymap.set('n', 'grh', function() + vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) + end, { buffer = ev.buf, desc = 'LSP hints toggle' }) + vim.keymap.set('n', 'grl', vim.lsp.codelens.run, { buffer = ev.buf, desc = 'vim.lsp.codelens.run()' }) + + vim.keymap.set('n', 'gre', function() + vim.diagnostic.setloclist() + end, { buffer = ev.buf, desc = 'LSP buffer diagnostics' }) + vim.keymap.set('n', 'grE', function() + vim.diagnostic.setqflist() + end, { buffer = ev.buf, desc = 'LSP diagnostics' }) + + vim.keymap.set('n', 'grc', function() + vim.lsp.buf.incoming_calls() + end, { buffer = ev.buf, desc = 'LSP incoming_calls' }) + vim.keymap.set('n', 'gro', function() + vim.lsp.buf.outgoing_calls() + end, { buffer = ev.buf, desc = 'LSP outgoing_calls' }) + + -- Auto-refresh code lenses + if client:supports_method('textDocument/codeLens') or client.server_capabilities.codeLensProvider then + vim.api.nvim_create_autocmd({ 'InsertLeave', 'TextChanged' }, { + group = vim.api.nvim_create_augroup(string.format('lsp-%s-%s', ev.buf, client.id), {}), + callback = function() + vim.lsp.codelens.refresh { bufnr = ev.buf } + end, + buffer = ev.buf, + }) + vim.lsp.codelens.refresh() + end + end, + }) + vim.api.nvim_exec_autocmds('FileType', {}) + end, + }, + { + 'stevearc/conform.nvim', + event = 'VeryLazy', + keys = { + { + '\\f', + function() + vim.b.disable_autoformat = not vim.b.disable_autoformat + Snacks.notify(string.format('Buffer formatting disabled: %s', vim.b.disable_autoformat)) + end, + mode = { 'n', 'x' }, + desc = 'toggle buffer formatting', + }, + { + '\\F', + function() + vim.g.disable_autoformat = not vim.g.disable_autoformat + Snacks.notify(string.format('Global formatting disabled: %s', vim.g.disable_autoformat)) + end, + mode = { 'n', 'x' }, + desc = 'toggle global formatting', + }, + }, + opts = { + notify_no_formatters = false, + formatters_by_ft = { + json = { 'jq' }, + lua = { 'stylua' }, + python = { 'ruff' }, + nix = { 'nixfmt' }, + fish = { 'fish_indent' }, + ['*'] = { 'trim_whitespace' }, + }, + format_on_save = function(bufnr) + -- Disable with a global or buffer-local variable + if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then + return + end + return { timeout_ms = 1500, lsp_format = 'fallback' } + end, + default_format_opts = { + timeout_ms = 1500, + lsp_format = 'fallback', + }, + }, + }, + { + 'mfussenegger/nvim-lint', + event = 'VeryLazy', + config = function() + require('lint').linters_by_ft = { + docker = { 'hadolint' }, + yaml = { 'yamllint' }, + sh = { 'shellcheck' }, + go = { 'golangcilint' }, + ruby = { 'rubocop' }, + fish = { 'fish' }, + bash = { 'bash' }, + nix = { 'nix' }, + php = { 'php' }, + } + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost', 'InsertLeave' }, { + group = vim.api.nvim_create_augroup('lint', { clear = true }), + callback = function() + if vim.bo.modifiable then + require('lint').try_lint(nil, { ignore_errors = true }) + end + end, + }) + end, + }, +} diff --git a/nvim/lua/plugins/mini.lua b/nvim/lua/plugins/mini.lua new file mode 100644 index 0000000..bd6a08b --- /dev/null +++ b/nvim/lua/plugins/mini.lua @@ -0,0 +1,163 @@ +return { + { + 'echasnovski/mini.nvim', + lazy = false, + keys = { + { + 'gp', + function() + MiniDiff.toggle_overlay(0) + end, + noremap = true, + desc = 'git diff overlay', + }, + { + 'go', + function() + return MiniGit.show_at_cursor() + end, + noremap = true, + desc = 'git show at cursor', + }, + { + 'gb', + 'Git blame -- %', + desc = 'git blame', + }, + { + 'gg', + ':Git ', + desc = 'git command', + }, + }, + config = function() + require('mini.basics').setup { mappings = { windows = true } } + require('mini.icons').setup() + vim.schedule(function() + local ai = require('mini.ai') + local extra_ai = require('mini.extra').gen_ai_spec + ai.setup { + n_lines = 300, + custom_textobjects = { + i = extra_ai.indent(), + g = extra_ai.buffer(), + l = extra_ai.line(), + u = ai.gen_spec.function_call(), + a = ai.gen_spec.treesitter { a = '@parameter.outer', i = '@parameter.inner' }, + k = ai.gen_spec.treesitter { a = '@assignment.lhs', i = '@assignment.lhs' }, + v = ai.gen_spec.treesitter { a = '@assignment.rhs', i = '@assignment.rhs' }, + f = ai.gen_spec.treesitter { a = '@function.outer', i = '@function.inner' }, + o = ai.gen_spec.treesitter { + a = { '@block.outer', '@conditional.outer', '@loop.outer' }, + i = { '@block.inner', '@conditional.inner', '@loop.inner' }, + }, + }, + } + require('mini.align').setup() + require('mini.bracketed').setup { file = { suffix = 'm' } } + require('mini.git').setup() + require('mini.surround').setup() + require('mini.splitjoin').setup { detect = { separator = '[,;\n]' } } + + require('mini.sessions').setup { + file = '', + autowrite = true, + hooks = { + pre = { + read = function(session) -- load Dart state *before* buffers are loaded + vim.cmd('rshada') + Dart.read_session(session['name']) + end, + write = function(session) + vim.cmd('wshada') + Dart.write_session(session['name']) + end, + }, + }, + } + require('plugins.lib.session_jj').setup() + + local jump = require('mini.jump2d') + jump.setup { + view = { n_steps_ahead = 1, dim = true }, + spotter = jump.gen_spotter.vimpattern(), + } + + local diff = require('mini.diff') + diff.setup { + options = { wrap_goto = true }, + source = { + require('plugins.lib.minidiff_jj').gen_source(), + diff.gen_source.git(), + }, + } + local miniclue = require('mini.clue') + miniclue.setup { + triggers = { + { mode = 'n', keys = '' }, + { mode = 'n', keys = 'g' }, + { mode = 'n', keys = "'" }, + { mode = 'n', keys = '`' }, + { mode = 'n', keys = '"' }, + { mode = 'n', keys = '' }, + { mode = 'n', keys = 'z' }, + { mode = 'n', keys = ']' }, + { mode = 'n', keys = '[' }, + { mode = 'n', keys = '\\' }, + }, + window = { + config = { width = 'auto' }, + }, + clues = { + miniclue.gen_clues.g(), + miniclue.gen_clues.marks(), + miniclue.gen_clues.registers(), + miniclue.gen_clues.windows(), + miniclue.gen_clues.z(), + }, + } + local files = require('mini.files') + files.setup { + mappings = { + go_in_plus = '', + }, + windows = { + preview = true, + width_focus = 30, + width_preview = 50, + }, + } + vim.keymap.set('n', 'nc', function() + files.open(vim.api.nvim_buf_get_name(0), false) -- open current buffer's dir + files.reveal_cwd() + end, { desc = 'minifiles open' }) + vim.api.nvim_create_autocmd('User', { + pattern = 'MiniFilesBufferCreate', + callback = function(args) + vim.keymap.set('n', 'nc', function() + files.synchronize() + files.close() + end, { buffer = args.data.buf_id }) + vim.keymap.set('n', '`', function() + local cur_entry_path = MiniFiles.get_fs_entry().path + local cur_directory = vim.fs.dirname(cur_entry_path) + if cur_directory ~= '' then + vim.fn.chdir(cur_directory) + end + end, { buffer = args.data.buf_id }) + end, + }) + vim.api.nvim_create_autocmd('User', { + pattern = 'MiniFilesActionRename', + callback = function(event) + Snacks.rename.on_rename_file(event.data.from, event.data.to) + end, + }) + + local multi = require('mini.keymap').map_multistep + multi({ 'i', 's' }, '', { 'blink_accept', 'vimsnippet_next', 'increase_indent' }) + multi({ 'i', 's' }, '', { 'vimsnippet_prev', 'decrease_indent' }) + end) + end, + }, +} diff --git a/nvim/lua/plugins/misc.lua b/nvim/lua/plugins/misc.lua new file mode 100644 index 0000000..20a9df5 --- /dev/null +++ b/nvim/lua/plugins/misc.lua @@ -0,0 +1,146 @@ +return { + { + 'iofq/dart.nvim', + lazy = false, + priority = 1001, + dependencies = 'echasnovski/mini.nvim', + opts = { + label_marked_fg = 'cyan' + }, + }, + { + 'windwp/nvim-autopairs', + event = 'VeryLazy', + config = true, + }, + { + 'nvim-treesitter/nvim-treesitter', + event = 'VeryLazy', + branch = 'main', + dependencies = { + { + 'nvim-treesitter/nvim-treesitter-textobjects', + branch = 'main', + config = true, + }, + { + 'nvim-treesitter/nvim-treesitter-context', + opts = { + max_lines = 5, + min_window_height = 50, + }, + }, + }, + }, + { + 'jinh0/eyeliner.nvim', + event = 'VeryLazy', + config = true, + }, + { + 'MeanderingProgrammer/render-markdown.nvim', + event = 'VeryLazy', + opts = { + completions = { + blink = { enabled = true }, + }, + }, + }, + { + 'sindrets/diffview.nvim', + event = 'VeryLazy', + opts = { + use_icons = false, + enhanced_diff_hl = true, + default_args = { + DiffviewOpen = { '--imply-local' }, + }, + view = { + merge_tool = { + layout = 'diff4_mixed', + disable_diagnostics = true, + }, + }, + keymaps = { + view = { + { { 'n' }, 'q', vim.cmd.DiffviewClose, { desc = 'Close Diffview' } }, + }, + file_panel = { + { { 'n' }, 'q', vim.cmd.DiffviewClose, { desc = 'Close Diffview' } }, + }, + file_history_panel = { + { { 'n' }, 'q', vim.cmd.DiffviewClose, { desc = 'Close Diffview' } }, + }, + }, + }, + keys = { + { 'nb', vim.cmd.DiffviewOpen, desc = 'diffview open' }, + { + 'nh', + 'DiffviewFileHistory %', + mode = { 'n', 'v' }, + desc = 'diffview history', + }, + { + 'nH', + 'DiffviewFileHistory', + mode = { 'n', 'v' }, + desc = 'diffview history', + }, + }, + }, + { + 'ThePrimeagen/refactoring.nvim', + event = 'VeryLazy', + config = true, + keys = { + { 'rv', 'Refactor inline_vardd', mode = { 'n', 'x' } }, + { + 'rr', + function() + require('refactoring').select_refactor { prefer_ex_cmd = true } + end, + mode = { 'n', 'x' }, + }, + }, + }, + { + 'stevearc/quicker.nvim', + event = 'VeryLazy', + opts = { + follow = { + enabled = true, + }, + }, + keys = { + { + 'qf', + function() + require('quicker').toggle { max_height = 20 } + end, + desc = 'Toggle qflist', + }, + { + 'qr', + function() + require('quicker').refresh() + end, + desc = 'Refresh qflist', + }, + { + 'q>', + function() + require('quicker').expand { before = 2, after = 2, add_to_existing = true } + end, + desc = 'Expand quickfix context', + }, + { + 'q<', + function() + require('quicker').collapse() + end, + desc = 'Collapse quickfix context', + }, + }, + }, +} diff --git a/nvim/lua/plugins/snacks.lua b/nvim/lua/plugins/snacks.lua new file mode 100644 index 0000000..246452e --- /dev/null +++ b/nvim/lua/plugins/snacks.lua @@ -0,0 +1,198 @@ +return { + { + 'folke/snacks.nvim', + lazy = false, + priority = 1000, + opts = { + bigfile = { enabled = true }, + notifier = { + enabled = true, + timeout = 4000, + }, + styles = { + notification = { + wo = { wrap = true }, + }, + terminal = { + border = 'rounded', + }, + }, + terminal = { enabled = true }, + indent = { enabled = true }, + input = { enabled = true }, + picker = { + enabled = true, + jump = { reuse_win = true }, + matcher = { + frecency = true, + history_bonus = true, + cwd_bonus = true, + }, + layout = 'ivy_split', + sources = { + grep = { hidden = true }, + explorer = { hidden = true }, + lsp_symbols = { + filter = { default = true }, + layout = 'left', + }, + smart = { + sort = { + fields = { + 'score:desc', + 'idx', + '#text', + }, + }, + multi = { + 'marks', + { source = 'buffers', current = false }, + { source = 'files', hidden = true }, + { source = 'git_files', untracked = true }, + }, + }, + }, + win = { + input = { + keys = { ['wq'] = { 'close', mode = 'i' } }, + }, + list = { + keys = { ['wq'] = { 'close', mode = 'i' } }, + }, + }, + }, + }, + keys = { + { + '', + function() + Snacks.terminal.toggle() + end, + mode = { 'n', 't' }, + desc = 'terminal open', + }, + { + '', + function() + Snacks.terminal.toggle('command -v fish >/dev/null && exec fish || exec bash') + end, + mode = { 'n', 't' }, + desc = 'terminal open', + }, + { + '', + function() + vim.cmd.delmarks { args = { '0-9' } } + vim.cmd.delmarks { args = { '"' } } + Snacks.picker.smart() + end, + desc = 'Fuzzy find smart', + }, + { + 'fe', + function() + Snacks.explorer() + end, + desc = 'snacks explorer', + }, + { + 'ff', + function() + Snacks.picker.files() + end, + desc = 'Fuzzy find files', + }, + { + 'fa', + function() + Snacks.picker.grep() + end, + desc = 'Fuzzy find grep', + }, + { + 'f8', + function() + Snacks.picker.grep_word() + end, + desc = 'Fuzzy find grep word', + }, + { + 'f?', + function() + Snacks.picker.pickers() + end, + desc = 'See all pickers', + }, + { + 'fu', + function() + Snacks.picker.undo() + end, + desc = 'Pick undotree', + }, + { + 'fj', + function() + Snacks.picker.jumps() + end, + desc = 'Pick jumps', + }, + { + 'f.', + function() + Snacks.picker.resume() + end, + desc = 'Fuzzy find resume', + }, + { + 'fb', + function() + Snacks.picker.buffers() + end, + desc = 'Fuzzy find buffers', + }, + { + 'fn', + function() + Snacks.picker.notifications() + end, + desc = 'pick notifications', + }, + { + 'gO', + function() + Snacks.picker.treesitter() + end, + desc = 'pick treesitter nodes', + }, + { + 'fq', + function() + Snacks.picker.qflist() + end, + desc = 'pick quickfix list', + }, + { + 'jf', + function() + require('plugins.lib.snacks_jj').status() + end, + desc = 'pick notifications', + }, + { + 'jj', + function() + require('plugins.lib.snacks_jj').revs() + end, + desc = 'pick notifications', + }, + { + 'gd', + function() + Snacks.git.blame_line() + end, + desc = 'Snacks git blame', + }, + }, + }, +}