diff --git a/flake.lock b/flake.lock index a9853c4..73d791e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,16 +1,13 @@ { "nodes": { "dart": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - }, + "flake": false, "locked": { - "lastModified": 1761202123, - "narHash": "sha256-ULrZW4b8SKRvPpJPt8/jkqqc/blQiIWUriNWVXA33so=", + "lastModified": 1760366615, + "narHash": "sha256-qGWNl+UQLvdFl8AAgywOHthizfKovk8az1xhRhQeVn8=", "owner": "iofq", "repo": "dart.nvim", - "rev": "71421e7ef5aee8267e24dc562fdd07a83bda192e", + "rev": "5fac43b0f7b5500c69a51a3717aef8ccceacd178", "type": "github" }, "original": { @@ -35,6 +32,38 @@ "type": "github" } }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -61,11 +90,11 @@ ] }, "locked": { - "lastModified": 1760948891, - "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", + "lastModified": 1759362264, + "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", + "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", "type": "github" }, "original": { @@ -78,23 +107,6 @@ "inputs": { "systems": "systems" }, - "locked": { - "lastModified": 1731533236, - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, "locked": { "lastModified": 1731533236, "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", @@ -156,6 +168,29 @@ "type": "github" } }, + "git-hooks_2": { + "inputs": { + "flake-compat": "flake-compat_3", + "gitignore": "gitignore_2", + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760392170, + "narHash": "sha256-WftxJgr2MeDDFK47fQKywzC72L2jRc/PWcyGdjaDzkw=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "46d55f0aeb1d567a78223e69729734f3dca25a85", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "gitignore": { "inputs": { "nixpkgs": [ @@ -178,6 +213,53 @@ "type": "github" } }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "neovim-nightly-overlay", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hercules-ci-effects": { + "inputs": { + "flake-parts": [ + "neovim-nightly-overlay", + "flake-parts" + ], + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1758022363, + "narHash": "sha256-ENUhCRWgSX4ni751HieNuQoq06dJvApV/Nm89kh+/A0=", + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "rev": "1a3667d33e247ad35ca250698d63f49a5453d824", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "type": "github" + } + }, "luvit-meta": { "flake": false, "locked": { @@ -196,18 +278,20 @@ }, "neovim-nightly-overlay": { "inputs": { + "flake-compat": "flake-compat_2", "flake-parts": "flake-parts_2", + "git-hooks": "git-hooks_2", + "hercules-ci-effects": "hercules-ci-effects", "neovim-src": "neovim-src", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1761955453, - "narHash": "sha256-hQomzSbBiFsDXDMCjHmWXrAMgFlQlCiy7T37Eq7RvT4=", + "lastModified": 1760400370, + "narHash": "sha256-ghN+xxNfS9xz9VT2f49KjhVAWYBMfo2Z1r8YttPqZvc=", "owner": "nix-community", "repo": "neovim-nightly-overlay", - "rev": "c58076a0d9b24bf77fef4fa2e7c43950914edf71", + "rev": "921a313d1522414e15f0843bae028f5327b0e2e6", "type": "github" }, "original": { @@ -219,11 +303,11 @@ "neovim-src": { "flake": false, "locked": { - "lastModified": 1761949631, - "narHash": "sha256-YgMQaFD4L9+PEYSkUlBkqaKt+ALPHiVgzgRbjOSW4tE=", + "lastModified": 1760398177, + "narHash": "sha256-E9Qv7RWRU3qxHtidPyjOMCiow9pRUI5yiNldYaWIeV8=", "owner": "neovim", "repo": "neovim", - "rev": "1fddd74da7428e38b79ccb817dbd6952ff1d8ac6", + "rev": "0ed5e00077dcc8ab1a99dd76a85628b11504ffa4", "type": "github" }, "original": { @@ -234,15 +318,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755660401, + "lastModified": 1760349414, + "narHash": "sha256-W4Ri1ZwYuNcBzqQQa7NnWfrv0wHMo7rduTWjIeU9dZk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5788de501b965d7413f2beaac10aeeb56f9a19a8", + "rev": "c12c63cd6c5eb34c7b4c3076c6a99e00fcab86ec", "type": "github" }, "original": { "owner": "NixOS", - "ref": "master", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -277,11 +362,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1761991861, - "narHash": "sha256-rccG7eGGelDsu4eXPi2vdBR1pddZFZAd/JlB3NLjgy4=", + "lastModified": 1760435515, + "narHash": "sha256-E9D5sWHmPCmWsrCB3Jogvr/7ODiVaKynDrOpG4ba2tI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0e65d6bc462e57fe6a76a49d4eea909ba08b1dc1", + "rev": "db25466bd95abdbe3012be2900a5562fcfb95d51", "type": "github" }, "original": { @@ -291,30 +376,14 @@ "type": "github" } }, - "nixpkgs_3": { - "locked": { - "lastModified": 1761114652, - "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "nvim-treesitter": { "flake": false, "locked": { - "lastModified": 1761385693, - "narHash": "sha256-/SGikTPEMxI7rcfGvuJlNZs73/wZiQx14QX9xlfsTv0=", + "lastModified": 1760260935, + "narHash": "sha256-To/syCZPs7vKA0WkuBz7ZxCGT/wzj705QfxZng6Nsjo=", "owner": "nvim-treesitter", "repo": "nvim-treesitter", - "rev": "98fe644cb3b5ba390d1bc3f89299f93c70020803", + "rev": "0606c7a9dcaa5c5beee0b0f09043e9fdd1ba0a68", "type": "github" }, "original": { @@ -324,26 +393,6 @@ "type": "github" } }, - "nvim-treesitter-main": { - "inputs": { - "nixpkgs": "nixpkgs_3", - "nvim-treesitter": "nvim-treesitter", - "nvim-treesitter-textobjects": "nvim-treesitter-textobjects" - }, - "locked": { - "lastModified": 1761496664, - "narHash": "sha256-xTQUiJu0jJNSEHEv4La1HbaFokup0eWr67Kqf/wDENA=", - "owner": "iofq", - "repo": "nvim-treesitter-main", - "rev": "834d66648bb7a96a2ad11d53a33f2d9b13766447", - "type": "github" - }, - "original": { - "owner": "iofq", - "repo": "nvim-treesitter-main", - "type": "github" - } - }, "nvim-treesitter-textobjects": { "flake": false, "locked": { @@ -364,11 +413,12 @@ "root": { "inputs": { "dart": "dart", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "gen-luarc": "gen-luarc", "neovim-nightly-overlay": "neovim-nightly-overlay", "nixpkgs": "nixpkgs_2", - "nvim-treesitter-main": "nvim-treesitter-main" + "nvim-treesitter": "nvim-treesitter", + "nvim-treesitter-textobjects": "nvim-treesitter-textobjects" } }, "systems": { @@ -386,18 +436,24 @@ "type": "github" } }, - "systems_2": { + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1760120816, + "narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "761ae7aff00907b607125b2f57338b74177697ed", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } } diff --git a/flake.nix b/flake.nix index a9cc1ad..68da80f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,23 +4,33 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/master"; flake-utils.url = "github:numtide/flake-utils"; - neovim-nightly-overlay = { - url = "github:nix-community/neovim-nightly-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay"; gen-luarc = { url = "github:mrcjkb/nix-gen-luarc-json"; inputs.nixpkgs.follows = "nixpkgs"; }; dart = { url = "github:iofq/dart.nvim"; + flake = false; }; - nvim-treesitter-main = { - url = "github:iofq/nvim-treesitter-main"; + nvim-treesitter = { + url = "github:nvim-treesitter/nvim-treesitter/main"; + flake = false; }; + 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) + # wf-nvim = { + # url = "github:Cassin01/wf.nvim"; + # flake = false; + # }; }; outputs = inputs@{ + self, nixpkgs, flake-utils, ... @@ -29,29 +39,22 @@ 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; }; - finalOverlays = [ - inputs.neovim-nightly-overlay.overlays.default - inputs.nvim-treesitter-main.overlays.default - (final: prev: { - vimPlugins = prev.vimPlugins.extend ( - f: p: { - nvim-treesitter = p.nvim-treesitter.withAllGrammars; - nvim-treesitter-textobjects = p.nvim-treesitter-textobjects.overrideAttrs { - dependencies = [ f.nvim-treesitter ]; - }; - } - ); - }) - neovim-overlay - ]; in flake-utils.lib.eachSystem systems ( system: let pkgs = import nixpkgs { inherit system; - overlays = finalOverlays ++ [ + config.allowUnfree = true; + overlays = [ + inputs.neovim-nightly-overlay.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. + # The generated file can be symlinked in the devShell's shellHook. inputs.gen-luarc.overlays.default ]; }; @@ -59,13 +62,16 @@ name = "nvim-devShell"; buildInputs = with pkgs; [ lua-language-server - nixd + nil stylua luajitPackages.luacheck + nvim-dev ]; shellHook = '' # symlink the .luarc.json generated in the overlay ln -fs ${pkgs.nvim-luarc-json} .luarc.json + # allow quick iteration of lua configs + ln -Tfns $PWD/nvim ~/.config/nvim-dev ''; }; in @@ -74,6 +80,7 @@ default = nvim; nvim = pkgs.nvim-pkg; nvim-min = pkgs.nvim-min-pkg; + nvim-dev = pkgs.nvim-dev; }; devShells = { default = shell; @@ -81,6 +88,6 @@ } ) // { - overlays.default = nixpkgs.lib.composeManyExtensions finalOverlays; + overlays.default = neovim-overlay; }; } diff --git a/nix/mkNeovim.nix b/nix/mkNeovim.nix index 83baf98..7d2e3a6 100644 --- a/nix/mkNeovim.nix +++ b/nix/mkNeovim.nix @@ -1,23 +1,197 @@ -{ pkgs, lib }: +# Function for creating a Neovim derivation { - plugins ? [ ], - packages ? [ ], + pkgs, + lib, + stdenv, + # Set by the overlay to ensure we use a compatible version of `wrapNeovimUnstable` + pkgs-wrapNeovim ? pkgs, +}: +with lib; +{ + # 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 - finalPlugins = plugins ++ [ - (pkgs.vimUtils.buildVimPlugin { - pname = "iofq-nvim"; - src = lib.cleanSource ../nvim; - version = "0.1"; - doCheck = false; - }) - ]; - wrapperArgs = ''--prefix PATH : "${lib.makeBinPath packages}"''; + # 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 = nvimRtpSrc; + + buildPhase = '' + mkdir -p $out/ + ''; + + installPhase = '' + cp -r . $out/ + ''; + }; + + # 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); + + # 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 { - inherit wrapperArgs; - plugins = finalPlugins; - withPython3 = false; - withRuby = false; - vimAlias = true; -} +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 430c345..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 = final; }; - 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 + nvim-treesitter.withAllGrammars + nvim-treesitter-context nvim-treesitter-textobjects quicker-nvim refactoring-nvim @@ -22,12 +27,12 @@ let snacks-nvim ]; - basePackages = with prev; [ + 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 @@ -39,21 +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; + 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..9a51a54 --- /dev/null +++ b/nix/plugin-overlay.nix @@ -0,0 +1,29 @@ +{ 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 = mkNvimPlugin inputs.dart "dart.nvim"; + nvim-treesitter-textobjects = mkNvimPlugin inputs.nvim-treesitter-textobjects "nvim-treesitter-textobjects"; + nvim-treesitter = prev'.nvim-treesitter.overrideAttrs (old: rec { + src = inputs.nvim-treesitter; + name = "${old.pname}-${src.rev}"; + postPatch = ""; + # ensure runtime queries get linked to RTP (:TSInstall does this too) + buildPhase = " + mkdir -p $out/queries + cp -a $src/runtime/queries/* $out/queries + "; + nvimSkipModules = [ "nvim-treesitter._meta.parsers" ]; + }); + } + ); +} diff --git a/nvim/after/lua/iofq/minidiff_jj.lua b/nvim/after/lua/iofq/minidiff_jj.lua deleted file mode 100644 index 0294773..0000000 --- a/nvim/after/lua/iofq/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/after/lua/iofq/snacks_jj.lua b/nvim/after/lua/iofq/snacks_jj.lua deleted file mode 100644 index 639ecfd..0000000 --- a/nvim/after/lua/iofq/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/after/plugin/autocmd.lua b/nvim/after/plugin/autocmd.lua deleted file mode 100644 index e22f2b7..0000000 --- a/nvim/after/plugin/autocmd.lua +++ /dev/null @@ -1,139 +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, -}) - --- Configure difftool buffers -vim.api.nvim_create_autocmd('FileType', { - pattern = 'qf', - group = vim.api.nvim_create_augroup('difftool', { clear = true }), - callback = function(event) - local function exec(fmt, str) - return os.execute(string.format(fmt, str)) - end - local function refresh() - local qf = vim.fn.getqflist() - - local entry = qf[1] - if not entry or not entry.user_data.diff then - return nil - end - - local ns = vim.api.nvim_create_namespace('nvim.difftool.hl') - vim.api.nvim_buf_clear_namespace(event.buf, ns, 0, -1) - for i, item in ipairs(qf) do - local path = vim.fn.fnamemodify(item.user_data.right, ':t') - local hl = 'Added' - if - exec('git diff --quiet -- %s', path) ~= 0 - or exec('git ls-files --error-unmatch -- %s > /dev/null 2>&1', path) ~= 0 - then - hl = 'Removed' - end - vim.hl.range(event.buf, ns, hl, { i - 1, 0 }, { i - 1, -1 }) - end - end - vim.keymap.set('n', 'gh', function() - local idx = vim.api.nvim_win_get_cursor(0)[1] - local qf = vim.fn.getqflist() - local filename = qf[idx].user_data.rel - - if exec('git diff --quiet --cached -- %s', filename) ~= 0 then - exec('git restore --quiet --staged -- %s', filename) - else - exec('git add -- %s', filename) - end - refresh() - end) - vim.schedule(refresh) - end, -}) - --- Init treesitter -cmd('FileType', { - group = vim.api.nvim_create_augroup('treesitter', { clear = true }), - 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/after/plugin/mini.lua b/nvim/after/plugin/mini.lua deleted file mode 100644 index c60ec00..0000000 --- a/nvim/after/plugin/mini.lua +++ /dev/null @@ -1,78 +0,0 @@ -local map = vim.keymap.set - -vim.schedule(function() - require('mini.align').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(), - b = require('mini.extra').gen_ai_spec.buffer(), - 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 -- %') - map('n', 'go', function() - return MiniGit.show_at_cursor() - end) - - local jump = require('mini.jump2d') - jump.setup { - mappings = { - start_jumping = 's', - }, - view = { n_steps_ahead = 1, dim = true }, - spotter = jump.gen_spotter.vimpattern(), - } - - local diff = require('mini.diff') - diff.setup { - source = { - require('iofq.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 - 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/after/plugin/plugins.lua b/nvim/after/plugin/plugins.lua deleted file mode 100644 index 58dae3e..0000000 --- a/nvim/after/plugin/plugins.lua +++ /dev/null @@ -1,153 +0,0 @@ -local map = vim.keymap.set - -require('mini.basics').setup { mappings = { windows = true } } -require('mini.icons').setup() - -require('dart').setup { - tabline = { - icons = false, - 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 = { - preset = 'left', - layout = { width = 90, min_width = 90 }, - }, - }, - 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('iofq.snacks_jj').status) - -vim.schedule(function() - require('nvim-treesitter').setup() - require('nvim-treesitter-textobjects').setup() - require('render-markdown').setup() - require('nvim-autopairs').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) - - 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' }, { - callback = function() - require('lint').try_lint(nil, { ignore_errors = true }) - 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', 'path', 'snippets', 'ripgrep', 'buffer' }, - providers = { - lsp = { fallbacks = {} }, -- include buffer even when LSP is active - path = { opts = { get_cwd = vim.fn.getcwd } }, -- use nvim pwd instead of current file pwd - 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) diff --git a/nvim/after/colors/iofq.lua b/nvim/colors/iofq.lua similarity index 99% rename from nvim/after/colors/iofq.lua rename to nvim/colors/iofq.lua index 7d80403..be525d8 100644 --- a/nvim/after/colors/iofq.lua +++ b/nvim/colors/iofq.lua @@ -626,7 +626,7 @@ hi(0, 'SpellLocal', { sp = '#5a93aa', undercurl = true }) hi(0, 'SpellRare', { sp = '#5a93aa', undercurl = true }) hi(0, 'Statement', { fg = '#ad5c7c' }) hi(0, 'StatusLine', { bg = 'none' }) -hi(0, 'StatusLineNC', { bg = 'none', fg = "grey" }) +hi(0, 'StatusLineNC', { bg = 'none' }) hi(0, 'String', { fg = '#7aa4a1' }) hi(0, 'Substitute', { bg = '#e85c51', fg = '#152528' }) hi(0, 'SymbolOutlineConnector', { link = 'Conceal' }) diff --git a/nvim/after/ftplugin/go.lua b/nvim/ftplugin/go.lua similarity index 100% rename from nvim/after/ftplugin/go.lua rename to nvim/ftplugin/go.lua diff --git a/nvim/after/ftplugin/php.lua b/nvim/ftplugin/php.lua similarity index 100% rename from nvim/after/ftplugin/php.lua rename to nvim/ftplugin/php.lua diff --git a/nvim/init.lua b/nvim/init.lua new file mode 100644 index 0000000..622287b --- /dev/null +++ b/nvim/init.lua @@ -0,0 +1,34 @@ +vim.g.mapleader = ' ' +-- 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/after/lsp/gopls.lua b/nvim/lsp/gopls.lua similarity index 100% rename from nvim/after/lsp/gopls.lua rename to nvim/lsp/gopls.lua diff --git a/nvim/after/lsp/lua_ls.lua b/nvim/lsp/lua_ls.lua similarity index 92% rename from nvim/after/lsp/lua_ls.lua rename to nvim/lsp/lua_ls.lua index f71f2bd..4be10e8 100644 --- a/nvim/after/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/config/autocmd.lua b/nvim/lua/config/autocmd.lua new file mode 100644 index 0000000..d40ad17 --- /dev/null +++ b/nvim/lua/config/autocmd.lua @@ -0,0 +1,105 @@ +-- 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/after/plugin/init.lua b/nvim/lua/config/init.lua similarity index 58% rename from nvim/after/plugin/init.lua rename to nvim/lua/config/init.lua index b2afa6a..f9ba85f 100644 --- a/nvim/after/plugin/init.lua +++ b/nvim/lua/config/init.lua @@ -1,10 +1,6 @@ -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 @@ -35,30 +31,7 @@ vim.diagnostic.config { 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) +vim.schedule(function() + require('config.autocmd') + require('config.keymaps') 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 }) - -vim.cmd.packadd('nvim.difftool') 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/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..6015930 --- /dev/null +++ b/nvim/lua/plugins/lsp.lua @@ -0,0 +1,133 @@ +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' }, + puppet = { 'puppet-lint' }, + 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' }, + puppet = { 'puppet-lint' }, + 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() + end + end, + }) + end, + }, +} diff --git a/nvim/lua/plugins/mini.lua b/nvim/lua/plugins/mini.lua new file mode 100644 index 0000000..c82350c --- /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 } } + 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(), + e = 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.icons').setup() + 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..7dacf20 --- /dev/null +++ b/nvim/lua/plugins/misc.lua @@ -0,0 +1,143 @@ +return { + { + 'iofq/dart.nvim', + lazy = false, + priority = 1001, + config = true, + }, + { + '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..d6dd0b2 --- /dev/null +++ b/nvim/lua/plugins/snacks.lua @@ -0,0 +1,191 @@ +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', + }, + }, + }, +}