commit 91755583fd28d3c8eb530cfe8ed8f3be2197034c Author: Daniel Winkler Date: Fri Jan 30 14:22:39 2026 +1100 init wrapper-module config diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bfb5c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9e005fb --- /dev/null +++ b/flake.lock @@ -0,0 +1,133 @@ +{ + "nodes": { + "fran": { + "inputs": { + "nixpkgs": [ + "rixpkgs" + ] + }, + "locked": { + "lastModified": 1768802006, + "narHash": "sha256-czGb4RwTBahNNK9S7ySXeTWIrn/jKH+74hN/uIS2XrM=", + "owner": "dwinkler1", + "repo": "fran", + "rev": "a3879317519fd3685f3b7679a1de4c5ae65a7cb9", + "type": "github" + }, + "original": { + "owner": "dwinkler1", + "repo": "fran", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1769598131, + "narHash": "sha256-e7VO/kGLgRMbWtpBqdWl0uFg8Y2XWFMdz0uUJvlML8o=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fa83fd837f3098e3e678e6cf017b2b36102c7211", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "plugins-cmp-pandoc-references": { + "flake": false, + "locked": { + "lastModified": 1743491695, + "narHash": "sha256-XsdneGNJzmRBggk8lz9JNDQYk7wbYfUAF2oZLXzFb9c=", + "owner": "jmbuhr", + "repo": "cmp-pandoc-references", + "rev": "130eae4f75029d6495808e0ea4b769fa1ce4c9ac", + "type": "github" + }, + "original": { + "owner": "jmbuhr", + "repo": "cmp-pandoc-references", + "type": "github" + } + }, + "plugins-cmp-r": { + "flake": false, + "locked": { + "lastModified": 1764700377, + "narHash": "sha256-xb7VFWM/BKAkN7fg62y8n618t2qkQjdYbPwhBhLJwtk=", + "owner": "R-nvim", + "repo": "cmp-r", + "rev": "70bfe8f4c062acc10266e24825439c009a0b1b89", + "type": "github" + }, + "original": { + "owner": "R-nvim", + "repo": "cmp-r", + "type": "github" + } + }, + "plugins-r": { + "flake": false, + "locked": { + "lastModified": 1769736135, + "narHash": "sha256-T4QgcBL+LCXvrEiRE2JW4jtUKl8DKzFHk8czGUO1jgY=", + "owner": "R-nvim", + "repo": "R.nvim", + "rev": "2701ec64f5485e17c0e057081a9ae2058d776464", + "type": "github" + }, + "original": { + "owner": "R-nvim", + "repo": "R.nvim", + "type": "github" + } + }, + "rixpkgs": { + "locked": { + "lastModified": 1768825970, + "narHash": "sha256-m/BI9IO7tMuOSdVNSqr0knQ4V9R6rgSXyGQOp3FovSA=", + "type": "tarball", + "url": "https://github.com/rstats-on-nix/nixpkgs/archive/2026-01-19.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/rstats-on-nix/nixpkgs/archive/2026-01-19.tar.gz" + } + }, + "root": { + "inputs": { + "fran": "fran", + "nixpkgs": "nixpkgs", + "plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references", + "plugins-cmp-r": "plugins-cmp-r", + "plugins-r": "plugins-r", + "rixpkgs": "rixpkgs", + "wrappers": "wrappers" + } + }, + "wrappers": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769731670, + "narHash": "sha256-iHq0bwOsDI5IkJuxVsljdztZdVKIBb8yO0RBRMrco68=", + "owner": "BirdeeHub", + "repo": "nix-wrapper-modules", + "rev": "4622dd5ef8e152475fce48307d3bc77211249c6a", + "type": "github" + }, + "original": { + "owner": "BirdeeHub", + "repo": "nix-wrapper-modules", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..075c367 --- /dev/null +++ b/flake.nix @@ -0,0 +1,202 @@ +# Copyright (c) 2026 BirdeeHub +# Licensed under the MIT license +{ + description = "Daniel's NixCats"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + wrappers = { + url = "github:BirdeeHub/nix-wrapper-modules"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + rixpkgs.url = "https://github.com/rstats-on-nix/nixpkgs/archive/2026-01-19.tar.gz"; + + fran = { + url = "github:dwinkler1/fran"; + inputs = { + nixpkgs.follows = "rixpkgs"; + }; + }; + + "plugins-r" = { + url = "github:R-nvim/R.nvim"; + flake = false; + }; + "plugins-cmp-r" = { + url = "github:R-nvim/cmp-r"; + flake = false; + }; + "plugins-cmp-pandoc-references" = { + url = "github:jmbuhr/cmp-pandoc-references"; + flake = false; + }; + }; + + outputs = { + self, + nixpkgs, + wrappers, + ... + } @ inputs: let + wrapperSettings = pkgs: + wrapper.config.wrap { + inherit pkgs; + cats = { + clickhouse = false; + gitPlugins = false; + julia = false; + lua = false; + markdown = false; + nix = true; + optional = false; + python = false; + r = true; + }; + + settings.lang_packages = { + python = with pkgs.python3Packages; [ + duckdb + polars + ]; + + r = with pkgs.rpkgs.rPackages; [ + arrow + broom + data_table + duckdb + janitor + styler + tidyverse + ]; + + julia = ["DataFramesMeta" "QuackIO"]; + }; + }; + + systems = [ + "aarch64-darwin" + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs systems; + + extra_pkg_config = { + # allowUnfree = true; + }; + + overlayDefs = import ./overlays inputs; + + dependencyOverlays = overlayDefs.dependencyOverlays; + + dependencyOverlay = overlayDefs.dependencyOverlay; + + mkPkgs = system: + import nixpkgs { + inherit system; + config = extra_pkg_config; + overlays = [dependencyOverlay]; + }; + + module = nixpkgs.lib.modules.importApply ./modules/neovim.nix inputs; + wrapper = wrappers.lib.evalModule module; + in { + overlays = { + default = nixpkgs.lib.composeManyExtensions [ + dependencyOverlay + (final: prev: { + n = wrapper.config.wrap {pkgs = final;}; + }) + ]; + dependencies = dependencyOverlay; + n = self.overlays.default; + }; + + wrapperModules = { + default = module; + neovim = self.wrapperModules.default; + }; + + wrappers = { + default = wrapper.config; + neovim = self.wrappers.default; + }; + + packages = forAllSystems ( + system: let + pkgs = mkPkgs system; + nvimPkg = wrapperSettings pkgs; + in { + default = nvimPkg; + n = nvimPkg; + vim = nvimPkg; + nvim = nvimPkg; + } + ); + + formatter = forAllSystems ( + system: let + pkgs = mkPkgs system; + in + pkgs.nixfmt-tree + ); + + devShells = forAllSystems ( + system: let + pkgs = mkPkgs system; + nvimPkg = wrapperSettings pkgs; + in { + default = pkgs.mkShell { + name = "n"; + packages = [nvimPkg]; + nativeBuildInputs = with pkgs; [] ++ (pkgs.lib.optionals self.wrappers.default.cats.optional [devenv]); + inputsFrom = []; + shellHook = ""; + }; + } + ); + + checks = forAllSystems ( + system: let + pkgs = mkPkgs system; + nvimPkg = wrapperSettings pkgs; + in { + default = nvimPkg; + module-eval = let + _ = wrapper.config; + in + pkgs.runCommand "check-module-eval" {} '' + echo "Module evaluation successful" > $out + ''; + package-build = pkgs.runCommand "check-n" {} '' + BINARY_PATH="${nvimPkg}/bin/n" + + if [ ! -x "$BINARY_PATH" ]; then + echo "Error: Binary n not found or not executable" + exit 1 + fi + + "$BINARY_PATH" --version > version_output.txt 2>&1 || true + + echo "Package validation successful" > $out + echo "Binary location: $BINARY_PATH" >> $out + if [ -s version_output.txt ]; then + echo "Version output:" >> $out + cat version_output.txt >> $out + fi + ''; + } + ); + + nixosModules.default = wrappers.lib.mkInstallModule { + name = "n"; + value = module; + }; + + homeModules.default = wrappers.lib.mkInstallModule { + name = "n"; + value = module; + loc = ["home" "packages"]; + }; + }; +} diff --git a/ftplugin/julia.lua b/ftplugin/julia.lua new file mode 100644 index 0000000..722b3f8 --- /dev/null +++ b/ftplugin/julia.lua @@ -0,0 +1,6 @@ +vim.b.slime_cell_delimiter = vim.b.slime_cell_delimiter or "# %%" + +local ts_lib = Config.treesitter_helpers + +local global_nodes_julia = { 'source_file', 'module_definition' } +ts_lib.setup_keybindings(global_nodes_julia) diff --git a/ftplugin/markdown.lua b/ftplugin/markdown.lua new file mode 100644 index 0000000..b673b60 --- /dev/null +++ b/ftplugin/markdown.lua @@ -0,0 +1,32 @@ +-- Add the key mappings only for Markdown files in a zk notebook. +if require("zk.util").notebook_root(vim.fn.expand('%:p')) ~= nil then + local map = vim.keymap.set + -- Open the link under the caret. + map("n", "", "lua vim.lsp.buf.definition()", { noremap = true, silent = false, buffer = true }) + + -- Create a new note after asking for its title. + -- This overrides the global `zn` mapping to create the note in the same directory as the current buffer. + map("n", "zhn", "ZkNew { dir = vim.fn.expand('%:p:h'), title = vim.fn.input('Title: ') }", + { noremap = true, silent = false, buffer = true, desc = "Note (here)" }) + -- Create a new note in the same directory as the current buffer, using the current selection for title. + map("v", "zhnt", ":'<,'>ZkNewFromTitleSelection { dir = vim.fn.expand('%:p:h') }", + { noremap = true, silent = false, buffer = true, desc = "Note from selection (title)" }) + -- Create a new note in the same directory as the current buffer, using the current selection for note content and asking for its title. + map("v", "zhnc", + ":'<,'>ZkNewFromContentSelection { dir = vim.fn.expand('%:p:h'), title = vim.fn.input('Title: ') }", + { noremap = true, silent = false, buffer = true, desc = "Note from selection (content)" }) + + -- Open notes linking to the current buffer. + map("n", "zb", "ZkBacklinks", { noremap = true, silent = false, buffer = true, desc = "Backlinks" }) + -- Alternative for backlinks using pure LSP and showing the source context. + --map('n', 'zb', 'lua vim.lsp.buf.references()', opts) + -- Open notes linked by the current buffer. + map("n", "zL", "ZkLinks", { noremap = true, silent = false, buffer = true, desc = "Links" }) + map("n", "zi", "ZkInsertLink", { noremap = true, silent = false, buffer = true, desc = "Insert link" }) + + -- Preview a linked note. + -- Open the code actions for a visual selection. + map("v", "za", ":'<,'>lua vim.lsp.buf.range_code_action()", + { noremap = true, silent = false, buffer = true, desc = "Code actions" }) + +end diff --git a/ftplugin/python.lua b/ftplugin/python.lua new file mode 100644 index 0000000..746bc54 --- /dev/null +++ b/ftplugin/python.lua @@ -0,0 +1,20 @@ + +vim.g.slime_python_ipython = 1 +vim.b.slime_cell_delimiter = vim.b.slime_cell_delimiter or "# %%" + +local ts_lib = Config.treesitter_helpers +local global_nodes_python = { 'module' } +ts_lib.setup_keybindings(global_nodes_python) + +local conform_format_group = + vim.api.nvim_create_augroup("PythonConformFormat_" .. vim.api.nvim_get_current_buf(), { clear = true }) +vim.api.nvim_create_autocmd("BufWritePre", { + group = conform_format_group, + buffer = 0, + callback = function() + require("conform").format({ + timeout_ms = 1000, + lsp_format = "prefer", + }) + end, +}) diff --git a/ftplugin/r.lua b/ftplugin/r.lua new file mode 100644 index 0000000..d0e9ab7 --- /dev/null +++ b/ftplugin/r.lua @@ -0,0 +1,56 @@ +vim.b.slime_cell_delimiter = vim.b.slime_cell_delimiter or "## ----" + +local assign_action = function() + if vim.bo.filetype ~= "r" then + return + end + + local ok, r_edit = pcall(require, "r.edit") + if not ok then + return + end + + if MiniTrailspace and MiniTrailspace.trim then + MiniTrailspace.trim() + end + r_edit.assign() +end + +vim.api.nvim_buf_create_user_command(0, "RAssign", assign_action, { desc = "Trim trailing space and insert <-" }) +-- Settings +vim.bo.comments = [[:#',:####,:###,:##,:#]] + +-- Keymaps +-- Note: These use mappings provided by R.nvim +vim.keymap.set("n", "", "RDSendLine", { buffer = true }) +vim.keymap.set("v", "", "RSendSelection", { buffer = true }) + +-- Assignment operator (--) +vim.keymap.set("i", "--", "lua MiniTrailspace.trim()RInsertAssign", { buffer = true, noremap = true }) + +-- Pipe operator (;;) +vim.keymap.set("i", ";;", "lua MiniTrailspace.trim()RInsertPipe", { buffer = true, noremap = true }) + +-- MiniClue / WhichKey hints +local r_clues = { + { mode = "n", keys = "a", desc = "+batch" }, + { mode = "n", keys = "b", desc = "+between/debug" }, + { mode = "n", keys = "c", desc = "+substitute" }, + { mode = "n", keys = "f", desc = "+functions" }, + { mode = "n", keys = "i", desc = "+install" }, + { mode = "n", keys = "k", desc = "+knit" }, + { mode = "n", keys = "p", desc = "+paragraph" }, + { mode = "n", keys = "r", desc = "+regular" }, + { mode = "n", keys = "s", desc = "+selection" }, + { mode = "n", keys = "t", desc = "+dput" }, + { mode = "n", keys = "u", desc = "+undebug" }, +} + +vim.b.miniclue_config = { + clues = { + r_clues, + }, + triggers = { + { mode = "n", keys = "", desc = "+R" }, + }, +} diff --git a/ftplugin/sql.lua b/ftplugin/sql.lua new file mode 100644 index 0000000..83740d7 --- /dev/null +++ b/ftplugin/sql.lua @@ -0,0 +1,11 @@ +vim.g.omni_sql_default_compl_type = "syntax" + +local ts_lib = Config.treesitter_helpers +local global_nodes_sql = { 'program', 'cte' } +ts_lib.setup_keybindings(global_nodes_sql) + +-- SQL specific keybindings +vim.keymap.set({ 'n' }, ';', function() + vim.api.nvim_call_function('slime#send', { ";\n" }) + end, + { noremap = true, silent = true, desc = "SQL statment return", buffer = true }) diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..fe39f8b --- /dev/null +++ b/init.lua @@ -0,0 +1,28 @@ +_G.Config = {} +local nix = require('config.nix').init { non_nix_value = true } +Config.isNixCats = nix.is_nix + + +require('lze').register_handlers(require('nixCatsUtils.lzUtils').for_cat) + +local mini_deps = require('mini.deps') + +if not Config.isNixCats then + local path_package = vim.fn.stdpath('data') .. '/site/' + local mini_path = path_package .. 'pack/deps/start/mini.nvim' + if not vim.uv.fs_stat(mini_path) then + vim.cmd('echo "Installing `mini.nvim`" | redraw') + local clone_cmd = { + 'git', 'clone', '--filter=blob:none', + 'https://github.com/echasnovski/mini.nvim', mini_path + } + vim.fn.system(clone_cmd) + vim.cmd('packadd mini.nvim | helptags ALL') + vim.cmd('echo "Installed `mini.nvim`" | redraw') + end + + -- Set up 'mini.deps' (customize to your liking) + mini_deps.setup({ path = { package = path_package } }) +else + mini_deps.setup() +end diff --git a/lua/config/nix.lua b/lua/config/nix.lua new file mode 100644 index 0000000..2a7433e --- /dev/null +++ b/lua/config/nix.lua @@ -0,0 +1,166 @@ +local M = {} + +local function detect_nix() + if vim.g.nix_info_plugin_name ~= nil then + return true + end + if vim.g[ [[nixCats-special-rtp-entry-nixCats]] ] ~= nil then + return true + end + return false +end + +M.is_nix = detect_nix() +M.non_nix_default = true + +local function setup_nixcats(non_nix_value) + if M.is_nix then + return + end + + local ok, utils = pcall(require, "nixCatsUtils") + if ok and utils and utils.setup then + utils.setup { non_nix_value = non_nix_value } + end +end + +local function init_from_nix_info() + if vim.g.nix_info_plugin_name == nil then + return nil + end + + local ok, nix_info = pcall(require, vim.g.nix_info_plugin_name) + if not ok or not nix_info then + return nil + end + + local function cat_lookup(cat) + if type(cat) == "table" then + for _, name in ipairs(cat) do + if nix_info(false, "settings", "cats", name) or nix_info(false, "info", "cats", name) then + return true + end + end + return false + end + return nix_info(false, "settings", "cats", cat) or nix_info(false, "info", "cats", cat) + end + + local nc = setmetatable({ + cats = nix_info.settings and nix_info.settings.cats or {}, + settings = nix_info.settings or {}, + get = function(_, default, ...) return nix_info(default, ...) end, + }, { + __call = function(_, default, ...) + if select("#", ...) == 0 then + return cat_lookup(default) + end + return nix_info(default, ...) + end, + __index = nix_info, + }) + + _G.nixCats = nc + package.preload['nixCats.cats'] = function() + return setmetatable(_G.nixCats.cats or {}, getmetatable(_G.nixCats)) + end + + return nc +end + +local function get_nixcats() + if _G.nixCats then + return _G.nixCats + end + + local nc = init_from_nix_info() + if nc then + return nc + end + + local ok, nc_module = pcall(require, "nixCats") + if ok and nc_module then + return nc_module + end + + return nil +end + +function M.init(opts) + local non_nix_value = true + if type(opts) == "table" and type(opts.non_nix_value) == "boolean" then + non_nix_value = opts.non_nix_value + end + + M.non_nix_default = non_nix_value + setup_nixcats(non_nix_value) + get_nixcats() + M.is_nix = detect_nix() + return M +end + +local function cat_enabled(nc, cat, default) + if not nc then + return default + end + + if type(cat) == "table" then + for _, name in ipairs(cat) do + if nc(name) then + return true + end + end + return default + end + + local val = nc(cat) + if val == nil then + return default + end + return val +end + +function M.get_cat(cat, default) + if default == nil then + default = M.non_nix_default + end + if not M.is_nix then + return default + end + local nc = get_nixcats() + return cat_enabled(nc, cat, default) +end + +function M.get_setting(default, ...) + if not M.is_nix then + return default + end + local nc = get_nixcats() + if not nc then + return default + end + + local value = nc(default, "settings", ...) + if value == nil then + return default + end + return value +end + +function M.get_info(default, ...) + if not M.is_nix then + return default + end + local nc = get_nixcats() + if not nc then + return default + end + + local value = nc(default, "info", ...) + if value == nil then + return default + end + return value +end + +return M diff --git a/lua/nixCatsUtils/init.lua b/lua/nixCatsUtils/init.lua new file mode 100644 index 0000000..fd4a44a --- /dev/null +++ b/lua/nixCatsUtils/init.lua @@ -0,0 +1,129 @@ +--[[ + This directory is the luaUtils template. + You can choose what things from it that you would like to use. + And then delete the rest. + Everything in this directory is optional. +--]] + +local M = {} + +--[[ + This file is for making your config still work WITHOUT nixCats. + When you don't use nixCats to load your config, + you wont have the nixCats plugin. + + The setup function defined here defines a mock nixCats plugin when nixCats wasnt used to load the config. + This will help avoid indexing errors when the nixCats plugin doesnt exist. + + NOTE: If you only ever use nixCats to load your config, you don't need this file. +--]] + +---@type boolean +M.isNixCats = vim.g.nix_info_plugin_name ~= nil or vim.g[ [[nixCats-special-rtp-entry-nixCats]] ] ~= nil + +---@class nixCatsSetupOpts +---@field non_nix_value boolean|nil + +---This function will setup a mock nixCats plugin when not using nix +---It will help prevent you from running into indexing errors without a nixCats plugin from nix. +---If you loaded the config via nix, it does nothing +---non_nix_value defaults to true if not provided or is not a boolean. +---@param v nixCatsSetupOpts +function M.setup(v) + if not M.isNixCats then + local nixCats_default_value + if type(v) == "table" and type(v.non_nix_value) == "boolean" then + nixCats_default_value = v.non_nix_value + else + nixCats_default_value = true + end + local mk_with_meta = function (tbl) + return setmetatable(tbl, { + __call = function(_, attrpath) + local strtable = {} + if type(attrpath) == "table" then + strtable = attrpath + elseif type(attrpath) == "string" then + for key in attrpath:gmatch("([^%.]+)") do + table.insert(strtable, key) + end + else + print("function requires a table of strings or a dot separated string") + return + end + return vim.tbl_get(tbl, unpack(strtable)); + end + }) + end + package.preload['nixCats'] = function () + local ncsub = { + get = function(_) return nixCats_default_value end, + cats = mk_with_meta({ + nixCats_config_location = vim.fn.stdpath('config'), + wrapRc = false, + }), + settings = mk_with_meta({ + nixCats_config_location = vim.fn.stdpath('config'), + configDirName = os.getenv("NVIM_APPNAME") or "nvim", + wrapRc = false, + }), + petShop = mk_with_meta({}), + extra = mk_with_meta({}), + pawsible = mk_with_meta({ + allPlugins = { + start = {}, + opt = {}, + }, + }), + configDir = vim.fn.stdpath('config'), + packageBinPath = os.getenv('NVIM_WRAPPER_PATH_NIX') or vim.v.progpath + } + return setmetatable(ncsub, {__call = function(_, cat) return ncsub.get(cat) end}) + end + _G.nixCats = require('nixCats') + end +end + +---allows you to guarantee a boolean is returned, and also declare a different +---default value than specified in setup when not using nix to load the config +---@overload fun(v: string|string[]): boolean +---@overload fun(v: string|string[], default: boolean): boolean +function M.enableForCategory(v, default) + if M.isNixCats or default == nil then + if nixCats(v) then + return true + else + return false + end + else + return default + end +end + +---if nix, return value of nixCats(v) else return default +---Exists to specify a different non_nix_value than the one in setup() +---@param v string|string[] +---@param default any +---@return any +function M.getCatOrDefault(v, default) + if M.isNixCats then + return nixCats(v) + else + return default + end +end + +---for conditionally disabling build steps on nix, as they are done via nix +---I should probably have named it dontAddIfCats or something. +---@overload fun(v: any): any|nil +---Will return the second value if nix, otherwise the first +---@overload fun(v: any, o: any): any +function M.lazyAdd(v, o) + if M.isNixCats then + return o + else + return v + end +end + +return M diff --git a/lua/nixCatsUtils/lzUtils.lua b/lua/nixCatsUtils/lzUtils.lua new file mode 100644 index 0000000..8cc8930 --- /dev/null +++ b/lua/nixCatsUtils/lzUtils.lua @@ -0,0 +1,37 @@ +--[[ + This directory is the luaUtils template. + You can choose what things from it that you would like to use. + And then delete the rest. + Everything in this directory is optional. +--]] + +local M = {} +local nix = require('config.nix') +-- A nixCats specific lze handler that you can use to conditionally enable by category easier. +-- at the start of your config, register with +-- require('lze').register_handlers(require('nixCatsUtils.lzUtils').for_cat) +-- before any calls to require('lze').load using the handler have been made. +-- accepts: +-- for_cat = { "your" "cat" }; +-- for_cat = { cat = { "your" "cat" }, default = bool } +-- for_cat = "your.cat"; +-- for_cat = { cat = "your.cat", default = bool } +-- where default is an alternate value for when nixCats was NOT used to install the config +M.for_cat = { + spec_field = "for_cat", + set_lazy = false, + modify = function(plugin) + if type(plugin.for_cat) == "table" and plugin.for_cat.cat ~= nil then + local default = plugin.for_cat.default + if default == nil then + default = false + end + plugin.enabled = nix.get_cat(plugin.for_cat.cat, default) + else + plugin.enabled = nix.get_cat(plugin.for_cat, false) + end + return plugin + end, +} + +return M diff --git a/lua/nix_smart_send.lua b/lua/nix_smart_send.lua new file mode 100644 index 0000000..c03af4c --- /dev/null +++ b/lua/nix_smart_send.lua @@ -0,0 +1,196 @@ +local M = {} + +-- Helper function to check if value exists in list (optimized with early return) +local function is_in_list(list, value) + if not list or not value then + return false + end + + for _, v in ipairs(list) do + if v == value then + return true + end + end + return false +end + +-- Define comment node types as constants +local COMMENT_TYPES = { + comment = true, + block_comment = true, + line_comment = true, +} + +function M.get_current_node() + local ts_utils = require('nvim-treesitter.ts_utils') + local cur_win = vim.api.nvim_get_current_win() + return ts_utils.get_node_at_cursor(cur_win, true) +end + +function M.detect_global_node() + local cur_node = M.get_current_node() + local root + + if not cur_node then + -- print("No node detected") + local parser = vim.treesitter.get_parser() + if not parser then + return nil + end + root = parser:parse()[1]:root() + else + root = cur_node:root() + end + + if not root then + return nil + end + + return root:type() +end + +function M.move_to_next_non_empty_line() + local ts_utils = require('nvim-treesitter.ts_utils') + -- Search for the next non-empty line + local line_num = vim.fn.search("[^;\\s]", "W") + + if line_num <= 0 then + -- print("No non-empty line found below the current position") + return false + end + + -- Get the line content and find first non-whitespace character + local line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] + local first_non_ws = line_content:find("%S") or 1 + vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 }) + + local node = M.get_current_node() + if not node or not node:type() then + -- print("No node found") + return false + end + + local global_node_type = M.detect_global_node() + + -- Skip comments and global nodes + while node and (COMMENT_TYPES[node:type()] or node:type() == global_node_type) do + line_num = line_num + 1 + local max_lines = vim.api.nvim_buf_line_count(0) + + if line_num > max_lines then + -- print("Reached end of buffer") + return false + end + + -- Get the line content and find first non-whitespace character + line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] + first_non_ws = line_content:find("%S") or 1 + vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 }) + node = ts_utils.get_node_at_cursor() + end + + return true +end + +function M.vselect_node(node) + local ts_utils = require('nvim-treesitter.ts_utils') + if not node then + return false + end + + local cur_buf = vim.api.nvim_get_current_buf() + ts_utils.update_selection(cur_buf, node, "V") + return true +end + +function M.select_until_global(global_nodes) + local ts_utils = require('nvim-treesitter.ts_utils') + local root_node = M.detect_global_node() + if not root_node and global_nodes then + root_node = global_nodes[1] + end + + -- Use empty table if no global nodes provided + global_nodes = global_nodes or {} + + local node = ts_utils.get_node_at_cursor() + if not node then + -- print("No syntax node found at cursor position") + return nil + end + + local node_type = node:type() + + if node_type == root_node then + -- print("Cursor is on the root " .. root_node .. " node or in an empty area.") + return nil + end + + -- Check if current node is a global + if is_in_list(global_nodes, node_type) then + if M.vselect_node(node) then + return node + end + end + + -- Traverse up the tree until we find a global node or reach the root + local parent = node:parent() + local parent_type = parent:type() or "" + if parent and is_in_list(global_nodes, parent_type) then + if M.vselect_node(node) then + return node + end + end + while parent and not is_in_list(global_nodes, parent:type()) do + node = parent + parent = node:parent() + end + + if M.vselect_node(node) then + return node + end + + return nil +end + +function M.slime_send_region() + -- Check if slime plugin is available + if not vim.fn.exists('*slime#send_op') then + vim.notify("slime plugin not available", vim.log.levels.ERROR) + return + end + + local slime_command = ":call slime#send_op(visualmode(), 1)" + local termcodes = vim.api.nvim_replace_termcodes(slime_command, true, true, true) + + vim.api.nvim_feedkeys(termcodes, "x", true) +end + +function M.send_repl(global_nodes) + local ts_utils = require('nvim-treesitter.ts_utils') + local cur_node = M.get_current_node() + + if not cur_node then + M.move_to_next_non_empty_line() + else + local cur_type = cur_node:type() + if COMMENT_TYPES[cur_type] or is_in_list(global_nodes, cur_type) then + M.move_to_next_non_empty_line() + end + end + + local sel_node = M.select_until_global(global_nodes) + if not sel_node then + -- print("No node selected for REPL") + return + end + + -- Send the selected text to the terminal using vim-slime + M.slime_send_region() + + -- Move cursor and continue + ts_utils.goto_node(sel_node, true) + M.move_to_next_non_empty_line() +end + +return M diff --git a/modules/module/settings/cats.nix b/modules/module/settings/cats.nix new file mode 100644 index 0000000..4c64d3b --- /dev/null +++ b/modules/module/settings/cats.nix @@ -0,0 +1,51 @@ +{ + config, + lib, + ... +}: +{ + options.cats = lib.mkOption { + type = lib.types.attrsOf lib.types.bool; + description = '' + Category toggles used to enable/disable specs by name. + + Keys map directly to specs (e.g., `python` controls `specs.python`). + Set a category to `false` to skip its dependency/plugin specs. + + Available categories: + - clickhouse: Clickhouse client and tools + - customPlugins: local plugin specs + - external: external tools and integrations + - general: core Neovim plugins/features + - gitPlugins: git-related plugins + - julia: Julia tooling and packages + - lua: Lua tooling and LSPs + - markdown: markdown tooling and plugins + - nix: Nix tooling and plugins + - optional: optional tools and utilities + - python: Python tooling and plugins + - r: R tooling and plugins + - test: test-only tooling (disabled by default) + - treesitterParsers: Treesitter parsers + - utils: general utilities + ''; + }; + + config.cats = { + clickhouse = lib.mkDefault false; + customPlugins = lib.mkDefault true; + external = lib.mkDefault true; + general = lib.mkDefault true; + gitPlugins = lib.mkDefault true; + julia = lib.mkDefault false; + lua = lib.mkDefault false; + markdown = lib.mkDefault false; + nix = lib.mkDefault false; + optional = lib.mkDefault false; + python = lib.mkDefault false; + r = lib.mkDefault false; + test = lib.mkDefault false; + treesitterParsers = lib.mkDefault true; + utils = lib.mkDefault true; + }; +} diff --git a/modules/module/settings/core.nix b/modules/module/settings/core.nix new file mode 100644 index 0000000..c00fca4 --- /dev/null +++ b/modules/module/settings/core.nix @@ -0,0 +1,35 @@ +{ + config, + lib, + ... +}: +{ + # Point to the directory containing init.lua, plugin/, lua/, etc. + config.settings.config_directory = ../../..; + + # Default colorscheme and background + config.settings.colorscheme = "kanagawa"; + config.settings.background = "dark"; + + # Enable RC wrapping (allows neovim to find the config) + config.settings.wrapRc = true; + + # Lua packages available to neovim (for :lua require()) + config.settings.nvim_lua_env = lp: + lib.optionals (config.cats.general or false) [ lp.tiktoken_core ]; + + # Binary name for the wrapper + config.binName = "n"; + + # Prevent neovim from loading system-wide config + config.settings.block_normal_config = true; + + # Don't symlink the config (we wrap it instead) + config.settings.dont_link = false; + + # Create additional aliases for the binary + config.settings.aliases = [ "vim" ]; + + # Enable wrapper handling of spec runtimeDeps (template pattern). + config.settings.autowrapRuntimeDeps = true; +} diff --git a/modules/module/settings/env.nix b/modules/module/settings/env.nix new file mode 100644 index 0000000..e8021ab --- /dev/null +++ b/modules/module/settings/env.nix @@ -0,0 +1,27 @@ +{ + config, + pkgs, + lib, + ... +}: +{ + # Environment variables set for the wrapper. + # These are available when running neovim. + config.env = lib.mkMerge [ + (lib.mkIf (config.cats.r or false) { + R_LIBS_USER = "./.Rlibs"; + }) + (lib.mkIf (config.cats.python or false) { + UV_PYTHON_DOWNLOADS = "never"; + UV_PYTHON = pkgs.python.interpreter; + }) + (lib.mkIf (config.cats.test or false) { + TESTVAR = "It worked!"; + }) + ]; + + # Environment variables with defaults (can be overridden by user) + config.envDefault = lib.mkIf (config.cats.test or false) { + TESTVAR2 = "It worked again!"; + }; +} diff --git a/modules/module/settings/hosts.nix b/modules/module/settings/hosts.nix new file mode 100644 index 0000000..0743979 --- /dev/null +++ b/modules/module/settings/hosts.nix @@ -0,0 +1,57 @@ +{ + config, + pkgs, + lib, + ... +}: +{ + config.hosts = lib.mkMerge [ + { + node.nvim-host.enable = true; + perl.nvim-host.enable = true; + ruby.nvim-host.enable = true; + + g = { + nvim-host.enable = true; + nvim-host.package = "${pkgs.neovide}/bin/neovide"; + nvim-host.argv0 = "neovide"; + nvim-host.flags."--neovim-bin" = "${placeholder "out"}/bin/${config.binName}"; + }; + + m = { + nvim-host.enable = false; + nvim-host.package = "${pkgs.uv}/bin/uv"; + nvim-host.argv0 = "uv"; + nvim-host.addFlag = [ + "run" + "marimo" + "edit" + ]; + }; + } + (lib.mkIf (config.cats.julia or true) { + jl = { + nvim-host.enable = true; + nvim-host.package = "${pkgs.julia-bin}/bin/julia"; + nvim-host.argv0 = "julia"; + nvim-host.addFlag = [ + "--project=@." + ]; + }; + }) + (lib.mkIf (config.cats.python or true) { + python3.nvim-host.enable = true; + }) + (lib.mkIf (config.cats.r or true) { + r = { + nvim-host.enable = true; + nvim-host.package = "${pkgs.rWrapper}/bin/R"; + nvim-host.argv0 = "R"; + nvim-host.addFlag = [ + "--no-save" + "--no-restore" + ]; + }; + }) + ]; +} diff --git a/modules/module/settings/lang-packages.nix b/modules/module/settings/lang-packages.nix new file mode 100644 index 0000000..3315363 --- /dev/null +++ b/modules/module/settings/lang-packages.nix @@ -0,0 +1,39 @@ +{ + config, + lib, + ... +}: +{ + options.settings.lang_packages = lib.mkOption { + type = lib.types.submodule { + options = { + python = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Additional Python-related packages appended to the python spec (overlay defaults remain)."; + }; + r = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Additional R-related packages appended to the r spec (overlay defaults remain)."; + }; + julia = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Additional Julia packages (names) passed to julia-bin.withPackages."; + }; + }; + }; + default = { }; + description = '' + Language-specific package overrides appended to each language spec's extraPackages. + Intended for flake.nix overrides via wrapper.config.wrap. + ''; + }; + + config.settings.lang_packages = { + python = lib.mkDefault [ ]; + r = lib.mkDefault [ ]; + julia = lib.mkDefault [ ]; + }; +} diff --git a/modules/module/settings/runtime-path.nix b/modules/module/settings/runtime-path.nix new file mode 100644 index 0000000..96c5d0f --- /dev/null +++ b/modules/module/settings/runtime-path.nix @@ -0,0 +1,35 @@ +{ + config, + lib, + ... +}: +let + collect_runtime_packages = runtime_deps_type: + config.specCollect + (acc: spec: + let + is_enabled = if spec ? enable then spec.enable else true; + has_runtime_deps = (spec.runtimeDeps or false) == runtime_deps_type; + packages = spec.extraPackages or [ ]; + in + acc ++ lib.optionals (is_enabled && has_runtime_deps) packages + ) + [ ]; + + prefix_packages = collect_runtime_packages "prefix"; + suffix_packages = collect_runtime_packages "suffix"; + + to_path_specs = packages: [ + { + data = [ + "PATH" + ":" + "${lib.makeBinPath packages}" + ]; + } + ]; +in +{ + config.prefixVar = lib.optionals (prefix_packages != [ ]) (to_path_specs prefix_packages); + config.suffixVar = lib.optionals (suffix_packages != [ ]) (to_path_specs suffix_packages); +} diff --git a/modules/module/specs/cats-enable.nix b/modules/module/specs/cats-enable.nix new file mode 100644 index 0000000..f5a9826 --- /dev/null +++ b/modules/module/specs/cats-enable.nix @@ -0,0 +1,51 @@ +{ config, lib, ... }: +{ + # This module implements category-based enabling of specs. + # It runs early (order 200) so other specMaps can see the enable flags. + # + # How it works: + # 1. For each spec, extract its name (removing -lazy suffix if present) + # 2. Check if there's a corresponding cats. toggle + # 3. Set spec.value.enable based on the cats toggle (default: true) + # 4. This allows specs to be conditionally included based on config.cats settings + # + # Example: If config.cats.python = false, then specs.python.enable = false + + config.specMaps = lib.mkOrder 200 [ + { + name = "CATS_ENABLE"; + data = + list: + map ( + v: + if v.type == "spec" || v.type == "parent" then + let + # Extract spec name, handling lazy specs (remove -lazy suffix) + specName = + if v.name == null then + null + else if lib.hasSuffix "-lazy" v.name then + lib.removeSuffix "-lazy" v.name + else + v.name; + + # Check if this spec has a corresponding cat toggle + catEnabled = + if specName != null && builtins.hasAttr specName config.cats then + config.cats.${specName} + else + true; # Default to enabled if no cat toggle exists + in + v + // { + value = v.value // { + # Use explicit enable if set, otherwise use cat toggle + enable = if v.value ? enable then v.value.enable else catEnabled; + }; + } + else + v + ) list; + } + ]; +} diff --git a/modules/module/specs/deps.nix b/modules/module/specs/deps.nix new file mode 100644 index 0000000..a8bfaf6 --- /dev/null +++ b/modules/module/specs/deps.nix @@ -0,0 +1,188 @@ +{ + config, + pkgs, + lib, + wlib, + ... +}: { + # ============================================================================ + # SPEC MODULE DEFAULTS + # ============================================================================ + # Define default options available to all specs + + config.specMods = {parentSpec ? null, ...}: { + options.extraPackages = lib.mkOption { + type = lib.types.listOf wlib.types.stringable; + default = []; + description = "a extraPackages spec field to put packages to suffix to the PATH"; + }; + }; + + # ============================================================================ + # EXTERNAL TOOLS SPEC + # ============================================================================ + # Core system tools and utilities + + config.specs.external = { + data = lib.mkDefault null; + before = ["INIT_MAIN"]; + config = '' + vim.o.shell = "${pkgs.zsh}/bin/zsh" + ''; + runtimeDeps = "prefix"; + extraPackages = with pkgs; [ + perl + ruby + shfmt + sqlfluff + tree-sitter + ]; + }; + + # ============================================================================ + # OPTIONAL TOOLS SPEC + # ============================================================================ + + config.specs.optional = lib.mkIf (config.cats.optional or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + before = ["INIT_MAIN"]; + extraPackages = with pkgs; [ + bat + broot + devenv + dust + fd + fzf + gawk + gh + git + hunspell + hunspellDicts.de-at + hunspellDicts.en-us + ispell + jq + just + lazygit + man + ncdu + pigz + poppler + ripgrep + tokei + wget + yq + zathura + ]; + }; + + # ============================================================================ + # MARKDOWN SPEC + # ============================================================================ + + config.specs.markdown = lib.mkIf (config.cats.markdown or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = with pkgs; [ + python313Packages.pylatexenc + quarto + zk + ]; + }; + + # ============================================================================ + # NIX SPEC + # ============================================================================ + + config.specs.nix = lib.mkIf (config.cats.nix or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = with pkgs; [ + alejandra + nix-doc + nixd + ]; + }; + + # ============================================================================ + # LUA SPEC + # ============================================================================ + + config.specs.lua = lib.mkIf (config.cats.lua or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = with pkgs; [ + lua-language-server + ]; + }; + + # ============================================================================ + # PYTHON SPEC + # ============================================================================ + + config.specs.python = lib.mkIf (config.cats.python or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = let + python_packages_fn = + if pkgs ? basePythonPackages + then ps: pkgs.basePythonPackages ps ++ config.settings.lang_packages.python + else _: config.settings.lang_packages.python; + python_with_packages = pkgs.python3.withPackages python_packages_fn; + in + with pkgs; [ + python_with_packages + nodejs + ruff + basedpyright + uv + ]; + }; + + # ============================================================================ + # R SPEC + # ============================================================================ + + config.specs.r = lib.mkIf (config.cats.r or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = let + r_packages = (pkgs.baseRPackages or []) ++ config.settings.lang_packages.r; + in + with pkgs; [ + (rWrapper.override {packages = r_packages;}) + radianWrapper + (quarto.override {extraRPackages = r_packages;}) + air-formatter + yaml-language-server + updateR + ]; + }; + + # ============================================================================ + # JULIA SPEC + # ============================================================================ + + config.specs.julia = lib.mkIf (config.cats.julia or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = let + julia_with_packages = + pkgs.julia-bin.withPackages config.settings.lang_packages.julia; + in [julia_with_packages]; + }; + + # ============================================================================ + # CLICKHOUSE SPEC + # ============================================================================ + + config.specs.clickhouse = lib.mkIf (config.cats.clickhouse or true) { + data = lib.mkDefault null; + runtimeDeps = "prefix"; + extraPackages = with pkgs; [ + clickhouse-lts + ]; + }; + + config.extraPackages = config.specCollect (acc: v: acc ++ (v.extraPackages or [])) []; +} diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix new file mode 100644 index 0000000..62ddda6 --- /dev/null +++ b/modules/module/specs/plugins.nix @@ -0,0 +1,183 @@ +{ + config, + pkgs, + lib, + ... +}: +{ + config.specs.gitPlugins = { + data = [ ]; + }; + + config.specs.r = { + data = [ + config.nvim-lib.neovimPlugins.r + ]; + }; + + config.specs.markdown-lazy = { + lazy = true; + data = [ + config.nvim-lib.neovimPlugins.cmp-pandoc-references + ]; + }; + + config.specs.r-lazy = { + lazy = true; + data = [ + config.nvim-lib.neovimPlugins.cmp-r + ]; + }; + + config.specs.general = { + data = with pkgs.vimPlugins; [ + lze + lzextras + plenary-nvim + neogit + { + data = mini-nvim; + pname = "mini.nvim"; + } + { + data = cyberdream-nvim; + pname = "cyberdream"; + } + { + data = onedark-nvim; + pname = "onedark"; + } + { + data = tokyonight-nvim; + pname = "tokyonight"; + } + { + data = kanagawa-nvim; + pname = "kanagawa"; + } + { + data = gruvbox-nvim; + pname = "gruvbox"; + } + { + data = nord-nvim; + pname = "nord"; + } + { + data = dracula-nvim; + pname = "dracula"; + } + { + data = vscode-nvim; + pname = "vscode"; + } + { + data = nightfox-nvim; + pname = "nightfox"; + } + { + data = catppuccin-nvim; + pname = "catppuccin"; + } + ]; + }; + + config.specs.lua = { + data = with pkgs.vimPlugins; [ + luvit-meta + { + data = lazydev-nvim; + pname = "lazydev"; + } + ]; + }; + + config.specs.markdown = { + data = with pkgs.vimPlugins; [ + quarto-nvim + render-markdown-nvim + { + data = otter-nvim; + pname = "otter"; + } + { + data = zk-nvim; + pname = "zk"; + } + ]; + }; + + config.specs.utils = { + data = with pkgs.vimPlugins; [ + blink-cmp + nvim-lspconfig + nvim-treesitter-context + nvim-treesitter-textobjects + { + data = pkgs.codecompanion-nvim; + pname = "codecompanion"; + } + ]; + }; + + config.specs.treesitterParsers = { + data = with pkgs.vimPlugins.nvim-treesitter-parsers; [ + bash + c + cpp + csv + diff + dockerfile + git_config + git_rebase + gitattributes + gitcommit + gitignore + html + javascript + json + julia + latex + lua + luadoc + make + markdown + markdown_inline + nix + python + query + r + rnoweb + regex + sql + toml + vim + vimdoc + xml + yaml + zig + ]; + }; + + config.specs.utils-lazy = { + lazy = true; + data = with pkgs.vimPlugins; [ + blink-compat + blink-copilot + cmp-cmdline + colorful-menu-nvim + conform-nvim + copilot-lua + nvim-dap + nvim-dap-ui + nvim-dap-virtual-text + nvim-lint + vim-slime + ]; + }; + + config.specs.gitPlugins-lazy = { + lazy = true; + data = [ ]; + }; +} diff --git a/modules/neovim.nix b/modules/neovim.nix new file mode 100644 index 0000000..ba0361c --- /dev/null +++ b/modules/neovim.nix @@ -0,0 +1,71 @@ +inputs: +{ + config, + wlib, + lib, + pkgs, + ... +}: +{ + # ============================================================================ + # IMPORTS + # ============================================================================ + # Import the base neovim wrapper module and all configuration modules + + imports = [ + wlib.wrapperModules.neovim + ./module/specs/deps.nix + ./module/specs/plugins.nix + ./module/specs/cats-enable.nix + ./module/settings/core.nix + ./module/settings/cats.nix + ./module/settings/env.nix + ./module/settings/hosts.nix + ./module/settings/lang-packages.nix + ./module/settings/runtime-path.nix + ]; + + # ============================================================================ + # HELPER FUNCTIONS + # ============================================================================ + # Utilities for working with plugin inputs + + options.nvim-lib.neovimPlugins = lib.mkOption { + readOnly = true; + type = lib.types.attrsOf wlib.types.stringable; + default = config.nvim-lib.pluginsFromPrefix "plugins-" inputs; + }; + + options.nvim-lib.pluginsFromPrefix = lib.mkOption { + type = lib.types.raw; + readOnly = true; + default = + prefix: inputs: + lib.pipe inputs [ + builtins.attrNames + (builtins.filter (s: lib.hasPrefix prefix s)) + (map ( + input: + let + name = lib.removePrefix prefix input; + in + { + inherit name; + value = config.nvim-lib.mkPlugin name inputs.${input}; + } + )) + builtins.listToAttrs + ]; + }; + + # ============================================================================ + # CONFIGURATION + # ============================================================================ + # Pass cats configuration to neovim and expose metadata + + config.settings.cats = config.cats; + config.info.cats = config.cats; + config.info.nixCats_config_location = config.settings.config_directory; + config.info.nixCats_wrapRc = config.settings.wrapRc or false; + config.info.nixCats_configDirName = "nvim"; +} diff --git a/overlays/README.md b/overlays/README.md new file mode 100644 index 0000000..bdeb78c --- /dev/null +++ b/overlays/README.md @@ -0,0 +1,98 @@ +# Overlays + +This directory contains composable Nix overlays used by the Neovim wrapper configuration. Each overlay is small and focused, so you can reuse or override them downstream. + +## Files + +- `r.nix` + R-related overrides (rix overlay). Exposes `pkgs.rpkgs` from rstats-on-nix and creates pre-configured `rWrapper` and `quarto` with standard R packages. + +- `python.nix` + Python-related overrides and package additions (e.g., extra Python packages). + +- `plugins.nix` + Neovim plugin overrides (e.g., patching or pinning plugin derivations). + +- `default.nix` + Aggregates and exports the overlays in a composable way. Includes the fran overlay for custom R packages. + +## Exports from `default.nix` + +`overlays/default.nix` exposes: + +- `rOverlay` - rix overlay for R packages from rstats-on-nix +- `franOverlay` - fran overlay for custom R packages and tools +- `pythonOverlay` +- `pluginsOverlay` +- `dependencyOverlays` (list of overlays in order) +- `dependencyOverlay` (composed overlay via `lib.composeManyExtensions`) +- `default` (alias of `dependencyOverlay`) +- `dependencies` (alias of `dependencyOverlays`) + +## Downstream usage examples + +### Use the composed default overlay + +```/dev/null/example.nix#L1-18 +{ + inputs, + ... +}: +let + overlayDefs = import ./overlays/default.nix inputs; +in { + nixpkgs.overlays = [ + overlayDefs.default + ]; +} +``` + +### Use specific overlays only + +```/dev/null/example.nix#L1-22 +{ + inputs, + ... +}: +let + overlayDefs = import ./overlays/default.nix inputs; +in { + nixpkgs.overlays = [ + overlayDefs.rOverlay + overlayDefs.pluginsOverlay + ]; +} +``` + +### Extend with your own overlay (composition) + +```/dev/null/example.nix#L1-29 +{ + inputs, + ... +}: +let + overlayDefs = import ./overlays/default.nix inputs; + myOverlay = final: prev: { + # Example: override a package + myTool = prev.myTool.override { /* ... */ }; + }; +in { + nixpkgs.overlays = [ + overlayDefs.default + myOverlay + ]; +} +``` + +## Adding a new overlay + +1. Create a new overlay file in this directory (e.g., `foo.nix`). +2. Import it in `overlays/default.nix` and add it to `dependencyOverlays`. +3. Optionally expose it as a named export (e.g., `fooOverlay`) for downstream reuse. + +## Notes + +- Keep overlays composable and focused. +- Avoid monolithic overlays; prefer small, purpose-specific overlays. +- When overriding plugins, keep patches minimal and document the intent. \ No newline at end of file diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..39910b4 --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,42 @@ +{ nixpkgs, ... }@inputs: +let + lib = nixpkgs.lib; + + rOverlay = import ./r.nix inputs; + franOverlay = inputs.fran.overlays.default; + pythonOverlay = import ./python.nix inputs; + pluginsOverlay = import ./plugins.nix inputs; + + dependencyOverlays = [ + rOverlay + franOverlay + pythonOverlay + pluginsOverlay + ]; + dependencyOverlay = lib.composeManyExtensions dependencyOverlays; +in +{ + inherit + rOverlay + franOverlay + pythonOverlay + pluginsOverlay + dependencyOverlays + dependencyOverlay; + + # Named exports for downstream composition. + default = dependencyOverlay; + dependencies = dependencyOverlays; + + overlays = { + inherit + rOverlay + franOverlay + pythonOverlay + pluginsOverlay + dependencyOverlays + dependencyOverlay; + default = dependencyOverlay; + dependencies = dependencyOverlays; + }; +} diff --git a/overlays/plugins.nix b/overlays/plugins.nix new file mode 100644 index 0000000..91c01aa --- /dev/null +++ b/overlays/plugins.nix @@ -0,0 +1,25 @@ +{ ... }: +final: prev: +{ + codecompanion-nvim = prev.vimPlugins.codecompanion-nvim.overrideAttrs { + checkInputs = with prev.vimPlugins; [ + blink-cmp + mini-nvim + ]; + dependencies = [ prev.vimPlugins.plenary-nvim ]; + nvimSkipModules = [ + "codecompanion.actions.static" + "codecompanion.actions.init" + "minimal" + "codecompanion.providers.actions.fzf_lua" + "codecompanion.providers.completion.cmp.setup" + "codecompanion.providers.actions.telescope" + "codecompanion.providers.actions.snacks" + ]; + }; + zk-nvim = prev.vimPlugins.zk-nvim.overrideAttrs { + nvimSkipModules = [ + "zk.pickers.fzf_lua" + ]; + }; +} diff --git a/overlays/python.nix b/overlays/python.nix new file mode 100644 index 0000000..bb17e94 --- /dev/null +++ b/overlays/python.nix @@ -0,0 +1,12 @@ +{ ... }: +final: prev: +let + reqPkgs = pyPackages: + with pyPackages; [ + numpy + ]; +in +{ + basePythonPackages = reqPkgs; + python = prev.python3.withPackages reqPkgs; +} diff --git a/overlays/r.nix b/overlays/r.nix new file mode 100644 index 0000000..dce7be3 --- /dev/null +++ b/overlays/r.nix @@ -0,0 +1,46 @@ +# R packages overlay (rix) +# +# This overlay provides access to R packages from rstats-on-nix. +# +# rstats-on-nix maintains snapshots of CRAN packages built with Nix: +# - Provides reproducible R package versions +# - Ensures binary cache availability for faster builds +# - Maintained by the rstats-on-nix community +# +# Available attributes after applying this overlay: +# - pkgs.rpkgs: R packages from rstats-on-nix +# - pkgs.rpkgs.rPackages: All CRAN packages +# - pkgs.rpkgs.quarto: Quarto publishing system +# - pkgs.rpkgs.rWrapper: R with package management +# - pkgs.rWrapper: R wrapper with standard packages pre-configured +# - pkgs.quarto: Quarto with R integration and standard packages +# +# Custom R packages and tools (radianWrapper, air-formatter) come from +# the fran overlay which should be applied separately. +# +# To use specific R packages, reference them via: +# with pkgs.rpkgs.rPackages; [ package1 package2 ] +# +# Update the R snapshot date in flake.nix inputs section: +# rixpkgs.url = "github:rstats-on-nix/nixpkgs/YYYY-MM-DD" +{rixpkgs, ...}: final: prev: let + # R packages from rstats-on-nix for the current system + rpkgs = rixpkgs.legacyPackages.${prev.stdenv.hostPlatform.system}; + + # Standard R packages used by default in rWrapper and quarto + reqPkgs = with rpkgs.rPackages; [ + languageserver + ]; +in { + inherit rpkgs; + baseRPackages = reqPkgs; + + # R wrapper with standard packages + rWrapper = rpkgs.rWrapper.override {packages = reqPkgs;}; + + # Quarto with R integration + quarto = rpkgs.quarto.override {extraRPackages = reqPkgs;}; + + # Update helper for rix + updateR = import ../scripts/updater.nix { pkgs = final; }; +} diff --git a/plugin/00_options.lua b/plugin/00_options.lua new file mode 100644 index 0000000..2714fa1 --- /dev/null +++ b/plugin/00_options.lua @@ -0,0 +1,183 @@ +--stylua: ignore start +local later = MiniDeps.later +local now = MiniDeps.now +local nix = require('config.nix') + + +if not Config.isNixCats then + now(function() + MiniDeps.add({ name = "mini.nvim" }) + end) +end + +-- Leader key ================================================================= +vim.g.mapleader = ' ' +vim.g.maplocalleader = ',' +-- General ==================================================================== +vim.o.backup = true -- Don't store backup +vim.opt.backupdir = vim.fn.stdpath('data') .. '/backups' +vim.o.mouse = 'a' -- Enable mouse +vim.o.mousescroll = 'ver:25,hor:6' -- Customize mouse scroll +vim.o.scrolloff = 8 -- Lines above and below cursor +vim.o.switchbuf = 'usetab' -- Use already opened buffers when switching +vim.o.writebackup = true -- Don't store backup (better performance) +vim.o.undofile = true -- Enable persistent undo +vim.o.wildmenu = true -- Enable wildmenu +vim.o.wildmode = 'full' -- Show all matches in command line completion + +vim.o.shada = "'100,<50,s10,:1000,/100,@100,h" -- Limit what is stored in ShaDa file + +vim.cmd('filetype plugin indent on') -- Enable all filetype plugins + +-- UI ========================================================================= +vim.o.breakindent = true -- Indent wrapped lines to match line start +vim.o.colorcolumn = '+1' -- Draw colored column one step to the right of desired maximum width +vim.o.cursorline = false -- Enable highlighting of the current line +vim.o.foldenable = false -- Enable folding +vim.o.linebreak = true -- Wrap long lines at 'breakat' (if 'wrap' is set) +vim.o.list = true -- Show helpful character indicators +vim.o.number = true -- Show line numbers +vim.o.pumheight = 10 -- Make popup menu smaller +vim.o.relativenumber = true -- Show relative line numbers +vim.o.ruler = false -- Don't show cursor position +vim.o.shortmess = 'FOSWaco' -- Disable certain messages from |ins-completion-menu| +vim.o.showmode = false -- Don't show mode in command line +vim.o.signcolumn = 'yes' -- Always show signcolumn or it would frequently shift +vim.o.splitbelow = true -- Horizontal splits will be below +vim.o.splitright = true -- Vertical splits will be to the right +vim.o.wrap = true -- Display long lines as just one line +vim.o.background = nix.get_setting('dark', "background") --'light' -- Set background + +vim.o.listchars = table.concat({ 'extends:…', 'nbsp:␣', 'precedes:…', 'tab:> ', 'trail:·' }, ',') -- Special text symbols +vim.o.cursorlineopt = 'screenline,number' -- Show cursor line only screen line when wrapped +vim.o.breakindentopt = 'list:-1' -- Add padding for lists when 'wrap' is on + +if vim.fn.has('nvim-0.9') == 1 then + vim.o.shortmess = 'CFOSWaco' -- Don't show "Scanning..." messages + vim.o.splitkeep = 'screen' -- Reduce scroll during window split +end + +if vim.fn.has('nvim-0.10') == 0 then + vim.o.termguicolors = true -- Enable gui colors (Neovim>=0.10 does this automatically) +end + +if vim.fn.has('nvim-0.11') == 1 then + -- vim.o.completeopt = 'menuone,fuzzy' -- Use fuzzy matching for built-in completion noselect, + vim.o.winborder = 'rounded' -- Use double-line as default border +end +if vim.fn.has('nvim-0.12') == 1 then + vim.o.pummaxwidth = 100 -- Limit maximum width of popup menu + vim.o.completefuzzycollect = 'keyword,files,whole_line' -- Use fuzzy matching when collecting candidates +end + +vim.o.complete = '.,w,b,kspell' -- Use spell check and don't use tags for completion +-- Colors ===================================================================== +-- Enable syntax highlighing if it wasn't already (as it is time consuming) +-- Don't use defer it because it affects start screen appearance +if vim.fn.exists('syntax_on') ~= 1 then vim.cmd('syntax enable') end + +-- Editing ==================================================================== +vim.o.autoindent = true -- Use auto indent +vim.o.expandtab = true -- Convert tabs to spaces +vim.o.formatoptions = 'qnl1j' -- Improve comment editing +vim.o.ignorecase = true -- Ignore case when searching (use `\C` to force not doing that) +vim.o.incsearch = true -- Show search results while typing +vim.o.infercase = true -- Infer letter cases for a richer built-in keyword completion +vim.o.shiftwidth = 2 -- Use this number of spaces for indentation +vim.o.smartcase = true -- Don't ignore case when searching if pattern has upper case +vim.o.smartindent = true -- Make indenting smart +vim.o.tabstop = 2 -- Insert 2 spaces for a tab +vim.o.virtualedit = 'block' -- Allow going past the end of line in visual block mode + +vim.o.iskeyword = '@,48-57,_,192-255,-' -- Treat dash separated words as a word text object + +-- Define pattern for a start of 'numbered' list. This is responsible for +-- correct formatting of lists when using `gw`. This basically reads as 'at +-- least one special character (digit, -, +, *) possibly followed some +-- punctuation (. or `)`) followed by at least one space is a start of list +-- item' +vim.o.formatlistpat = [[^\s*[0-9\-\+\*]\+[\.\)]*\s\+]] + +-- Spelling =================================================================== +vim.o.spelllang = 'en_us,de' -- Define spelling dictionaries +vim.o.spelloptions = 'camel' -- Treat parts of camelCase words as seprate words + +-- vim.o.dictionary = vim.fn.stdpath('config') .. '/misc/dict/english.txt' -- Use specific dictionaries + +-- Folds ====================================================================== +vim.o.foldmethod = 'indent' -- Set 'indent' folding method +vim.o.foldlevel = 1 -- Display all folds except top ones +vim.o.foldnestmax = 10 -- Create folds only for some number of nested levels +vim.g.markdown_folding = 1 -- Use folding by heading in markdown files + +if vim.fn.has('nvim-0.10') == 1 then + vim.o.foldtext = '' -- Use underlying text with its highlighting +end + + + +-- Diagnostics ================================================================ +local diagnostic_opts = { + -- Define how diagnostic entries should be shown + signs = { priority = 9999, severity = { min = 'HINT', max = 'ERROR' } }, + virtual_lines = false, + virtual_text = { current_line = false, severity = { min = 'WARN', max = 'ERROR' } }, + jump = { float = false }, + underline = false, + -- Don't update diagnostics when typing + update_in_insert = false, +} +later(function() vim.diagnostic.config(diagnostic_opts) end) + + +-- Custom autocommands ======================================================== +local augroup = vim.api.nvim_create_augroup('CustomSettings', {}) + +vim.api.nvim_create_autocmd('FileType', { + pattern = { 'markdown' }, + group = augroup, + callback = function() + vim.diagnostic.config({ + signs = { + severity = { min = 'WARN', max = 'ERROR' } + }, + virtual_text = { + current_line = false, + severity = { min = 'HINT', max = 'ERROR' } + } + }) + end +}) + +vim.api.nvim_create_autocmd("FileType", { + desc = "remove formatoptions", + callback = function() + vim.opt.formatoptions:remove({ "r", "o" }) -- Don't continue comments on enter or o + vim.b.minitrailspace_disable = true -- Don't highlight trailing space by default + end, +}) + +vim.api.nvim_create_autocmd('FileType', { + group = augroup, + callback = function() + -- Don't auto-wrap comments and don't insert comment leader after hitting 'o' + -- If don't do this on `FileType`, this keeps reappearing due to being set in + -- filetype plugins. + vim.cmd('setlocal formatoptions-=r formatoptions-=o') + end, + desc = [[Ensure proper 'formatoptions']], +}) +-- Neovide ============================================== +if vim.g.neovide then + vim.g.neovide_cursor_vfx_mode = "pixiedust" + vim.g.neovide_cursor_smooth_blink = true + vim.g.neovide_cursor_animation_length = 0.02 + vim.g.neovide_cursor_short_animation_length = 0 + vim.g.neovide_font_hinting = 'none' + vim.g.neovide_font_edging = 'subpixelantialias' + vim.o.guifont = 'Iosevka Nerd Font,Symbols Nerd Font:h14:#e-subpixelantialias:#h-none' + vim.g.neovide_floating_corner_radius = 0.35 + vim.keymap.set("n", "nf", "NeovideFullscreen", { desc = "Toggle Neovide Fullscreen" }) +end + +--stylua: ignore end diff --git a/plugin/01_lib.lua b/plugin/01_lib.lua new file mode 100644 index 0000000..1197ee8 --- /dev/null +++ b/plugin/01_lib.lua @@ -0,0 +1,119 @@ +-- Global Functions +Config.new_scratch_buffer = function() vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(true, true)) end + +-- Toggle quickfix window +Config.toggle_quickfix = function() + local cur_tabnr = vim.fn.tabpagenr() + for _, wininfo in ipairs(vim.fn.getwininfo()) do + if wininfo.quickfix == 1 and wininfo.tabnr == cur_tabnr then return vim.cmd('cclose') end + end + vim.cmd('copen') +end + +Config.log = {} +Config.log_buf_id = Config.log_buf_id or nil +Config.start_hrtime = Config.start_hrtime or vim.loop.hrtime() + +Config.log_print = function() + if Config.log_buf_id == nil or not vim.api.nvim_buf_is_valid(Config.log_buf_id) then + Config.log_buf_id = vim.api.nvim_create_buf(true, true) + end + vim.api.nvim_win_set_buf(0, Config.log_buf_id) + vim.api.nvim_buf_set_lines(Config.log_buf_id, 0, -1, false, vim.split(vim.inspect(Config.log), '\n')) +end + +Config.log_clear = function() + Config.log = {} + Config.start_hrtime = vim.loop.hrtime() + vim.cmd('echo "Cleared log"') +end + +-- Execute current line with `lua` +Config.execute_lua_line = function() + local line = 'lua ' .. vim.api.nvim_get_current_line() + vim.api.nvim_command(line) + print(line) + vim.api.nvim_input('') +end + +-- Try opening current file's dir with fallback to cwd +Config.try_opendir = function() + local buff = vim.api.nvim_buf_get_name(0) + local ok, err = pcall(MiniFiles.open, buff) + if ok then return end + vim.notify(err) + MiniFiles.open() +end + +-- For mini.start +--- Edit a file in the specified window, with smart buffer reuse +--- @param path string: File path to edit +--- @param win_id number|nil: Window ID (defaults to current window) +--- @return number|nil: Buffer ID on success, nil on failure +Config.edit = function(path, win_id) + -- Validate inputs + if type(path) ~= 'string' or path == '' then + return nil + end + + win_id = win_id or 0 + if not vim.api.nvim_win_is_valid(win_id == 0 and vim.api.nvim_get_current_win() or win_id) then + return nil + end + + local current_buf = vim.api.nvim_win_get_buf(win_id) + + -- Check if current buffer can be reused (empty, unmodified, single window) + local is_empty_buffer = vim.fn.bufname(current_buf) == '' + local is_regular_buffer = vim.bo[current_buf].buftype ~= 'quickfix' + local is_unmodified = not vim.bo[current_buf].modified + local is_single_window = #vim.fn.win_findbuf(current_buf) == 1 + local has_only_empty_line = vim.deep_equal(vim.fn.getbufline(current_buf, 1, '$'), { '' }) + + local can_reuse_buffer = is_empty_buffer and is_regular_buffer and is_unmodified + and is_single_window and has_only_empty_line + + -- Create or get buffer for the file + local normalized_path = vim.fn.fnamemodify(path, ':.') + local target_buf = vim.fn.bufadd(normalized_path) + + -- Set buffer in window (use pcall to handle swap file messages gracefully) + local success = pcall(vim.api.nvim_win_set_buf, win_id, target_buf) + if not success then + return nil + end + + -- Ensure buffer is listed + vim.bo[target_buf].buflisted = true + + -- Clean up old buffer if it was reused + if can_reuse_buffer then + pcall(vim.api.nvim_buf_delete, current_buf, { unload = false }) + end + + return target_buf +end + +-- Load library +local packdir = nixCats.vimPackDir or MiniDeps.config.path.package + +-- See https://github.com/echasnovski/mini.deps/blob/2953b2089591a49a70e0a88194dbb47fb0e4635c/lua/mini/deps.lua#L518C5-L518C39 +Config.source_path = function(path) + pcall(function() vim.cmd('source ' .. vim.fn.fnameescape(path)) end) +end + +Config.add = (function(pkg) + vim.cmd.packadd(pkg) + local should_load_after_dir = vim.v.vim_did_enter == 1 and vim.o.loadplugins + if not should_load_after_dir then return end + local after_paths = vim.fn.glob( + packdir .. '/pack/myNeovimPackages/opt/' .. pkg .. '/after/plugin/**/*.{vim,lua}', + false, + true + ) + vim.iter(after_paths):map(function(p) + Config.source_path(p) + end) +end) + +Config.now_if_args = vim.fn.argc(-1) > 0 and MiniDeps.now or MiniDeps.later diff --git a/plugin/02_startup.lua b/plugin/02_startup.lua new file mode 100644 index 0000000..c4fa2a9 --- /dev/null +++ b/plugin/02_startup.lua @@ -0,0 +1,130 @@ +local M = {} + +-- Helper function to normalize input to a list +local function normalize_filetypes_input(input) + if type(input) == "string" then + return { input } + elseif type(input) == "table" then + return input + else + vim.notify("get_recent_files_by_ft_or_ext: Invalid input type for filetypes", vim.log.levels.ERROR) + return nil + end +end + +-- Helper function to check if a file matches any target filetype +local function matches_target_filetype(file_path, file_ext, detected_ft, target_ft_map) + for target_ft in pairs(target_ft_map) do + if file_ext:lower() == target_ft:lower() or + (detected_ft and detected_ft == target_ft) then + return target_ft + end + end + return nil +end + +-- Helper function to safely detect filetype +local function detect_filetype(file_path) + local success, ft_match_fn = pcall(function() return vim.filetype.match end) + if not (success and type(ft_match_fn) == "function") then + return nil + end + + local ok, result = pcall(ft_match_fn, { filename = file_path }) + return ok and type(result) == "string" and result ~= "" and result or nil +end + +-- Helper function to capitalize first letter +local function capitalize_first(str) + return str:sub(1, 1):upper() .. str:sub(2) +end + +function M.get_recent_files_by_ft_or_ext(target_filetypes_input) + local target_filetypes_list = normalize_filetypes_input(target_filetypes_input) + if not target_filetypes_list or #target_filetypes_list == 0 then + return {} + end + + -- Create lookup map for O(1) filetype checking + local target_ft_map = {} + for _, ft in ipairs(target_filetypes_list) do + target_ft_map[ft] = true + end + + local oldfiles = vim.v.oldfiles + if not oldfiles or #oldfiles == 0 then + return {} + end + + local cwd = vim.fn.getcwd() + local fnamemodify = vim.fn.fnamemodify + local filereadable = vim.fn.filereadable + local getftime = vim.fn.getftime + + -- Track most recent file for each target filetype + local most_recent_files = {} + for _, ft in ipairs(target_filetypes_list) do + most_recent_files[ft] = { file = nil, time = 0 } + end + + local processed_paths = {} + + for _, file_path in ipairs(oldfiles) do + local full_path = fnamemodify(file_path, ':p') + + -- Skip if already processed or invalid + if processed_paths[full_path] or + filereadable(full_path) ~= 1 or + not full_path:find(cwd, 1, true) then + goto continue + end + + processed_paths[full_path] = true + + local file_ext = fnamemodify(full_path, ':e') + local detected_ft = detect_filetype(full_path) + local matched_ft = matches_target_filetype(full_path, file_ext, detected_ft, target_ft_map) + + if matched_ft then + local mod_time = getftime(full_path) + if mod_time > most_recent_files[matched_ft].time then + most_recent_files[matched_ft] = { file = full_path, time = mod_time } + end + end + + ::continue:: + end + + -- Build result items + local result_items = {} + for ft, data in pairs(most_recent_files) do + if data.file then + local filename = fnamemodify(data.file, ':t') + local relative_path = fnamemodify(data.file, ':~:.') + + table.insert(result_items, { + action = function() Config.edit(data.file) end, + name = string.format('%s (%s)', filename, relative_path), + section = 'Recent ' .. capitalize_first(ft), + }) + end + end + + return result_items +end + +M.footer_text = (function() + return [[ +$$$$$$$\ $$\ $$\ $$\ $$\ $$\ $$$$$$$\ $$\ $$\ $$\ $$\ +$$ __$$\ \__| $$ |$ | $$ |$$ |$$ __$$\ $$ | $$ | $$ |\__| +$$ | $$ | $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$ |\_/$$$$$$$\ $$ /$$ / $$ | $$ | $$$$$$\ $$$$$$\ $$$$$$\ $$ | $$ |$$\ $$$$$$\$$$$\ +$$ | $$ | \____$$\ $$ __$$\ $$ |$$ __$$\ $$ | $$ _____| $$ /$$ / $$ | $$ | \____$$\\_$$ _| \____$$\\$$\ $$ |$$ |$$ _$$ _$$\ +$$ | $$ | $$$$$$$ |$$ | $$ |$$ |$$$$$$$$ |$$ | \$$$$$$\ $$ /$$ / $$ | $$ | $$$$$$$ | $$ | $$$$$$$ |\$$\$$ / $$ |$$ / $$ / $$ | +$$ | $$ |$$ __$$ |$$ | $$ |$$ |$$ ____|$$ | \____$$\ $$ /$$ / $$ | $$ |$$ __$$ | $$ |$$\ $$ __$$ | \$$$ / $$ |$$ | $$ | $$ | +$$$$$$$ |\$$$$$$$ |$$ | $$ |$$ |\$$$$$$$\ $$ | $$$$$$$ |$$ /$$ / $$$$$$$ |\$$$$$$$ | \$$$$ |\$$$$$$$ | \$ / $$ |$$ | $$ | $$ | +\_______/ \_______|\__| \__|\__| \_______|\__| \_______/ \__/ \__/ \_______/ \_______| \____/ \_______| \_/ \__|\__| \__| \__| +]] +end +) + +Config.startup = M diff --git a/plugin/03_terminal.lua b/plugin/03_terminal.lua new file mode 100644 index 0000000..95d25b1 --- /dev/null +++ b/plugin/03_terminal.lua @@ -0,0 +1,78 @@ +local M = {} + +-- Configuration +Config.opt_bracket = true +M.opt_term = nil + +-- Default terminal commands +-- Users can override this via Config.terminal_commands in their setup +local defaults = { + clickhouse_client = "clickhouse client -m", + clickhouse_local = "clickhouse local -m", + duckdb = "duckdb", + julia = "julia", + python = "ipython", + shell = "echo 'Hello " .. vim.env.USER .. "!'", +} + +-- Registry of terminal commands +M.commands = vim.tbl_deep_extend("force", defaults, Config.terminal_commands or {}) + +-- Bracket paste control +function M.toggle_bracket() + Config.opt_bracket = not Config.opt_bracket + vim.g.slime_bracketed_paste = Config.opt_bracket + return Config.opt_bracket +end + +-- Terminal management +function M.split_and_open_terminal() + vim.cmd("below terminal") + vim.cmd("resize " .. math.floor(vim.fn.winheight(0) * 0.9)) + local term_buf = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()) + M.opt_term = term_buf + + -- Set buffer-local variables for vim-slime + local job_id = vim.b[term_buf].terminal_job_id + vim.b[term_buf].slime_config = { jobid = job_id } + + return M.opt_term +end + +-- Public functions +function M.open_in_terminal(cmd) + local command = cmd or "" + local current_window = vim.api.nvim_get_current_win() + local code_buf = vim.api.nvim_get_current_buf() + + -- Open terminal and get buffer + local term_buf = M.split_and_open_terminal() + + -- Send command if provided + if command ~= "" then + -- We can use standard slime sending if needed, or direct chan_send for initialization + local job_id = vim.b[term_buf].terminal_job_id + if job_id then + vim.api.nvim_chan_send(job_id, command .. "\r") + end + end + + -- Configure slime for the ORIGINAL code buffer to point to this new terminal + -- This makes "Send to Terminal" work immediately + local slime_config = { jobid = vim.b[term_buf].terminal_job_id } + + -- Fix: Set the variable on the captured code buffer, not the current (terminal) buffer + vim.api.nvim_buf_set_var(code_buf, "slime_config", slime_config) + + -- Switch back to code buffer + vim.api.nvim_set_current_win(current_window) +end + +-- Predefined terminal commands +for name, command in pairs(M.commands) do + M["open_" .. name] = function() + M.open_in_terminal(command) + end +end + +Config.terminal = M diff --git a/plugin/04_treesitter.lua b/plugin/04_treesitter.lua new file mode 100644 index 0000000..5a8d315 --- /dev/null +++ b/plugin/04_treesitter.lua @@ -0,0 +1,147 @@ +local M = {} + +-- Default parsers list moved from startup config +M.default_parsers = { + "bash", "bibtex", "c", "caddy", "cmake", "comment", "commonlisp", "cpp", "css", "csv", + "cuda", "desktop", "diff", "dockerfile", "doxygen", "editorconfig", "fortran", "git_config", "git_rebase", + "gitattributes", "gitcommit", "gitignore", "gnuplot", "go", "gpg", "html", "javascript", "jq", "json", "json5", + "julia", "just", "latex", "ledger", "lua", "luadoc", "luap", "luau", "make", "markdown", "markdown_inline", + "matlab", "meson", "muttrc", "nix", "nu", "passwd", "powershell", "prql", "python", "r", "query", "readline", "regex", + "requirements", "rnoweb", "rust", "sql", "ssh_config", "swift", "tmux", "toml", "tsv", "tsx", "typescript", "typst", + "vala", "vim", "vimdoc", "yaml", "zig", +} + +-- Cache treesitter utils to avoid repeated requires +local smart_send = require('nix_smart_send') + +-- Helper function to check if value exists in list (optimized with early return) +local function is_in_list(list, value) + if not list or not value then + return false + end + + for _, v in ipairs(list) do + if v == value then + return true + end + end + return false +end + + +function M.add_global_node(nodes) + if not nodes then + return nil + end + + local node_type = M.get_type() + if not node_type then + return nodes + end + + -- Create a copy to avoid modifying the original + local global_nodes = vim.deepcopy(nodes) + + -- Check if node type already exists to avoid duplicates + if not is_in_list(global_nodes, node_type) then + table.insert(global_nodes, node_type) + end + + return global_nodes +end + +function M.remove_global_node(nodes) + if not nodes then + return nil + end + + local node_type = M.get_type() + if not node_type then + return nodes + end + + local global_nodes = vim.deepcopy(nodes) + + -- Remove all occurrences (iterate backwards to avoid index issues) + for i = #global_nodes, 1, -1 do + if global_nodes[i] == node_type then + table.remove(global_nodes, i) + end + end + + return global_nodes +end + +function M.set_global_nodes() + local input = vim.fn.input("Enter root nodes: ") + if input == "" then + return {} + end + + local nodes_in = {} + -- Trim whitespace from each node name + for node in string.gmatch(input, '([^,]+)') do + local trimmed = vim.trim(node) + if trimmed ~= "" then + table.insert(nodes_in, trimmed) + end + end + + return nodes_in +end + +function M.get_type() + local cur_node = smart_send.get_current_node() + if not cur_node then + print("Not a node") + return nil + end + + local node_type = cur_node:type() + print("Node type: " .. node_type) + return node_type +end + + +function M.setup_keybindings(global_nodes) + local current_global_nodes = global_nodes + + vim.keymap.set({ 'n' }, 'r', function() + current_global_nodes = M.set_global_nodes() + end, + { noremap = true, silent = true, desc = "set global_nodes", buffer = true }) + + vim.keymap.set({ 'n', 'v' }, 'v', function() + smart_send.move_to_next_non_empty_line(); smart_send.select_until_global(current_global_nodes) + end, + { noremap = true, silent = true, desc = "Visual select next node after WS", buffer = true }) + + vim.keymap.set('n', 'a', function() smart_send.send_repl(current_global_nodes) end, + { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) + + vim.keymap.set({ 'n', 'i' }, '', function() smart_send.send_repl(current_global_nodes) end, + { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) + + vim.keymap.set('n', '', function() smart_send.send_repl(current_global_nodes) end, + { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) + + vim.keymap.set('n', 'n', + function() current_global_nodes = M.add_global_node(current_global_nodes) end, + { noremap = true, silent = true, desc = "Add node under cursor to globals", buffer = true }) + + vim.keymap.set('n', 'x', + function() current_global_nodes = M.remove_global_node(current_global_nodes) end, + { noremap = true, silent = true, desc = "Remove node under cursor from globals", buffer = true }) + + vim.keymap.set('n', 'o', function() + pout = table.concat(global_nodes, ', ') .. "" + print(pout) + end, { noremap = true, silent = true, desc = "Print globals", buffer = true }) + + vim.keymap.set('n', 'p', function() M.get_type() end, + { noremap = true, silent = true, desc = "Print node type", buffer = true }) +end + +Config.treesitter_helpers = M + +return M diff --git a/plugin/10_keymap.lua b/plugin/10_keymap.lua new file mode 100644 index 0000000..c18bfd9 --- /dev/null +++ b/plugin/10_keymap.lua @@ -0,0 +1,318 @@ +-- Basic mappings ============================================================= +-- NOTE: Most basic mappings come from 'mini.basics' +-- Shorter version of the most frequent way of going outside of terminal window +vim.keymap.set('t', '', [[h]]) +-- Select all +-- vim.keymap.set({ "n", "v", "x" }, "", "gg3vG$", { noremap = true, silent = true, desc = "Select all" }) +-- Escape deletes highlights +vim.keymap.set("n", "", "nohlsearch") +-- Paste before/after linewise +local cmd = vim.fn.has('nvim-0.12') == 1 and 'iput' or 'put' +vim.keymap.set({ 'n', 'x' }, '[p', 'exe "' .. cmd .. '! " . v:register', { desc = 'Paste Above' }) +vim.keymap.set({ 'n', 'x' }, ']p', 'exe "' .. cmd .. ' " . v:register', { desc = 'Paste Below' }) + +vim.keymap.set({ "n", "v", "x" }, "p", '"+p', { noremap = true, silent = true, desc = "Paste from clipboard" }) +vim.keymap.set({ "n", "v", "x" }, "y", '"+y', { noremap = true, silent = true, desc = "Copy toclipboard" }) +-- Leader mappings ============================================================ +-- stylua: ignore start + +-- Create global tables with information about clue groups in certain modes +-- Structure of tables is taken to be compatible with 'mini.clue'. +_G.Config.leader_group_clues = { + { mode = 'n', keys = 'a', desc = '+AI' }, + { mode = 'n', keys = 'b', desc = '+Buffer' }, + { mode = 'n', keys = 'e', desc = '+Explore' }, + { mode = 'n', keys = 'f', desc = '+Find' }, + { mode = 'n', keys = 'fl', desc = '+LSP' }, + { mode = 'n', keys = 'fa', desc = '+Git' }, + { mode = 'n', keys = 'g', desc = '+Git' }, + { mode = 'n', keys = 'l', desc = '+LSP' }, + { mode = 'n', keys = 'L', desc = '+Lua/Log' }, + { mode = 'n', keys = 'o', desc = '+Other' }, + { mode = 'n', keys = 'r', desc = '+R' }, + { mode = 'n', keys = 't', desc = '+Terminal' }, + { mode = 'n', keys = 'u', desc = '+UI' }, + { mode = 'n', keys = 'v', desc = '+Visits' }, + { mode = 'n', keys = 'w', desc = '+Windows' }, + { mode = 'x', keys = 'l', desc = '+LSP' }, + { mode = 'x', keys = 'r', desc = '+R' }, + { mode = 'n', keys = 'z', desc = '+ZK' }, + { mode = 'n', keys = 'zr', desc = '+Reviews' }, + { mode = 'x', keys = 'a', desc = '+AI' }, +} + +-- Create `` mappings +local nmap_leader = function(suffix, rhs, desc, opts) + opts = opts or {} + opts.desc = desc + vim.keymap.set('n', '' .. suffix, rhs, opts) +end +local xmap_leader = function(suffix, rhs, desc, opts) + opts = opts or {} + opts.desc = desc + vim.keymap.set('x', '' .. suffix, rhs, opts) +end +-- Other mappings +local nmap_lsp = function(keys, func, desc) + if desc then + desc = desc .. "(LSP)" + end + + vim.keymap.set("n", keys, func, { desc = desc }) +end + +-- Switch buffers +nmap_leader('', 'bnext', 'Next buffer') +nmap_leader('', 'bprev', 'Prev buffer') + +-- a is for 'AI' +nmap_leader("ac", "CodeCompanionChat Toggle", "Chat Toggle") +nmap_leader("ae", "CodeCompanion /explain", "Explain Code") +nmap_leader("af", "CodeCompanion /fix", "Fix Code") +nmap_leader("ag", "CodeCompanion /commit", "Generate commit message") +nmap_leader("ai", "CodeCompanionActions", "Chat Action") +nmap_leader("al", "CodeCompanion /lsp", "Explain LSP Diagnostics") +nmap_leader("an", "CodeCompanionChat Add", "Chat New") +nmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") +nmap_leader("ax", "CodeCompanion /fixer", "Code Fixer") +nmap_leader("ax", "CodeCompanion /fixer", "Code Fixer") +xmap_leader("ae", "CodeCompanion /explain", "Explain Code") +xmap_leader("af", "CodeCompanion /fix", "Fix Code") +xmap_leader("ap", "CodeCompanion /expert", "Code Fixer") +xmap_leader("ap", "CodeCompanion /expert", "Code Fixer") +xmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") + +-- b is for 'buffer' +nmap_leader('bb', 'b#', 'Alternate') +nmap_leader('bd', 'lua MiniBufremove.delete()', 'Delete') +nmap_leader('bD', 'lua MiniBufremove.delete(0, true)', 'Delete!') +nmap_leader('bs', 'lua Config.new_scratch_buffer()', 'Scratch') +nmap_leader('bw', 'lua MiniBufremove.wipeout()', 'Wipeout') +nmap_leader('bW', 'lua MiniBufremove.wipeout(0, true)', 'Wipeout!') +nmap_leader('bq', 'qall', 'Quit all') + +-- e is for 'explore' and 'edit' +nmap_leader('ed', 'lua MiniFiles.open()', 'Directory') +nmap_leader('ef', 'lua Config.try_opendir()', 'File directory') +nmap_leader('es', 'lua MiniSessions.select()', 'Sessions') +nmap_leader('eq', 'lua Config.toggle_quickfix()', 'Quickfix') +nmap_leader('ez', 'lua MiniFiles.open(os.getenv("ZK_NOTEBOOK_DIR"))', 'Notes directory') + +-- f is for 'fuzzy find' +nmap_leader('f/', 'Pick history scope="/"', '"/" history') +nmap_leader('f:', 'Pick history scope=":"', '":" history') +nmap_leader('f,', 'Pick visit_labels', 'Visit labels') +nmap_leader('faa', 'Pick git_hunks scope="staged"', 'Added hunks (all)') +nmap_leader('faA', 'Pick git_hunks path="%" scope="staged"', 'Added hunks (current)') +nmap_leader('fb', 'Pick buffers', 'Buffers') +nmap_leader(',', 'Pick buffers', 'Buffers') +nmap_leader('fac', 'Pick git_commits', 'Commits (all)') +nmap_leader('faC', 'Pick git_commits path="%"', 'Commits (current)') +nmap_leader('fd', 'Pick diagnostic scope="all"', 'Diagnostic workspace') +nmap_leader('fD', 'Pick diagnostic scope="current"', 'Diagnostic buffer') +nmap_leader('ff', 'Pick files', 'Files') +nmap_leader('fg', 'Pick grep_live', 'Grep live') +nmap_leader('fG', 'Pick grep pattern=""', 'Grep current word') +nmap_leader('fh', 'Pick help', 'Help tags') +nmap_leader('fH', 'Pick hl_groups', 'Highlight groups') +nmap_leader('fj', 'Pick buf_lines scope="all"', 'Lines (all)') +nmap_leader('fJ', 'Pick buf_lines scope="current"', 'Lines (current)') +nmap_leader('fam', 'Pick git_hunks', 'Modified hunks (all)') +nmap_leader('faM', 'Pick git_hunks path="%"', 'Modified hunks (current)') +nmap_leader('fm', 'Pick marks', 'Marks') +nmap_leader('fn', 'ZkNotes', "Notes") +nmap_leader('fk', 'Pick keymaps', 'Keymaps') +nmap_leader('fR', 'Pick resume', 'Resume') +nmap_leader('fp', 'Pick projects', 'Projects') +nmap_leader('fq', 'Pick list scope="quickfix"', 'Quickfix') +nmap_leader('fr', 'Pick lsp scope="references"', 'References (LSP)') +nmap_leader('flr', 'Pick lsp scope="references"', 'References (LSP)') +nmap_leader('fS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') +nmap_leader('flS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') +nmap_leader('fs', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') +nmap_leader('fls', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') +nmap_leader('fld', 'Pick lsp scope="definition"', 'Definition (LSP)') +nmap_leader('flD', 'Pick lsp scope="declaration"', 'Declaration (LSP)') +nmap_leader('flt', 'Pick lsp scope="type_definition"', 'Type Definition (LSP)') +nmap_leader('fv', 'Pick visit_paths cwd=""', 'Visit paths (all)') +nmap_leader('fV', 'Pick visit_paths', 'Visit paths (cwd)') + +-- g is for git +local git_log_cmd = [[Git log --pretty=format:\%h\ \%as\ │\ \%s --topo-order]] + +nmap_leader('gc', 'Git commit', 'Commit') +nmap_leader('gC', 'Git commit --amend', 'Commit amend') +nmap_leader('gd', 'Git diff', 'Diff') +nmap_leader('gD', 'Git diff -- %', 'Diff buffer') +nmap_leader('gg', 'lua require("neogit").open()', 'Git tab') +nmap_leader('gl', '' .. git_log_cmd .. '', 'Log') +nmap_leader('gL', '' .. git_log_cmd .. ' --follow -- %', 'Log buffer') +nmap_leader('go', 'lua MiniDiff.toggle_overlay()', 'Toggle overlay') +nmap_leader('gp', 'Git pull', 'Pull') +nmap_leader('gP', 'Git push', 'Push') +nmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at cursor') + +xmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at selection') + +-- j/k navigate quickfix +nmap_leader("j", 'cnextzz', "Quickfix next") +nmap_leader("k", 'cprevzz', "Quickfix prev") + +-- l is for 'LSP' (Language Server Protocol) +vim.keymap.set({ 'n' }, 'grd', 'lua vim.lsp.buf.definition()', { desc = 'Definition' }) +vim.keymap.set({ 'n' }, 'grk', 'lua vim.lsp.buf.hover()', { desc = 'Documentation' }) +vim.keymap.set({ 'n' }, 'gre', 'lua vim.diagnostic.open_float()', { desc = 'Diagnostics' }) + +nmap_lsp("K", 'lua vim.lsp.buf.hover()', "Documentation") +local formatting_cmd = 'lua require("conform").format({ lsp_fallback = true })' +nmap_leader('la', 'lua vim.lsp.buf.code_action()', 'Actions') +nmap_leader('le', 'lua vim.diagnostic.open_float()', 'Diagnostics popup') +nmap_leader('lf', formatting_cmd, 'Format') +nmap_leader('lk', 'lua vim.lsp.buf.hover()', 'Documentation') +nmap_leader('li', 'lua vim.lsp.buf.implementation()', 'Information') +-- use ]d and [d +--nmap_leader('lj', 'lua vim.diagnostic.goto_next()', 'Next diagnostic') +--nmap_leader('lk', 'lua vim.diagnostic.goto_prev()', 'Prev diagnostic') +nmap_leader('lR', 'lua vim.lsp.buf.references()', 'References') +nmap_leader('lr', 'lua vim.lsp.buf.rename()', 'Rename') +nmap_leader('ls', 'lua vim.lsp.buf.definition()', 'Source definition') + +xmap_leader('lf', formatting_cmd, 'Format selection') + +-- L is for 'Lua' +nmap_leader('Lc', 'lua Config.log_clear()', 'Clear log') +nmap_leader('LL', 'luafile %echo "Sourced lua"', 'Source buffer') +nmap_leader('Ls', 'lua Config.log_print()', 'Show log') +nmap_leader('Lx', 'lua Config.execute_lua_line()', 'Execute `lua` line') + +-- m is free + +-- o is for 'other' +local trailspace_toggle_command = 'lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable' +nmap_leader('od', 'Neogen', 'Document') +nmap_leader('oh', 'normal gxiagxila', 'Move arg left') +nmap_leader('ol', 'normal gxiagxina', 'Move arg right') +nmap_leader('or', 'lua MiniMisc.resize_window()', 'Resize to default width') +nmap_leader('oS', 'lua Config.insert_section()', 'Section insert') +nmap_leader('ot', 'lua MiniTrailspace.trim()', 'Trim trailspace') +nmap_leader('oT', trailspace_toggle_command, 'Trailspace hl toggle') +nmap_leader('oz', 'lua MiniMisc.zoom()', 'Zoom toggle') +nmap_leader('ow', + "lua MiniSessions.write(vim.fn.input('Session name: ', string.match(vim.fn.getcwd(), \"[^/]+$\") .. '-session.vim'))", + 'Write session') + +-- r is for 'R' +nmap_leader('rc', 'RSend devtools::check()', 'Check') +nmap_leader('rC', 'RSend devtools::test_coverage()', 'Coverage') +nmap_leader('rd', 'RSend devtools::document()', 'Document') +nmap_leader('ri', 'RSend devtools::install(keep_source=TRUE)', 'Install') +nmap_leader('rk', 'RSend quarto::quarto_preview("%")', 'Knit file') +nmap_leader('rl', 'RSend devtools::load_all()', 'Load all') +nmap_leader('rL', 'RSend devtools::load_all(recompile=TRUE)', 'Load all recompile') +nmap_leader('rm', 'RSend Rcpp::compileAttributes()', 'Run examples') +nmap_leader('rT', 'RSend testthat::test_file("%")', 'Test file') +nmap_leader('rt', 'RSend devtools::test()', 'Test') + +-- - Copy to clipboard and make reprex (which itself is loaded to clipboard) +xmap_leader('rx', '"+y :RSend reprex::reprex()', 'Reprex selection') + +-- s is for 'send' (Send text to neoterm buffer) +nmap_leader('s', 'SlimeSendCurrentLinej', 'Send to terminal') + +-- - In simple visual mode send text and move to the last character in +-- selection and move to the right. Otherwise (like in line or block visual +-- mode) send text and move one line down from bottom of selection. +xmap_leader('s', 'SlimeRegionSend', 'Send to terminal') + +-- t is for 'terminal' +vim.keymap.set("t", "", [[]], { desc = "Exit terminal mode" }) +vim.keymap.set("n", "tc", 'lua Config.terminal.open_clickhouse_client()', + { desc = "Open Clickhouse client" }) +vim.keymap.set("n", "tl", 'lua Config.terminal.open_clickhouse_local()', + { desc = "Open Clickhouse local" }) +vim.keymap.set("n", "tp", 'lua Config.terminal.open_python()', { desc = "Open Python" }) +vim.keymap.set("n", "tj", 'lua Config.terminal.open_julia()', { desc = "Open Julia" }) +vim.keymap.set("n", "td", 'lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()', + { desc = "Open DuckDB" }) +vim.keymap.set("n", "tx", 'lua Config.terminal.open_in_terminal()', { desc = "Terminal Command" }) +vim.keymap.set("n", "tt", 'lua Config.terminal.open_shell()', { desc = "Terminal" }) +nmap_leader("tb", 'lua Config.terminal.toggle_bracket()', "Toggle bracketed paste") +nmap_leader("up", 'lua Config.terminal.toggle_bracket()', "Toggle bracketed paste") + +-- u is for UI +nmap_leader('ut', 'TSContext toggle', 'Toggle TScontext') +nmap_leader('ua', 'Copilot toggle', 'Toggle AI completion') + +-- v is for 'visits' +nmap_leader('vv', 'lua MiniVisits.add_label("core")', 'Add "core" label') +nmap_leader('vV', 'lua MiniVisits.remove_label("core")', 'Remove "core" label') +nmap_leader('vl', 'lua MiniVisits.add_label()', 'Add label') +nmap_leader('vL', 'lua MiniVisits.remove_label()', 'Remove label') + +local map_pick_core = function(keys, cwd, desc) + local rhs = function() + local sort_latest = MiniVisits.gen_sort.default({ recency_weight = 1 }) + MiniExtra.pickers.visit_paths({ + cwd = cwd, + filter = 'core', + sort = sort_latest + }, { source = { name = desc } }) + end + nmap_leader(keys, rhs, desc) +end +map_pick_core('vc', '', 'Core visits (all)') +map_pick_core('vC', nil, 'Core visits (cwd)') + +-- w is for 'windows' +nmap_leader("wh", "h", "Go to Left Window", { remap = true }) +nmap_leader("wj", "j", "Go to Lower Window", { remap = true }) +nmap_leader("wk", "k", "Go to Upper Window", { remap = true }) +nmap_leader("wl", "l", "Go to Right Window", { remap = true }) + +nmap_leader("_", "s", "Split Window Below", { remap = true }) +nmap_leader("|", "v", "Split Window Right", { remap = true }) +nmap_leader("wd", "c", "Delete Window", { remap = true }) +nmap_leader("wo", "o", "Delete Other Windows", { remap = true }) + +-- z is for 'ZettelKasten' +nmap_leader("zo", 'ZkNotes', "Notes") +nmap_leader("zt", 'ZkTags', "Tags") + +nmap_leader( + "zrd", + 'ZkNew { group = "dreviews" }', + "Daily Review" +) +nmap_leader( + "zrw", + 'ZkNew { group = "wreviews" }', + "Weekly Review" +) +nmap_leader( + "zn", + 'ZkNew { group = "inbox", title = vim.fn.input("Title: ") }', + "New" +) +nmap_leader( + "zp", + "ZkNew { group = 'permanent', title = vim.fn.input('Title: ') }", + "Permanent" +) + +nmap_leader( + "zl", + "ZkNew { group = 'literature', title = vim.fn.input('Title: '), extra.author = vim.fn.input('Author: '), extra.year = vim.fn.input('Year: ') }", + "Literature" +) + +nmap_leader( + "zd", + "ZkNew { group = 'dashboard', title = vim.fn.input('Title: ') }", + "Dashboard" +) +nmap_leader( + "zP", + "ZkNew { group = 'project', title = vim.fn.input('Title: ')}", + "Project" +) +-- stylua: ignore end diff --git a/plugin/20_startup.lua b/plugin/20_startup.lua new file mode 100644 index 0000000..dfae59e --- /dev/null +++ b/plugin/20_startup.lua @@ -0,0 +1,314 @@ +local now = MiniDeps.now +local later = MiniDeps.later +local now_if_args = Config.now_if_args +local nix = require('config.nix') + +if not Config.isNixCats then + local add = MiniDeps.add + now_if_args(function() + add({ + source = "nvim-treesitter/nvim-treesitter", + checkout = "master", + monitor = "main", + hooks = { + post_checkout = function() + vim.cmd("TSUpdate") + end, + }, + }) + add({ + source = "nvim-treesitter/nvim-treesitter-textobjects", + checkout = "main", + }) + add({ source = "zk-org/zk-nvim" }) + end) +end + +-- Mini.nvim +now(function() + local colorschemeName = nix.get_setting("onedark_dark", "colorscheme") + if colorschemeName == 'light' then + local palette = require('mini.hues').make_palette({ + background = '#fefcf5', + foreground = '#657b83', + accent = 'bg', + saturation = 'high', + n_hues = 8 + }) + palette.fg_mid2 = "#586e75" + palette.fg_mid = "#073642" + palette.bg_edge = "#fdf6e3" + palette.accent_bg = "#eee8d5" + require('mini.hues').apply_palette(palette) + else + if colorschemeName == "cyberdream" and vim.o.background == 'light' then + colorschemeName = colorschemeName .. '-light' + end + vim.cmd.colorscheme(colorschemeName) + end +end) + +now(function() + require("mini.basics").setup({ + options = { + basic = true, + extra_ui = true + }, + mappings = { + -- jk linewise, gy/gp system clipboard, gV select last change/yank + basic = true, + -- move between windows, resize + windows = true, + move_with_alt = true, + option_toggle_prefix = "u" + }, + autocommands = { + basic = true, + relnum_in_visual_mode = true + }, + }) +end) + +now(function() + require("mini.icons").setup({ + use_file_extension = function(ext, _) + local suf3, suf4 = ext:sub(-3), ext:sub(-4) + return suf3 ~= "scm" and suf3 ~= "txt" and suf3 ~= "yml" and suf4 ~= "json" and suf4 ~= "yaml" + end, + }) + later(MiniIcons.mock_nvim_web_devicons) + later(MiniIcons.tweak_lsp_kind) +end) + +now(function() + local predicate = function(notif) + if not (notif.data.source == "lsp_progress" and notif.data.client_name == "lua_ls") then + return true + end + -- Filter out some LSP progress notifications from 'lua_ls' + return notif.msg:find("Diagnosing") == nil and notif.msg:find("semantic tokens") == nil + end + local custom_sort = function(notif_arr) + return MiniNotify.default_sort(vim.tbl_filter(predicate, notif_arr)) + end + + require("mini.notify").setup({ content = { sort = custom_sort } }) + vim.notify = MiniNotify.make_notify() +end) + +now(function() + require("mini.sessions").setup() +end) + +now(function() + local starter = require("mini.starter") + starter.setup({ + evaluate_single = true, + items = { + starter.sections.recent_files(5, true), + function() + local section = Config.startup.get_recent_files_by_ft_or_ext({ + "r", + "sql", + "julia", + "python", + "lua", + }) + return section + end, + starter.sections.pick(), + starter.sections.sessions(5, true), + starter.sections.builtin_actions(), + starter.sections.recent_files(3, false), + }, + footer = Config.startup.footer_text, + content_hooks = { + starter.gen_hook.adding_bullet(), + starter.gen_hook.indexing( + "all", + { "Builtin actions", "Recent files (current directory)", "Recent files", } + ), + starter.gen_hook.aligning("center", "center"), + starter.gen_hook.padding(3, 2), + }, + }) +end) + +now(function() + require("mini.statusline").setup() +end) + +now(function() + require("mini.tabline").setup() +end) + +now(function() + local miniclue = require("mini.clue") + --stylua: ignore + miniclue.setup({ + window = { + config = { + width = 'auto' + }, + delay = 100, + }, + clues = { + Config.leader_group_clues, + miniclue.gen_clues.builtin_completion(), + miniclue.gen_clues.g(), + miniclue.gen_clues.marks(), + miniclue.gen_clues.registers(), + miniclue.gen_clues.windows({ submode_resize = true, submode_move = true }), + miniclue.gen_clues.z(), + }, + triggers = { + { mode = 'n', keys = '' }, -- Leader triggers + { mode = 'n', keys = '' }, -- LocalLeader triggers + { mode = 'x', keys = '' }, + { mode = 'x', keys = '' }, + { mode = 'n', keys = [[\]] }, -- mini.basics + { mode = 'n', keys = '[' }, -- mini.bracketed + { mode = 'n', keys = ']' }, + { mode = 'x', keys = '[' }, + { mode = 'x', keys = ']' }, + { mode = 'i', keys = '' }, -- Built-in completion + { mode = 'n', keys = 'g' }, -- `g` key + { mode = 'x', keys = 'g' }, + { mode = 'n', keys = '`' }, + { mode = 'x', keys = '`' }, + { mode = 'n', keys = '"' }, -- Registers + { mode = 'x', keys = '"' }, + { mode = 'i', keys = '' }, + { mode = 'c', keys = '' }, + { mode = 'n', keys = '' }, -- Window commands + { mode = 'n', keys = 'z' }, -- `z` key + { mode = 'x', keys = 'z' }, + }, + }) +end) + +-- Treesitter + +now_if_args(function() + vim.treesitter.language.register("markdown", { "markdown", "codecompanion" }) + + -- Base configuration + local opts = { + highlight = { enable = true }, + indent = { enable = false }, + textobjects = { + move = { + enable = true, + set_jumps = true, -- whether to set jumps in the jumplist + goto_next_start = { + ["]a"] = "@paramter.inner", + ["]f"] = "@function.outer", + ["]o"] = "@loop.*", + ["]s"] = { query = "@local.scope", desc = "Next scope" }, + ["]z"] = { query = "@fold", desc = "Next fold" }, + }, + goto_next_end = { + ["]M"] = "@function.outer", + ["]["] = "@class.outer", + }, + goto_previous_start = { + ["[a"] = "@parameter.inner", + ["[f"] = "@function.outer", + ["[o"] = "@loop.*", + ["[s"] = { query = "@local.scope", query_group = "locals", desc = "Prev. scope" }, + ["[z"] = { query = "@fold", query_group = "folds", desc = "Prev. fold" }, + }, + goto_previous_end = { + ["[M"] = "@function.outer", + ["[]"] = "@class.outer", + }, + goto_next = { + ["]e"] = "@conditional.outer", + }, + goto_previous = { + ["[e"] = "@conditional.outer", + } + }, + swap = { + enable = true, + swap_next = { + ["x"] = "@parameter.inner", + }, + swap_previous = { + ["X"] = "@parameter.inner", + }, + }, + lsp_interop = { + enable = true, + border = 'none', + floating_preview_opts = {}, + peek_definition_code = { + ["lm"] = "@function.outer", + ["lM"] = "@class.outer", + }, + }, + }, + } + + -- Environment-specific Overrides + if not Config.isNixCats then + opts.auto_install = true + opts.ensure_installed = Config.treesitter_helpers.default_parsers + else + opts.auto_install = false + -- Nix handles installation, so ensure_installed is skipped/empty + end + + -- Manual parser check for non-Nix users + if not Config.isNixCats then + local installed_check = function(lang) + return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0 + end + local to_install = vim.tbl_filter(installed_check, opts.ensure_installed) + if #to_install > 0 then + require("nvim-treesitter").install(to_install) + end + end + + + local configs = require("nvim-treesitter.configs") + configs.setup(opts) + require 'treesitter-context'.setup { + enable = true, + multiwindow = false, -- Enable multiwindow support. + max_lines = 30, -- How many lines the window should span. Values <= 0 mean no limit. + min_window_height = 70, -- Minimum editor window height to enable context. Values <= 0 mean no limit. + line_numbers = true, + multiline_threshold = 10, -- Maximum number of lines to show for a single context + trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer' + mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline' + -- Separator between context and content. Should be a single character string, like '-'. + -- When separator is set, the context will only show up when there are at least 2 lines above cursorline. + separator = '-', + zindex = 20, -- The Z-index of the context window + on_attach = nil, -- (fun(buf: integer): boolean) return false to disable attaching + } +end) + +-- zk +now_if_args(function() + require("zk").setup({ + picker = "minipick", + lsp = { + -- `config` is passed to `vim.lsp.start_client(config)` + config = { + cmd = { "zk", "lsp" }, + name = "zk", + -- on_attach = ... + -- etc, see `:h vim.lsp.start_client()` + }, + + -- automatically attach buffers in a zk notebook that match the given filetypes + auto_attach = { + enabled = true, + filetypes = { "markdown" }, + }, + + }, + }) +end) diff --git a/plugin/21_datascience.lua b/plugin/21_datascience.lua new file mode 100644 index 0000000..2c0786d --- /dev/null +++ b/plugin/21_datascience.lua @@ -0,0 +1,104 @@ +local now = MiniDeps.now +local now_if_args = Config.now_if_args +local later = MiniDeps.later +local add = Config.add +local nix = require('config.nix') + +if not Config.isNixCats then + local m_add = MiniDeps.add + + now(function() + m_add({ source = "R-nvim/R.nvim" }) + end) + + now_if_args(function() + m_add({ source = "jmbuhr/otter.nvim" }) + end) + + later(function() + m_add({ source = "jpalardy/vim-slime" }) + end) +end + +-- terminal +later(function() + vim.g.slime_target = "neovim" + vim.g.slime_no_mappings = true + add("vim-slime") + vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%" + vim.g.slime_bracketed_paste = Config.opt_bracket + vim.g.slime_input_pid = false + vim.g.slime_suggest_default = true + vim.g.slime_menu_config = false + vim.g.slime_neovim_ignore_unlisted = false + + -- Define standard slime mappings + vim.keymap.set("v", "", "SlimeRegionSend", { noremap = true }) + vim.keymap.set("v", "", "SlimeRegionSend", { noremap = true }) + vim.keymap.set("n", "", "SlimeLineSend", { noremap = true }) + -- Standardize on C-c C-c as well (common convention) + vim.keymap.set("v", "", "SlimeRegionSend", { noremap = true }) + vim.keymap.set("n", "", "SlimeParagraphSend", { noremap = true }) +end) + +-- r +now(function() + if nix.get_cat("r", false) then + vim.g.rout_follow_colorscheme = true + require("r").setup({ + -- Create a table with the options to be passed to setup() + R_args = { "--quiet", "--no-save" }, + auto_start = "no", + objbr_auto_start = false, + objbr_place = 'console,below', + rconsole_width = 120, + min_editor_width = 80, + rconsole_height = 20, + nvimpager = "split_h", + pdfviewer = "zathura", + }) + end +end) + + +-- Quarto +now(function() + vim.treesitter.language.register("markdown", { "quarto", "rmd" }) + + vim.api.nvim_create_autocmd("FileType", { + pattern = { "quarto" }, + callback = function() + require("otter").activate() + end, + }) + + require("otter").setup({ + lsp = { + diagnostic_update_events = { "BufWritePost", "InsertLeave" }, + }, + buffers = { + set_filetype = true, + write_to_disk = true, + }, + }) +end) + +later(function() + require("quarto").setup({ + lspFeatures = { + enabled = true, + languages = { "r", "python", "julia" }, + diagnostics = { + enabled = true, + triggers = { "BufWrite" }, + }, + completion = { + enabled = true, + }, + }, + codeRunner = { + enabled = true, + default_method = "slime", + }, + }) +end) diff --git a/plugin/22_languages.lua b/plugin/22_languages.lua new file mode 100644 index 0000000..9c0ef35 --- /dev/null +++ b/plugin/22_languages.lua @@ -0,0 +1,53 @@ +local add = Config.add +local now_if_args = Config.now_if_args +local later = MiniDeps.later + +if not Config.isNixCats then + local m_add = MiniDeps.add + later(function() + m_add({ source = "Bilal2453/luvit-meta" }) + m_add({ source = "folke/lazydev.nvim" }) + end) +end + +-- lua +later(function() + add("luvit-meta") + add("lazydev") + require("lazydev").setup({ + library = { + -- See the configuration section for more details + -- Load luvit types when the `vim.uv` word is found + "lua", + "mini.nvim", + "MiniDeps", + { path = "luvit-meta/library", words = { "vim%.uv" } }, + { path = "${3rd}/luv/library", words = { "vim%.uv" } }, + }, + }) +end) + +-- Markdown +now_if_args(function() + add("render-markdown.nvim") + require('render-markdown').setup({ + -- completions = { blink = { enabled = true } }, + file_types = { 'markdown', 'quarto', 'rmd', 'codecompanion', }, + link = { + wiki = { + body = function(ctx) + local diagnostics = vim.diagnostic.get(ctx.buf, { + lnum = ctx.row, + severity = vim.diagnostic.severity.HINT, + }) + for _, diagnostic in ipairs(diagnostics) do + if diagnostic.source == 'marksman' then + return diagnostic.message + end + end + return nil + end, + }, + }, + }) +end) diff --git a/plugin/23_editor.lua b/plugin/23_editor.lua new file mode 100644 index 0000000..eadff4f --- /dev/null +++ b/plugin/23_editor.lua @@ -0,0 +1,348 @@ +local later = MiniDeps.later +local add = Config.add + +if not Config.isNixCats then + local m_add = MiniDeps.add + + later(function() + m_add("stevearc/conform.nvim") + end) +end + +-- Formatting +later(function() + add("conform.nvim") + require("conform").setup({ + -- Map of filetype to formatters + formatters_by_ft = { + javascript = { "prettier" }, + json = { "prettier" }, + python = { "black" }, + nix = { "alejandra" }, + -- r = { "my_styler" }, + rmd = { "injected" }, + quarto = { "injected" }, + }, + + lsp_format = "fallback", + + formatters = { + my_styler = { + command = "R", + -- A list of strings, or a function that returns a list of strings + -- Return a single string instead of a list to run the command in a shell + args = { "-s", "-e", "styler::style_file(commandArgs(TRUE)[1])", "--args", "$FILENAME" }, + stdin = false, + }, + }, + }) +end) + +-- Edit +later(function() + local ai = require("mini.ai") + local spec_treesitter = ai.gen_spec.treesitter + ai.setup({ + search_method = "cover", + n_lines = 1000, + }) +end) + +later(function() + require("mini.align").setup() +end) + +later(function() + require("mini.animate").setup({ scroll = { enable = false } }) +end) + +later(function() + require("mini.bracketed").setup({ + diagnostic = { + options = { + float = false, + }, + }, + }) +end) + +later(function() + require("mini.bufremove").setup() +end) + +later(function() + require("mini.comment").setup() +end) + +later(function() + require("mini.cursorword").setup({ delay = 1000 }) +end) + +later(function() + require("mini.diff").setup({ + view = { + style = "sign", + }, + mappings = { + apply = "ga", + reset = "gr", + textobject = "o", + }, + options = { + linematch = 1000, + algorithm = 'myers', + }, + }) +end) + +later(function() + require("mini.files").setup({ + windows = { + preview = true, + width_focus = 80, + width_preview = 90, + }, + mappings = { + mark_goto = "'", + synchronize = ':', + } + }) + local minifiles_augroup = vim.api.nvim_create_augroup("ec-mini-files", {}) + vim.api.nvim_create_autocmd("User", { + group = minifiles_augroup, + pattern = "MiniFilesExplorerOpen", + callback = function() + MiniFiles.set_bookmark("h", os.getenv("HOME") or vim.env.HOME, { desc = "Home" }) + MiniFiles.set_bookmark("c", vim.fn.stdpath("config"), { desc = "Config" }) + MiniFiles.set_bookmark("w", vim.fn.getcwd, { desc = "Working directory" }) + MiniFiles.set_bookmark("z", os.getenv("ZK_NOTEBOOK_DIR") or vim.env.HOME, { desc = "ZK" }) + end, + }) + + -- Set focused directory as current working directory + local function remove_string(string1, string2) + return string2:gsub(string1, "", 1) + end + local set_cwd = function() + local path = (MiniFiles.get_fs_entry() or {}).path + if path == nil then + return vim.notify("Cursor is not on valid entry") + end + + local pwd = vim.fs.dirname(path) + vim.notify("PWD: " .. '.' .. vim.fn.pathshorten(pwd, 6)) + vim.fn.chdir(pwd) + end + + -- Yank in register full path of entry under cursor + local yank_path = function() + local path = (MiniFiles.get_fs_entry() or {}).path + if path == nil then + return vim.notify("Cursor is not on valid entry") + end + vim.notify("Yanked: " .. path) + vim.fn.setreg(vim.v.register, path) + end + + -- Yank in register relative path of entry under cursor + local yank_relpath = function() + local path = (MiniFiles.get_fs_entry() or {}).path + local cwd = vim.fn.getcwd() .. '/' + local relpath = remove_string(cwd, path) + if path == nil then + return vim.notify("Cursor is not on valid entry") + end + vim.notify("Yanked: " .. relpath) + vim.fn.setreg(vim.v.register, relpath) + end + + local ui_open = function() + vim.ui.open(MiniFiles.get_fs_entry().path) + end + vim.api.nvim_create_autocmd("User", { + pattern = "MiniFilesBufferCreate", + callback = function(args) + local b = args.data.buf_id + vim.keymap.set("n", "g~", set_cwd, { buffer = b, desc = "Set cwd" }) + vim.keymap.set("n", "gX", ui_open, { buffer = b, desc = "Open UI" }) + vim.keymap.set("n", "gY", yank_path, { buffer = b, desc = "Yank path" }) + vim.keymap.set("n", "gy", yank_relpath, { buffer = b, desc = "Yank relpath" }) + end, + }) +end) + +later(function() + require("mini.git").setup() +end) + +later(function() + require("mini.extra").setup() +end) + +later(function() + local hipatterns = require("mini.hipatterns") + local hi_words = MiniExtra.gen_highlighter.words + hipatterns.setup({ + highlighters = { + fixme = hi_words({ "FIXME", "Fixme", "fixme" }, "MiniHipatternsFixme"), + hack = hi_words({ "HACK", "Hack", "hack" }, "MiniHipatternsHack"), + todo = hi_words({ "TODO", "Todo", "todo" }, "MiniHipatternsTodo"), + note = hi_words({ "NOTE", "Note", "note" }, "MiniHipatternsNote"), + + hex_color = hipatterns.gen_highlighter.hex_color(), + }, + }) +end) + +later(function() + require("mini.indentscope").setup() +end) + +later(function() + require("mini.jump").setup() +end) + +later(function() + local jump2d = require("mini.jump2d") + jump2d.setup({ + spotter = jump2d.gen_spotter.pattern("[^%s%p]+"), + allowed_lines = { + blank = false, + cursor_at = false + }, + labels = "asdfghjklweruiopzxcnm,;", + view = { dim = true, n_steps_ahead = 2 }, + mappings = { + start_jumping = "sj", + }, + }) + vim.keymap.set({ "n", "x", "o" }, "", function() + MiniJump2d.start(MiniJump2d.builtin_opts.single_character) + end) +end) + +later(function() + local minikeymap = require("mini.keymap") + minikeymap.setup() + local map_multistep = minikeymap.map_multistep + local tab_steps = { + "blink_next", + "pmenu_next", + "increase_indent", + "jump_after_close", + } + map_multistep("i", "", tab_steps) + local shifttab_steps = { + "blink_prev", + "pmenu_prev", + "decrease_indent", + "jump_before_open", + } + map_multistep("i", "", shifttab_steps) + map_multistep("i", "", { + "blink_accept", + "pmenu_accept", + "minipairs_cr", + }) + map_multistep("i", "", { "hungry_bs", "minipairs_bs" }) + + local tab_steps_n = { + "minisnippets_next", + "jump_after_tsnode", + "jump_after_close", + } + map_multistep("n", "", tab_steps_n) + + local shifttab_steps_n = { + "minisnippets_prev", + "jump_before_tsnode", + "jump_before_open", + } + map_multistep("n", "", shifttab_steps_n) +end) + +later(function() + require("mini.move").setup({ options = { reindent_linewise = false } }) +end) + +later(function() + require("mini.operators").setup({ + replace = { + prefix = "gl" + }, + }) +end) + +later(function() + require("mini.pairs").setup({ + mappings = { + ['"'] = { neigh_pattern = '[^%a\\"].' }, + ["'"] = { neigh_pattern = "[^%a\\'#]." }, + }, + modes = { + insert = true, + command = true, + terminal = true + } + }) +end) + +later(function() + require("mini.misc").setup({ make_global = { "put", "put_text", "stat_summary", "bench_time" } }) + -- MiniMisc.setup_auto_root() + MiniMisc.setup_termbg_sync() + MiniMisc.setup_restore_cursor() +end) + +later(function() + local choose_all = function() + local mappings = MiniPick.get_picker_opts().mappings + vim.api.nvim_input(mappings.mark_all .. mappings.choose_marked) + end + require("mini.pick").setup({ + mappings = { + choose_marked = '', + choose_all = { char = '', func = choose_all }, + } + }) + + vim.ui.select = MiniPick.ui_select + -- vim.api.nvim_set_hl(0, "MiniPickMatchCurrent", { bg = "#fe640b", bold = true }) +end) + +later(function() + local snippets = require("mini.snippets") + local gen_loader = snippets.gen_loader + local lang_patterns = { + markdown_inline = { "quarto.json" }, + } + snippets.setup({ + snippets = { + -- Load custom file with global snippets first (adjust for Windows) + gen_loader.from_file(vim.fn.stdpath('config') .. "/snippets/global.json"), + + -- Load snippets based on current language by reading files from + -- "snippets/" subdirectories from 'runtimepath' directories. + gen_loader.from_lang({ lang_patterns = lang_patterns }), + }, + -- expand = { match = match_strict }, + }) +end) + +later(function() + require("mini.splitjoin").setup() +end) + +later(function() + require("mini.surround").setup() + -- Disable `s` shortcut (use `cl` instead) for safer usage of 'mini.surround' + vim.keymap.set({ "n", "x" }, "s", "") +end) + +later(function() + require("mini.trailspace").setup() +end) + +later(function() + require("mini.visits").setup() +end) diff --git a/plugin/24_completion.lua b/plugin/24_completion.lua new file mode 100644 index 0000000..2b21185 --- /dev/null +++ b/plugin/24_completion.lua @@ -0,0 +1,351 @@ +local add = Config.add +local later = MiniDeps.later +local now_if_args = Config.now_if_args + +-- Constants +local BLINK_VERSION = "v1.4.1" + +-- Plugin sources configuration +local PLUGIN_SOURCES = { + "hrsh7th/cmp-cmdline", + "xzbdmw/colorful-menu.nvim", + "zbirenbaum/copilot.lua", + "jmbuhr/cmp-pandoc-references", + "fang2hou/blink-copilot", + "olimorris/codecompanion.nvim" +} + +local PLUGIN_ADDS = { + "cmp-cmdline", + "blink.compat", + "colorful-menu.nvim", + "cmp-pandoc-references", +} + +-- Helper functions +local function create_system_prompt(role_description) + return function(context) + return "I want you to act as a senior " .. context.filetype .. " developer. " .. role_description + end +end + +local function get_code_block(context) + local text = require("codecompanion.helpers.actions").get_code(context.start_line, context.end_line) + return "```" .. context.filetype .. "\n" .. text .. "\n```" +end + +local function create_common_opts(mapping, short_name) + return { + mapping = mapping, + modes = { "v" }, + short_name = short_name, + auto_submit = true, + stop_context_insertion = true, + user_prompt = true, + } +end + +local function get_mini_icons_highlight(ctx) + local _, hl, _ = require("mini.icons").get("lsp", ctx.kind) + return hl +end + +local function get_blink_fuzzy_setting() + local setting = { + sorts = { "exact", "score", "sort_text" } + } + + if not Config.isNixCats then + setting.prebuilt_binary = { force_version = BLINK_VERSION } + end + + return setting +end + +-- Plugin loading +if not Config.isNixCats then + local m_add = MiniDeps.add + + now_if_args(function() + m_add({ + source = "saghen/blink.cmp", + depends = { "rafamadriz/friendly-snippets" }, + checkout = BLINK_VERSION, + }) + end) + + later(function() + for _, source in ipairs(PLUGIN_SOURCES) do + m_add({ source = source }) + end + end) +end + +local function get_codecompanion_config() + return { + adapters = { + http = { + copilot = function() + return require("codecompanion.adapters").extend("copilot", { + schema = { + model = { default = "gemini-3-pro-preview" } + } + }) + end, + } + }, + display = { + chat = { + show_settings = false, + window = { + layout = "horizontal", + position = "bottom", + height = 0.33, + }, + }, + }, + prompt_library = { + ["Code Expert"] = { + strategy = "chat", + description = "Get expert advice from an LLM", + opts = create_common_opts("ae", "expert"), + prompts = { + { + role = "system", + content = create_system_prompt( + "I will ask you specific questions and I want you to return concise explanations and codeblock examples." + ), + }, + { + role = "user", + content = function(context) + return "I have the following code:\n\n" .. get_code_block(context) .. "\n\n" + end, + opts = { contains_code = true }, + }, + }, + }, + ["Code Fixer"] = { + strategy = "chat", + description = "Fix code errors with expert guidance", + opts = create_common_opts("af", "afixer"), + prompts = { + { + role = "system", + content = create_system_prompt( + "I have a block of code that is not working and will give you a hint about the error. I want you to return the corrected code and a concise explanation of the corrections." + ), + }, + { + role = "user", + content = function(context) + return "The following code has an error:\n\n" .. get_code_block(context) .. "\n\nThe error is:" + end, + opts = { contains_code = true }, + }, + }, + }, + ["Suggest"] = { + strategy = "chat", + description = "Suggest improvements to the buffer", + opts = { + mapping = "as", + modes = { "v" }, + short_name = "suggest", + auto_submit = true, + user_prompt = false, + stop_context_insertion = false, + }, + prompts = { + { + role = "system", + content = create_system_prompt( + "When asked to improve code, follow these steps:\n" .. + "1. Identify the programming language.\n" .. + "2. Think separately for each function or significant block of code and think about possible improvements (e.g., for better readability or speed) in the context of the language.\n" .. + "3. Think about the whole document and think about possible improvements.\n" .. + "4. Provide the improved code.\n" .. + "5. Provide a concise explanation of the improvements." + ), + }, + { + role = "user", + content = function(context) + return "Please improve the following code:\n\n" .. get_code_block(context) + end, + opts = { contains_code = true }, + }, + }, + }, + } + } +end + +-- Batch add simple plugins +later(function() + for _, plugin in ipairs(PLUGIN_ADDS) do + add(plugin) + end +end) + +-- Configure plugins with setup +later(function() + add("copilot.lua") + require("copilot").setup({ + suggestion = { enabled = false }, + panel = { enabled = false }, + filetypes = { + help = true, + julia = true, + lua = true, + markdown = true, + nix = true, + python = true, + r = true, + sh = function() + if string.match(vim.fs.basename(vim.api.nvim_buf_get_name(0)), '^%.env.*') then + -- disable for .env files + return false + end + return true + end, + ["."] = false + }, + server_opts_overrides = { + settings = { + telemetry = { telemetryLevel = 'off' } + } + }, + should_attach = function(_, bufname) + if string.match(bufname, "env") then + return false + end + return true + end + }) +end) + +later(function() + add("blink-copilot") + require("blink-copilot").setup({ + max_completions = 1, + }) +end) + + +later(function() + add("codecompanion.nvim") + + -- now use function + require("codecompanion").setup(get_codecompanion_config()) + vim.cmd([[cab cc CodeCompanion]]) +end) + +now_if_args(function() + add("blink.cmp") + + require("blink.cmp").setup({ + keymap = { + preset = "default", + [""] = { "show", "select_next" }, + [""] = { "accept" }, + }, + cmdline = { + enabled = true, + keymap = { + preset = "inherit", + [""] = { "show", "select_next" }, + [""] = { "show", "select_prev" }, + [""] = { "accept" }, + }, + completion = { + menu = { auto_show = true }, + list = { + selection = { preselect = false, auto_insert = true } + }, + }, + sources = function() + local cmd_type = vim.fn.getcmdtype() + if cmd_type == "/" or cmd_type == "?" then + return { "buffer" } + elseif cmd_type == ":" or cmd_type == "@" then + return { "cmdline", "cmp_cmdline" } + end + return {} + end, + }, + fuzzy = get_blink_fuzzy_setting(), + signature = { + enabled = true, + window = { show_documentation = true } + }, + completion = { + menu = { + draw = { + treesitter = { "lsp" }, + components = { + label = { + text = function(ctx) + return require("colorful-menu").blink_components_text(ctx) + end, + highlight = function(ctx) + return require("colorful-menu").blink_components_highlight(ctx) + end, + }, + kind_icon = { highlight = get_mini_icons_highlight }, + kind = { highlight = get_mini_icons_highlight }, + }, + }, + }, + list = { + selection = { preselect = false, auto_insert = true } + }, + documentation = { auto_show = true }, + trigger = { show_in_snippet = false }, + }, + snippets = { preset = "mini_snippets" }, + sources = { + default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot", "codecompanion" }, + providers = { + path = { + score_offset = 50, + opts = { + get_cwd = function(_) + return vim.fn.getcwd() + end, + }, + }, + lsp = { score_offset = 40 }, + snippets = { score_offset = 0 }, + cmp_cmdline = { + name = "cmp_cmdline", + module = "blink.compat.source", + enabled = false, + score_offset = 10, + opts = { cmp_name = "cmdline" } + }, + cmp_r = { + name = "cmp_r", + module = "blink.compat.source", + }, + copilot = { + name = "copilot", + module = "blink-copilot", + score_offset = 45, + async = true, + }, + codecompanion = { + name = "CodeCompanion", + module = "codecompanion.providers.completion.blink", + score_offset = 45, + async = true, + }, + references = { + name = "pandoc_references", + module = "cmp-pandoc-references.blink", + score_offset = 50, + }, + }, + }, + }) +end) diff --git a/plugin/25_lsp.lua b/plugin/25_lsp.lua new file mode 100644 index 0000000..d943ba1 --- /dev/null +++ b/plugin/25_lsp.lua @@ -0,0 +1,89 @@ +local now_if_args = Config.now_if_args + +if not Config.isNixCats then + local m_add = MiniDeps.add + now_if_args(function() + m_add("neovim/nvim-lspconfig") + end) +end + +now_if_args(function() + local servers = { + clangd = {}, + basedpyright = {}, + ruff = {}, + marksman = { + filetypes = { "markdown", "markdown_inline", "codecompanion" }, + }, + r_language_server = { + filetypes = { 'r', 'rmd', 'rmarkdown' }, + settings = { + ['r_language_server'] = { + lsp = { + rich_documentation = true, + enable = true, + }, + }, + } + }, + julials = { + settings = { + julia = { + format = { + indent = 2, + }, + lsp = { + autoStart = true, + provideFormatter = true, + }, + }, + }, + }, + lua_ls = { + settings = { + Lua = { + completion = { + callSnippet = "Replace", + }, + runtime = { + version = "LuaJIT", + }, + diagnostics = { + disable = { "trailing-space" }, + }, + workspace = { + checkThirdParty = false, + }, + doc = { + privateName = { "^_" }, + }, + telemetry = { + enable = false, + }, + }, + }, + }, + } + + local lsp_flags = { + allow_incremental_sync = true, + } + + if vim.fn.has("nvim-0.11") == 1 then + -- Neovim 0.11+ Native LSP Configuration + for name, config in pairs(servers) do + vim.lsp.config(name, config) + end + vim.lsp.config('*', { flags = lsp_flags }) + + -- Enable all defined servers + vim.lsp.enable(vim.tbl_keys(servers)) + else + -- Fallback for Neovim < 0.11 (using nvim-lspconfig) + local lspconfig = require('lspconfig') + for name, config in pairs(servers) do + local final_config = vim.tbl_extend("force", { flags = lsp_flags }, config) + lspconfig[name].setup(final_config) + end + end +end) diff --git a/scripts/updater.nix b/scripts/updater.nix new file mode 100644 index 0000000..063ec8d --- /dev/null +++ b/scripts/updater.nix @@ -0,0 +1,14 @@ +{pkgs}: +pkgs.writeShellApplication { + name = "updateR"; + + # Tools your script needs at runtime + runtimeInputs = [ + pkgs.wget + pkgs.gnused + pkgs.coreutils + ]; + + # Keep script in separate file, but embed contents + text = builtins.readFile ./updater.sh; +} diff --git a/scripts/updater.sh b/scripts/updater.sh new file mode 100644 index 0000000..ccf5e2e --- /dev/null +++ b/scripts/updater.sh @@ -0,0 +1,22 @@ +echo "📡 Fetching latest R version from rstats-on-nix..." +RVER=$( wget -qO- 'https://raw.githubusercontent.com/ropensci/rix/refs/heads/main/inst/extdata/available_df.csv' | tail -n 1 | head -n 1 | cut -d',' -f4 | tr -d '"' ) + +# Validate RVER matches YYYY-MM-DD format +if [[ ! "$RVER" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "❌ Error: Failed to fetch valid R version date. Got: '$RVER'" + exit 1 +fi + +echo "✅ R date is $RVER" + +# Create backup of flake.nix before modifying +cp flake.nix flake.nix.backup + +# Update rixpkgs date in flake.nix +if sed -i "s|rixpkgs.url = \"github:rstats-on-nix/nixpkgs/[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\";|rixpkgs.url = \"github:rstats-on-nix/nixpkgs/$RVER\";|" flake.nix; then + echo "✅ Updated rixpkgs date in flake.nix" + rm flake.nix.backup +else + echo "⚠️ Warning: Failed to update flake.nix, restoring backup" + mv flake.nix.backup flake.nix +fi diff --git a/spell/de.utf-8.spl b/spell/de.utf-8.spl new file mode 100644 index 0000000..b83bdc1 Binary files /dev/null and b/spell/de.utf-8.spl differ diff --git a/spell/de.utf-8.sug b/spell/de.utf-8.sug new file mode 100644 index 0000000..d4411b9 Binary files /dev/null and b/spell/de.utf-8.sug differ diff --git a/tests/init.lua b/tests/init.lua new file mode 100644 index 0000000..42c7e5a --- /dev/null +++ b/tests/init.lua @@ -0,0 +1,28 @@ +local function assert_ok(value, message) + if not value then + error(message or "assertion failed") + end +end + +local ok, nix = pcall(require, "config.nix") +assert_ok(ok, "Failed to require config.nix") + +local init_ok, helper = pcall(function() + return nix.init({ non_nix_value = true }) +end) +assert_ok(init_ok and helper, "Failed to initialize config.nix helper") + +-- Basic shape checks +assert_ok(type(helper.is_nix) == "boolean", "Expected helper.is_nix to be boolean") + +-- Cat/setting access should return defaults without errors +local cat_value = helper.get_cat("general", true) +assert_ok(type(cat_value) == "boolean", "Expected get_cat to return boolean") + +local background = helper.get_setting("dark", "background") +assert_ok(type(background) == "string", "Expected get_setting to return string") + +local info_value = helper.get_info("nvim", "nixCats_configDirName") +assert_ok(type(info_value) == "string", "Expected get_info to return string") + +print("[tests/init.lua] nix helper smoke test passed")