├── .gitignore ├── stylua.toml ├── nix ├── TODO.md ├── files │ ├── ignore │ ├── tom.pub │ └── AGENTS.md ├── modules │ ├── colors.nix │ ├── shell.nix │ ├── darwin.nix │ ├── git.nix │ ├── home-manager.nix │ └── opencode.nix ├── flake.nix └── flake.lock ├── .github ├── README.md └── workflows │ └── gitguardian.yml ├── nvim ├── TODO.md ├── lua │ ├── util │ │ ├── pick.lua │ │ ├── ui.lua │ │ ├── keymaps.lua │ │ ├── format.lua │ │ ├── lsp.lua │ │ ├── cmp.lua │ │ ├── root.lua │ │ └── init.lua │ ├── config │ │ ├── init.lua │ │ ├── autocmds.lua │ │ ├── options.lua │ │ └── keymaps.lua │ └── plugins.lua ├── init.lua └── lazy-lock.json ├── Makefile ├── AGENTS.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | undo 3 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 120 4 | [sort_requires] 5 | enabled = true 6 | -------------------------------------------------------------------------------- /nix/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - bat theme 4 | - dynamic theme https://x.com/mitchellh/status/1859725298360889539 5 | https://github.com/raindev/daybreak.nvim 6 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ```fish 4 | gh repo clone tmm/dotfiles 5 | cd dotfiles 6 | make 7 | ``` 8 | 9 | ## Commands 10 | 11 | ```fish 12 | make # Bootstrap setup 13 | make nix # Set up nix 14 | make nvim # Set up nvim 15 | ``` 16 | -------------------------------------------------------------------------------- /nix/files/ignore: -------------------------------------------------------------------------------- 1 | .Trash 2 | .bun 3 | .cache 4 | .cargo 5 | .cups 6 | .dropbox 7 | .files 8 | .fly 9 | .foundry 10 | .gem 11 | .hex 12 | .local 13 | .mix 14 | .nix-* 15 | .npm 16 | .replay 17 | .rustup 18 | .svm 19 | .vscode 20 | Dropbox 21 | Library 22 | Movies 23 | Music 24 | Pictures 25 | .mylogin.cnf 26 | .npmrc 27 | .python_history 28 | .viminfo 29 | **/.DS_Store 30 | **/.elixir_ls 31 | **/.localized 32 | **/node_modules 33 | **/undo 34 | -------------------------------------------------------------------------------- /nvim/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | lazy 3aa2916569df2664cb68e1c7c38882868f36f8d0 4 | 5 | ## Plugins 6 | 7 | - https://github.com/nvim-treesitter/nvim-treesitter-textobjects 8 | 9 | ## Misc 10 | 11 | - add snippets 12 | - emmet-like plugin 13 | - add recording macro to statusline 14 | 15 | ## Links 16 | 17 | - https://github.com/MariaSolOs/dotfiles 18 | - https://github.com/folke/dot 19 | - https://github.com/ssgabrieldev/tools/blob/master/nvim/lua/plugins/dap.lua 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PIP=PIP_REQUIRE_VIRTUALENV=false pip 2 | 3 | BREW := $(shell [ $$(uname -m) = arm64 ] && echo /opt/homebrew || echo /usr/local)/bin/brew 4 | OS := $(shell uname) 5 | 6 | all: $(OS) nix nvim 7 | 8 | Darwin: 9 | Linux: 10 | 11 | $(BREW): 12 | @echo Installing Homebrew 13 | @sudo curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh | bash 14 | 15 | .PHONY: nix 16 | nix: 17 | @sudo curl -fsSL https://nixos.org/nix/install | bash 18 | @nix run nix-darwin -- switch --flake $$DOTFILES_HOME/nix 19 | @darwin-rebuild switch --flake $$DOTFILES_HOME/nix 20 | 21 | .PHONY: nvim 22 | nvim: 23 | @nvim -c qall 24 | @nvim --headless -c 'autocmd User LazyInstall quitall' -c 'Lazy install' 25 | -------------------------------------------------------------------------------- /nix/files/tom.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC/Jq1ON3FEmGqWtKwd2JML+imauO3Rfdk8J9ra8yvuxcXLT7bAOGGOKm8nETd9IVeTZMoU01yyHtrbwI3G+LG2imPrXmSgUri6MGZ/Ef5+7pUsTPStjN38mpSoMsQuxdH95vwWOZG4+pf+0PWM57B4iWHqpYn/ZHox3NG7CKYpNkfzdY9OdH00h16Z+Amapykk2CD3QHhNZSjKNf+ADcnskJCu/rSRudXKXRp6iN8Bm91xk4rYXEZg53a2Ga9o++3jCg0mep6qMEFG3CcF4sFR/mN1zxa3Q9npEagNmZ+pvjV8notbbpVsNyiv/xlpDE34FkOksmYK2vIW2LY6ClHGdDe7kcSTFdOfDmTsztlh0H/h61iE/BPMrZwqIrlqwzpP6VRxqavH2zIHusBS5NX88ee5dN8JSvLQ/BfODQv3vp+tHKLSbIi5mqhnAK4DbmYGddswSCUFvI0NPTYNZgXNmZQ0CpKPyYu0uphN8Ra3pblbTpNdeWuP5zI1NW70rkLVuaPC/OgJL4UWKKnbBKPAH/FHrkjFVvBcAH2DYYYTS+E3Iu45p9HRaZcXv2Wrhw2d9AcQcqtqV0SlITg1rxVEBn8t/pxkBL4vBkCM9YVKYWSFOQK5kpbzUmfA1R9wYTAIzD8uw6rxQdXV3KM8msaCqtlqpMLRqCuO2XoA23e+/Q== tom@meagher.co 2 | -------------------------------------------------------------------------------- /nix/modules/colors.nix: -------------------------------------------------------------------------------- 1 | { 2 | dark = { 3 | background = "#0f0f0e"; 4 | foreground = "#ffffff"; 5 | cursor = "#F76EC9"; 6 | 7 | black = "#1D1D1B"; 8 | blue = "#95b3d0"; 9 | cyan = "#7DE8E8"; 10 | green = "#94D1B3"; 11 | magenta = "#d98cd9"; 12 | orange = "#ffc799"; 13 | red = "#d6665c"; 14 | white = "#ffffff"; 15 | yellow = "#ffff66"; 16 | }; 17 | 18 | bright = { 19 | background = "#fdfdfc"; 20 | foreground = "#000000"; 21 | cursor = "#F76EC9"; 22 | 23 | black = "#000000"; 24 | blue = "#5454a6"; 25 | cyan = "#85ffbc"; 26 | green = "#00662d"; 27 | magenta = "#FF00FF"; 28 | orange = "#e6b594"; 29 | red = "#990000"; 30 | white = "#000000"; 31 | yellow = "#ffff00"; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/gitguardian.yml: -------------------------------------------------------------------------------- 1 | name: GitGuardian scan 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | scanning: 7 | name: GitGuardian scan 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 # fetch all history so multiple commits can be scanned 14 | 15 | - name: GitGuardian scan 16 | uses: GitGuardian/ggshield/actions/secret@cfb60b30abf1fd5f18a736220612c4b055a3932b 17 | env: 18 | GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} 19 | GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} 20 | GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} 21 | GITHUB_PUSH_BASE_SHA: ${{ github.event.base }} 22 | GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }} 23 | -------------------------------------------------------------------------------- /nvim/lua/util/pick.lua: -------------------------------------------------------------------------------- 1 | local M = setmetatable({}, { 2 | __call = function(m, ...) 3 | return m.wrap(...) 4 | end, 5 | }) 6 | 7 | function M.open(command, opts) 8 | return function() 9 | opts = opts or {} 10 | 11 | if type(opts.cwd) == "boolean" then 12 | require("util.init").warn("pick: opts.cwd should be a string or nil") 13 | opts.cwd = nil 14 | end 15 | 16 | if not opts.cwd and opts.root ~= false then 17 | opts.cwd = require("util.root").get({ buf = opts.buf }) 18 | end 19 | 20 | require("snacks").picker.pick(command, opts) 21 | end 22 | end 23 | 24 | function M.wrap(command, opts) 25 | opts = opts or {} 26 | return function() 27 | M.open(command, vim.deepcopy(opts)) 28 | end 29 | end 30 | 31 | function M.config_files() 32 | return M.wrap("files", { cwd = vim.fn.stdpath("config") }) 33 | end 34 | 35 | return M 36 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Agent Guidelines for Dotfiles Repository 2 | 3 | Agent guidance for this repository. 4 | 5 | > **Communication Style**: Be brief, concise. Maximize information density, minimize tokens. Incomplete sentences acceptable when clear. Remove filler words. Prioritize clarity over grammar. 6 | 7 | ## Build/Test Commands 8 | 9 | - `drs` - Apply Nix configuration with darwin-rebuild 10 | 11 | ## Code Style Guidelines 12 | 13 | ### Nix Files 14 | 15 | - Use 2-space indentation 16 | - Follow attribute set formatting with proper alignment 17 | - Use `with pkgs;` for package lists 18 | - Keep imports at top of file 19 | 20 | ### Lua Files (Neovim) 21 | 22 | - Use 2-space indentation (per stylua.toml) 23 | - 120 character line limit 24 | - Sort requires alphabetically (enabled in stylua.toml) 25 | - Use snake_case for variables and functions 26 | - Use PascalCase for modules/classes 27 | - Return module table at end of files 28 | 29 | ### General Conventions 30 | 31 | - No trailing whitespace 32 | - Unix line endings (LF) 33 | - UTF-8 encoding 34 | - Prefer explicit over implicit configurations 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Tom Meagher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nvim/init.lua: -------------------------------------------------------------------------------- 1 | require("config.options") 2 | 3 | -- Bootstrap lazy.nvim 4 | local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" 5 | if not (vim.uv or vim.loop).fs_stat(lazypath) then 6 | local lazyrepo = "https://github.com/folke/lazy.nvim.git" 7 | local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) 8 | if vim.v.shell_error ~= 0 then 9 | vim.api.nvim_echo({ 10 | { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, 11 | { out, "WarningMsg" }, 12 | { "\nPress any key to exit..." }, 13 | }, true, {}) 14 | vim.fn.getchar() 15 | os.exit(1) 16 | end 17 | end 18 | vim.opt.rtp:prepend(lazypath) 19 | 20 | require("lazy").setup({ 21 | change_detection = { 22 | enabled = true, 23 | notify = false, 24 | }, 25 | checker = { 26 | enabled = true, 27 | notify = false, 28 | }, 29 | dev = { 30 | path = "~/Developer", 31 | }, 32 | install = { colorscheme = { "rsms" } }, 33 | spec = { 34 | { import = "plugins" }, 35 | }, 36 | }) 37 | 38 | -- delay notifications till vim.notify was replaced or after 500ms 39 | require("util.init").lazy_notify() 40 | 41 | local group = vim.api.nvim_create_augroup("TmmVim", { clear = true }) 42 | vim.api.nvim_create_autocmd("User", { 43 | group = group, 44 | pattern = "VeryLazy", 45 | callback = function() 46 | require("config.keymaps") 47 | require("config.autocmds") 48 | 49 | require("util.format").setup() 50 | end, 51 | }) 52 | -------------------------------------------------------------------------------- /nix/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "home"; 3 | 4 | inputs = { 5 | darwin.inputs.nixpkgs.follows = "nixpkgs"; 6 | darwin.url = "github:lnl7/nix-darwin/master"; 7 | home-manager.inputs.nixpkgs.follows = "nixpkgs"; 8 | home-manager.url = "github:nix-community/home-manager/master"; 9 | nixpkgs.url = "github:nixos/nixpkgs/master"; 10 | nixpkgs-unstable.url = "github:nixoS/nixpkgs/nixpkgs-unstable"; 11 | }; 12 | 13 | outputs = 14 | inputs: 15 | let 16 | dotfilesDir = "Developer/dotfiles"; 17 | username = "tmm"; 18 | in 19 | { 20 | darwinConfigurations.${username} = inputs.darwin.lib.darwinSystem { 21 | specialArgs = { 22 | inherit username; 23 | }; 24 | system = "aarch64-darwin"; 25 | pkgs = import inputs.nixpkgs { 26 | system = "aarch64-darwin"; 27 | config.allowUnfree = true; 28 | }; 29 | modules = [ 30 | ./modules/darwin.nix 31 | inputs.home-manager.darwinModules.home-manager 32 | { 33 | home-manager = { 34 | useGlobalPkgs = true; 35 | useUserPackages = true; 36 | users.${username} = import ./modules/home-manager.nix; 37 | extraSpecialArgs = { 38 | inherit dotfilesDir; 39 | pkgsUnstable = import inputs.nixpkgs-unstable { 40 | system = "aarch64-darwin"; 41 | }; 42 | }; 43 | }; 44 | } 45 | ]; 46 | }; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /nix/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "darwin": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1755825449, 11 | "narHash": "sha256-XkiN4NM9Xdy59h69Pc+Vg4PxkSm9EWl6u7k6D5FZ5cM=", 12 | "owner": "lnl7", 13 | "repo": "nix-darwin", 14 | "rev": "8df64f819698c1fee0c2969696f54a843b2231e8", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "lnl7", 19 | "ref": "master", 20 | "repo": "nix-darwin", 21 | "type": "github" 22 | } 23 | }, 24 | "home-manager": { 25 | "inputs": { 26 | "nixpkgs": [ 27 | "nixpkgs" 28 | ] 29 | }, 30 | "locked": { 31 | "lastModified": 1756022458, 32 | "narHash": "sha256-J1i35r4HfNDdPpwL0vOBaZopQudAUVtartEerc1Jryc=", 33 | "owner": "nix-community", 34 | "repo": "home-manager", 35 | "rev": "9e3a33c0bcbc25619e540b9dfea372282f8a9740", 36 | "type": "github" 37 | }, 38 | "original": { 39 | "owner": "nix-community", 40 | "ref": "master", 41 | "repo": "home-manager", 42 | "type": "github" 43 | } 44 | }, 45 | "nixpkgs": { 46 | "locked": { 47 | "lastModified": 1756173483, 48 | "narHash": "sha256-oZXL9UHIBi6omPmre8WSFcHKbAblhEmpxYvaeZGERng=", 49 | "owner": "nixos", 50 | "repo": "nixpkgs", 51 | "rev": "4c66df3a815eff7cef4f96057af20bc6d59a4950", 52 | "type": "github" 53 | }, 54 | "original": { 55 | "owner": "nixos", 56 | "ref": "master", 57 | "repo": "nixpkgs", 58 | "type": "github" 59 | } 60 | }, 61 | "nixpkgs-unstable": { 62 | "locked": { 63 | "lastModified": 1756128520, 64 | "narHash": "sha256-R94HxJBi+RK1iCm8Y4Q9pdrHZl0GZoDPIaYwjxRNPh4=", 65 | "owner": "nixoS", 66 | "repo": "nixpkgs", 67 | "rev": "c53baa6685261e5253a1c355a1b322f82674a824", 68 | "type": "github" 69 | }, 70 | "original": { 71 | "owner": "nixoS", 72 | "ref": "nixpkgs-unstable", 73 | "repo": "nixpkgs", 74 | "type": "github" 75 | } 76 | }, 77 | "root": { 78 | "inputs": { 79 | "darwin": "darwin", 80 | "home-manager": "home-manager", 81 | "nixpkgs": "nixpkgs", 82 | "nixpkgs-unstable": "nixpkgs-unstable" 83 | } 84 | } 85 | }, 86 | "root": "root", 87 | "version": 7 88 | } 89 | -------------------------------------------------------------------------------- /nvim/lazy-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "amp.nvim": { "branch": "main", "commit": "3b9ad5ef0328de1b35cc9bfa723a37db5daf9434" }, 3 | "blink.cmp": { "branch": "main", "commit": "b19413d214068f316c78978b08264ed1c41830ec" }, 4 | "conform.nvim": { "branch": "master", "commit": "9b8fa5e0b78168f68bee9bf886dc20f287c61e02" }, 5 | "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, 6 | "friendly-snippets": { "branch": "main", "commit": "572f5660cf05f8cd8834e096d7b4c921ba18e175" }, 7 | "gitsigns.nvim": { "branch": "main", "commit": "5813e4878748805f1518cee7abb50fd7205a3a48" }, 8 | "grug-far.nvim": { "branch": "main", "commit": "b58b2d65863f4ebad88b10a1ddd519e5380466e0" }, 9 | "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, 10 | "lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" }, 11 | "lush.nvim": { "branch": "main", "commit": "9c60ec2279d62487d942ce095e49006af28eed6e" }, 12 | "mason-lspconfig.nvim": { "branch": "main", "commit": "1a31f824b9cd5bc6f342fc29e9a53b60d74af245" }, 13 | "mason.nvim": { "branch": "main", "commit": "fc98833b6da5de5a9c5b1446ac541577059555be" }, 14 | "mini.icons": { "branch": "main", "commit": "ff2e4f1d29f659cc2bad0f9256f2f6195c6b2428" }, 15 | "mini.pairs": { "branch": "main", "commit": "d5a29b6254dad07757832db505ea5aeab9aad43a" }, 16 | "mini.surround": { "branch": "main", "commit": "88c52297ed3e69ecf9f8652837888ecc727a28ee" }, 17 | "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, 18 | "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, 19 | "nvim-lspconfig": { "branch": "master", "commit": "ef96fce99c72ff4b6e9c44ed6b15f8a52e6a7284" }, 20 | "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, 21 | "nvim-treesitter-context": { "branch": "master", "commit": "64dd4cf3f6fd0ab17622c5ce15c91fc539c3f24a" }, 22 | "nvim-ts-autotag": { "branch": "main", "commit": "c4ca798ab95b316a768d51eaaaee48f64a4a46bc" }, 23 | "oil.nvim": { "branch": "master", "commit": "cbcb3f997f6f261c577b943ec94e4ef55108dd95" }, 24 | "rustaceanvim": { "branch": "master", "commit": "823d2adfed6e8ba13e6e0dfd5d2e278868557017" }, 25 | "snacks.nvim": { "branch": "main", "commit": "fe7cfe9800a182274d0f868a74b7263b8c0c020b" }, 26 | "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, 27 | "ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" }, 28 | "twoslash-queries.nvim": { "branch": "main", "commit": "1262c20cad5abd6e89995dc4bc0eaab0e2e4e0b9" }, 29 | "vim-repeat": { "branch": "master", "commit": "65846025c15494983dafe5e3b46c8f88ab2e9635" }, 30 | "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } 31 | } 32 | -------------------------------------------------------------------------------- /nix/modules/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | programs.fish = { 4 | enable = true; 5 | interactiveShellInit = '' 6 | # Ghostty supports auto-injection but Nix-darwin hard overwrites XDG_DATA_DIRS 7 | # which make it so that we can't use the auto-injection. We have to source 8 | # manually. 9 | if set -q GHOSTTY_RESOURCES_DIR 10 | source "$GHOSTTY_RESOURCES_DIR/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish" 11 | end 12 | 13 | # disable welcome message 14 | set -g fish_greeting 15 | 16 | # cargo 17 | fish_add_path $HOME/.cargo/bin 18 | 19 | # pnpm 20 | set -gx PNPM_HOME "$HOME/.local/share/pnpm" 21 | set -gx PATH "$PNPM_HOME" $PATH 22 | 23 | # foundry 24 | set -gx FOUNDRY_DIR "$HOME/.foundry" 25 | set -gx PATH "$FOUNDRY_DIR" $PATH 26 | set FOUNDRY_BIN $HOME/.foundry/bin 27 | set -gx FOUNDRY_DISABLE_NIGHTLY_WARNING true 28 | fish_add_path $FOUNDRY_BIN 29 | 30 | # Add `pg_config` to path 31 | # https://fishshell.com/docs/current/tutorial.html?highlight=fish_user_path#path 32 | set PG_CONFIG /Applications/Postgres.app/Contents/Versions/latest/bin 33 | fish_add_path $PG_CONFIG 34 | 35 | fnm env | source 36 | fzf --fish | source 37 | ''; 38 | plugins = [ 39 | # https://github.com/jorgebucaran/autopair.fish 40 | { 41 | name = "autopair.fish"; 42 | src = pkgs.fetchFromGitHub { 43 | owner = "jorgebucaran"; 44 | repo = "autopair.fish"; 45 | rev = "4d1752ff5b39819ab58d7337c69220342e9de0e2"; 46 | sha256 = "sha256-s1o188TlwpUQEN3X5MxUlD/2CFCpEkWu83U9O+wg3VU="; 47 | }; 48 | } 49 | ]; 50 | shellAbbrs = { 51 | a = "ambr"; 52 | b = "bun"; 53 | d = "docker"; 54 | de = "delta"; 55 | dc = "docker compose"; 56 | g = "git"; 57 | i = "iex"; 58 | lsd = "eza -d .*"; 59 | m = "mix"; 60 | n = "npm"; 61 | o = "opencode"; 62 | p = "pnpm"; 63 | v = "nvim"; 64 | }; 65 | shellAliases = { 66 | cat = "bat --style=numbers,changes --theme=\$(defaults read -globalDomain AppleInterfaceStyle &> /dev/null && echo tokyonight_night || echo tokyonight_day)"; 67 | find = "fd"; 68 | fup = "echo $fish_user_paths | tr \" \" \"\n\" | nl"; 69 | ghostty = "/Applications/Ghostty.app/Contents/MacOS/ghostty"; 70 | howto = "gh copilot suggest -t shell"; 71 | ls = "eza"; 72 | reload = "exec $SHELL -l"; 73 | vim = "nvim"; 74 | hide = "defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"; 75 | show = "defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"; 76 | 77 | drs = "sudo darwin-rebuild switch --flake $DOTFILES_HOME/nix"; 78 | dot = "pushd . && cd $DOTFILES_HOME && nvim"; 79 | 80 | hidedesktop = "defaults write com.apple.finder CreateDesktop -bool false && killall Finder"; 81 | showdesktop = "defaults write com.apple.finder CreateDesktop -bool true && killall Finder"; 82 | }; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /nvim/lua/config/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.icons = { 4 | dap = { 5 | Stopped = { "󰁕 ", "DiagnosticWarn", "DapStoppedLine" }, 6 | Breakpoint = " ", 7 | BreakpointCondition = " ", 8 | BreakpointRejected = { " ", "DiagnosticError" }, 9 | LogPoint = ".>", 10 | }, 11 | diagnostics = { 12 | Error = " ", 13 | Warn = " ", 14 | Hint = " ", 15 | Info = " ", 16 | }, 17 | ft = { 18 | octo = "", 19 | }, 20 | git = { 21 | Added = " ", 22 | Branch = "", 23 | Modified = " ", 24 | Removed = " ", 25 | }, 26 | kinds = { 27 | Array = " ", 28 | Boolean = "󰨙 ", 29 | Class = " ", 30 | Color = " ", 31 | Control = " ", 32 | Collapsed = " ", 33 | Constant = "󰏿 ", 34 | Constructor = " ", 35 | Copilot = " ", 36 | Enum = " ", 37 | EnumMember = " ", 38 | Event = " ", 39 | Field = " ", 40 | File = " ", 41 | Folder = " ", 42 | Function = "󰊕 ", 43 | Interface = " ", 44 | Key = " ", 45 | Keyword = " ", 46 | Method = "󰊕 ", 47 | Module = " ", 48 | Namespace = "󰦮 ", 49 | Null = " ", 50 | Number = "󰎠 ", 51 | Object = " ", 52 | Operator = " ", 53 | Package = " ", 54 | Property = " ", 55 | Reference = " ", 56 | Snippet = "󱄽 ", 57 | String = " ", 58 | Struct = "󰆼 ", 59 | Text = " ", 60 | TypeParameter = " ", 61 | Unit = " ", 62 | Value = " ", 63 | Variable = "󰀫 ", 64 | }, 65 | misc = { 66 | Bug = "", 67 | Dots = "󰇘", 68 | PromptPrefix = "❯", 69 | Trace = "", 70 | }, 71 | tree = { 72 | Closed = "▶︎", 73 | Open = "▼", 74 | Empty = "▽", 75 | }, 76 | } 77 | 78 | M.kind_filter = { 79 | default = { 80 | "Class", 81 | "Constructor", 82 | "Enum", 83 | "Field", 84 | "Function", 85 | "Interface", 86 | "Method", 87 | "Module", 88 | "Namespace", 89 | "Package", 90 | "Property", 91 | "Struct", 92 | "Trait", 93 | }, 94 | markdown = false, 95 | help = false, 96 | -- you can specify a different filter for each filetype 97 | lua = { 98 | "Class", 99 | "Constructor", 100 | "Enum", 101 | "Field", 102 | "Function", 103 | "Interface", 104 | "Method", 105 | "Module", 106 | "Namespace", 107 | -- "Package", -- remove package since luals uses it for control flow structures 108 | "Property", 109 | "Struct", 110 | "Trait", 111 | }, 112 | } 113 | 114 | function M.get_kind_filter(buf) 115 | buf = (buf == nil or buf == 0) and vim.api.nvim_get_current_buf() or buf 116 | local ft = vim.bo[buf].filetype 117 | if M.kind_filter == false then 118 | return 119 | end 120 | if M.kind_filter[ft] == false then 121 | return 122 | end 123 | if type(M.kind_filter[ft]) == "table" then 124 | return M.kind_filter[ft] 125 | end 126 | ---@diagnostic disable-next-line: return-type-mismatch 127 | return type(M.kind_filter) == "table" and type(M.kind_filter.default) == "table" and M.kind_filter.default or nil 128 | end 129 | 130 | return M 131 | -------------------------------------------------------------------------------- /nix/modules/darwin.nix: -------------------------------------------------------------------------------- 1 | { pkgs, username, ... }: 2 | { 3 | environment = { 4 | systemPackages = with pkgs; [ 5 | fish 6 | ]; 7 | variables = { 8 | NEXT_TELEMETRY_DISABLED = "1"; 9 | NUXT_TELEMETRY_DISABLED = "1"; 10 | }; 11 | }; 12 | fonts = { 13 | packages = [ pkgs.nerd-fonts.jetbrains-mono ]; 14 | }; 15 | homebrew = { 16 | enable = true; 17 | casks = [ 18 | "1password" 19 | "1password-cli" 20 | "betterdisplay" 21 | "cleanshot" 22 | "contexts" 23 | "daisydisk" 24 | "discord" 25 | "dropbox" 26 | "figma" 27 | "firefox" 28 | "flux" 29 | "google-chrome" 30 | "numi" 31 | "orbstack" 32 | "pixelsnap" 33 | "raycast" 34 | "remarkable" 35 | "sublime-text" 36 | "signal" 37 | "telegram" 38 | "thunderbird" 39 | "vlc" 40 | "zoom" 41 | ]; 42 | masApps = { 43 | BetterSnapTool = 417375580; 44 | Craft = 1487937127; 45 | Dato = 1470584107; 46 | "Day One" = 1055511498; 47 | "Pure Paste" = 1611378436; 48 | "reMarkable desktop" = 1276493162; 49 | Velja = 1607635845; 50 | Xcode = 497799835; 51 | }; 52 | }; 53 | ids.gids.nixbld = 30000; 54 | nix = { 55 | settings = { 56 | substituters = [ "https://cache.nixos.org" ]; 57 | trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; 58 | trusted-users = [ 59 | "root" 60 | username 61 | ]; 62 | experimental-features = [ 63 | "nix-command" 64 | "flakes" 65 | ]; 66 | }; 67 | }; 68 | programs.fish.enable = true; 69 | security.pam.services.sudo_local.touchIdAuth = true; 70 | system.defaults = { 71 | dock.autohide = true; 72 | dock.autohide-delay = 0.0; 73 | dock.autohide-time-modifier = 0.5; 74 | dock.mineffect = "scale"; 75 | dock.show-process-indicators = true; 76 | dock.tilesize = 48; 77 | finder._FXShowPosixPathInTitle = true; 78 | finder.AppleShowAllExtensions = true; 79 | finder.AppleShowAllFiles = true; 80 | finder.FXEnableExtensionChangeWarning = false; 81 | finder.FXPreferredViewStyle = "Nlsv"; 82 | finder.QuitMenuItem = true; 83 | finder.ShowPathbar = true; 84 | finder.ShowStatusBar = true; 85 | NSGlobalDomain."com.apple.mouse.tapBehavior" = 1.0; 86 | NSGlobalDomain."com.apple.trackpad.scaling" = 3.0; 87 | NSGlobalDomain._HIHideMenuBar = false; 88 | NSGlobalDomain.AppleICUForce24HourTime = true; 89 | NSGlobalDomain.AppleInterfaceStyle = "Dark"; 90 | NSGlobalDomain.AppleInterfaceStyleSwitchesAutomatically = false; 91 | NSGlobalDomain.AppleScrollerPagingBehavior = true; 92 | NSGlobalDomain.AppleShowAllExtensions = true; 93 | NSGlobalDomain.AppleShowAllFiles = true; 94 | NSGlobalDomain.InitialKeyRepeat = 14; 95 | NSGlobalDomain.KeyRepeat = 1; 96 | }; 97 | system.keyboard = { 98 | enableKeyMapping = true; 99 | remapCapsLockToEscape = true; 100 | }; 101 | system.primaryUser = "tmm"; 102 | system.stateVersion = 6; 103 | users.users.tmm = { 104 | home = "/Users/${username}"; 105 | shell = pkgs.fish; 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /nix/modules/git.nix: -------------------------------------------------------------------------------- 1 | { ... }: 2 | { 3 | programs.git = { 4 | enable = true; 5 | aliases = { 6 | a = "add"; 7 | aa = "add ."; 8 | au = "add --update"; 9 | b = "branch"; 10 | c = "commit -m"; 11 | cn = "commit --no-verify -m"; 12 | ch = "checkout"; 13 | l = "log"; 14 | p = "push"; 15 | pf = "push --force"; 16 | pl = "pull"; 17 | s = "status"; 18 | 19 | amend = "commit --amend --reuse-message=HEAD"; 20 | go = "!go() { git checkout -b $1 2> /dev/null || git checkout $1; }; go"; 21 | hist = "log --pretty=oneline --pretty=format:'%Cred%h%Creset %C(yellow)%an%Creset %s%C(normal dim)%d%Creset %Cgreen(%cr)%Creset' --date=relative --abbrev-commit"; 22 | monkeys = "shortlog --summary --numbered"; 23 | undo = "reset --soft HEAD^"; 24 | unstage = "reset HEAD --"; 25 | }; 26 | ignores = [ 27 | "*.un~" 28 | ".*.sw[a-z]" 29 | ".DS_Store" 30 | ".Spotlight-V100" 31 | ".Trashes" 32 | "._*" 33 | ".env" 34 | ".envrc" 35 | "Session.vim" 36 | ]; 37 | userName = "Tom Meagher"; 38 | userEmail = "tom@meagher.co"; 39 | extraConfig = { 40 | branch.sort = "-committerdate"; 41 | color.ui = "auto"; 42 | commit.gpgsign = true; 43 | core = { 44 | editor = "nvim"; 45 | excludesfile = "~/.config/git/ignore_global"; 46 | pager = "delta"; 47 | }; 48 | credential.helper = "osxkeychain"; 49 | delta = { 50 | # TODO: nvim support 51 | # hyperlinks = true; 52 | navigate = true; 53 | line-numbers = true; 54 | side-by-side = true; 55 | file-added-label = " "; 56 | file-decoration-style = "#272725 ul"; 57 | file-modified-label = " "; 58 | file-removed-label = " "; 59 | file-renamed-label = " "; 60 | file-style = "#BFBFBF"; 61 | hunk-header-style = "omit"; 62 | line-numbers-left-style = "#272725"; 63 | line-numbers-minus-style = "#F0F6FC #552527"; 64 | line-numbers-plus-style = "#F0F6FC #1F4429"; 65 | line-numbers-right-style = "#272725"; 66 | line-numbers-zero-style = "#4A4945"; 67 | minus-emph-style = "#F0F6FC #7F302F"; 68 | minus-empty-line-marker-style = "syntax #301B1E"; 69 | minus-non-emph-style = "syntax #301B1E"; 70 | minus-style = "syntax #301B1E"; 71 | plus-emph-style = "#F0F6FC #1D572D"; 72 | plus-empty-line-marker-style = "syntax #14261D"; 73 | plus-non-emph-style = "syntax #14261D"; 74 | plus-style = "syntax #14261D"; 75 | }; 76 | github.user = "tmm"; 77 | gpg = { 78 | format = "ssh"; 79 | ssh.program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign"; 80 | }; 81 | include = { 82 | path = "~/.config/delta/themes.gitconfig"; 83 | }; 84 | init.defaultBranch = "main"; 85 | interactive = { 86 | diffFilter = "delta --color-only"; 87 | }; 88 | merge = { 89 | conflictstyle = "zdiff3"; 90 | }; 91 | pull.rebase = false; 92 | push = { 93 | autoSetupRemote = true; 94 | default = "current"; 95 | }; 96 | user.signingkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHLwl9HCwJ1+kNCbrx3N15oIcNfW7SgZBTFlmQnQEVn4"; 97 | }; 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /nvim/lua/config/autocmds.lua: -------------------------------------------------------------------------------- 1 | local function augroup(name) 2 | return vim.api.nvim_create_augroup("tmm_" .. name, { clear = true }) 3 | end 4 | 5 | -- Check if we need to reload the file when it changed 6 | vim.api.nvim_create_autocmd({ "FocusGained", "TermClose", "TermLeave" }, { 7 | group = augroup("checktime"), 8 | callback = function() 9 | if vim.o.buftype ~= "nofile" then 10 | vim.cmd("checktime") 11 | end 12 | end, 13 | }) 14 | 15 | -- Highlight on yank 16 | vim.api.nvim_create_autocmd("TextYankPost", { 17 | group = augroup("highlight_yank"), 18 | callback = function() 19 | (vim.hl or vim.highlight).on_yank() 20 | end, 21 | }) 22 | 23 | -- resize splits if window got resized 24 | vim.api.nvim_create_autocmd({ "VimResized" }, { 25 | group = augroup("resize_splits"), 26 | callback = function() 27 | local current_tab = vim.fn.tabpagenr() 28 | vim.cmd("tabdo wincmd =") 29 | vim.cmd("tabnext " .. current_tab) 30 | end, 31 | }) 32 | 33 | -- go to last loc when opening a buffer 34 | vim.api.nvim_create_autocmd("BufReadPost", { 35 | group = augroup("last_loc"), 36 | callback = function(event) 37 | local exclude = { "gitcommit" } 38 | local buf = event.buf 39 | if vim.tbl_contains(exclude, vim.bo[buf].filetype) or vim.b[buf].lazyvim_last_loc then 40 | return 41 | end 42 | vim.b[buf].lazyvim_last_loc = true 43 | local mark = vim.api.nvim_buf_get_mark(buf, '"') 44 | local lcount = vim.api.nvim_buf_line_count(buf) 45 | if mark[1] > 0 and mark[1] <= lcount then 46 | pcall(vim.api.nvim_win_set_cursor, 0, mark) 47 | end 48 | end, 49 | }) 50 | 51 | -- close some filetypes with 52 | vim.api.nvim_create_autocmd("FileType", { 53 | group = augroup("close_with_q"), 54 | pattern = { 55 | "PlenaryTestPopup", 56 | "checkhealth", 57 | "dbout", 58 | "gitsigns-blame", 59 | "grug-far", 60 | "help", 61 | "lspinfo", 62 | "neotest-output", 63 | "neotest-output-panel", 64 | "neotest-summary", 65 | "notify", 66 | "qf", 67 | "startuptime", 68 | "tsplayground", 69 | }, 70 | callback = function(event) 71 | vim.bo[event.buf].buflisted = false 72 | vim.schedule(function() 73 | vim.keymap.set("n", "q", function() 74 | vim.cmd("close") 75 | pcall(vim.api.nvim_buf_delete, event.buf, { force = true }) 76 | end, { 77 | buffer = event.buf, 78 | silent = true, 79 | desc = "Quit buffer", 80 | }) 81 | end) 82 | end, 83 | }) 84 | 85 | -- make it easier to close man-files when opened inline 86 | vim.api.nvim_create_autocmd("FileType", { 87 | group = augroup("man_unlisted"), 88 | pattern = { "man" }, 89 | callback = function(event) 90 | vim.bo[event.buf].buflisted = false 91 | end, 92 | }) 93 | 94 | -- wrap and check for spell in text filetypes 95 | vim.api.nvim_create_autocmd("FileType", { 96 | group = augroup("wrap_spell"), 97 | pattern = { "text", "plaintex", "typst", "gitcommit", "markdown" }, 98 | callback = function() 99 | vim.opt_local.wrap = true 100 | vim.opt_local.spell = true 101 | end, 102 | }) 103 | 104 | -- fix conceallevel for json files 105 | vim.api.nvim_create_autocmd({ "FileType" }, { 106 | group = augroup("json_conceal"), 107 | pattern = { "json", "jsonc", "json5" }, 108 | callback = function() 109 | vim.opt_local.conceallevel = 0 110 | end, 111 | }) 112 | 113 | -- auto create dir when saving a file, in case some intermediate directory does not exist 114 | vim.api.nvim_create_autocmd({ "BufWritePre" }, { 115 | group = augroup("auto_create_dir"), 116 | callback = function(event) 117 | if event.match:match("^%w%w+:[\\/][\\/]") then 118 | return 119 | end 120 | local file = vim.uv.fs_realpath(event.match) or event.match 121 | vim.fn.mkdir(vim.fn.fnamemodify(file, ":p:h"), "p") 122 | end, 123 | }) 124 | -------------------------------------------------------------------------------- /nvim/lua/config/options.lua: -------------------------------------------------------------------------------- 1 | vim.g.mapleader = " " 2 | vim.g.maplocalleader = " " 3 | 4 | -- auto format 5 | vim.g.autoformat = true 6 | 7 | -- root dir detection 8 | -- Each entry can be: 9 | -- * the name of a detector function like `lsp` or `cwd` 10 | -- * a pattern or array of patterns like `.git` or `lua`. 11 | -- * a function with signature `function(buf) -> string|string[]` 12 | vim.g.root_spec = { ".git", "lsp", "cwd" } 13 | 14 | -- Set LSP servers to be ignored when used with `util.root.detectors.lsp` 15 | -- for detecting the LSP root 16 | vim.g.root_lsp_ignore = { "copilot" } 17 | 18 | -- Hide deprecation warnings 19 | vim.g.deprecation_warnings = false 20 | 21 | -- Show the current document symbols location from Trouble in lualine 22 | -- You can disable this for a buffer by setting `vim.b.trouble_lualine = false` 23 | vim.g.trouble_lualine = true 24 | 25 | local opt = vim.opt 26 | opt.autowrite = true -- Enable auto write 27 | opt.clipboard = vim.env.SSH_TTY and "" or "unnamedplus" -- Sync with system clipboard 28 | opt.completeopt = "menu,menuone,noselect" 29 | opt.conceallevel = 0 -- Set to `2` to hide * markup for bold and italic, but not markers with substitutions 30 | opt.confirm = true -- Confirm to save changes before exiting modified buffer 31 | opt.cursorline = true -- Enable highlighting of the current line 32 | opt.expandtab = true -- Use spaces instead of tabs 33 | opt.fillchars = { 34 | foldopen = "", 35 | foldclose = "", 36 | fold = " ", 37 | foldsep = " ", 38 | diff = "/", 39 | eob = " ", 40 | } 41 | opt.foldexpr = "v:lua.require'util.ui'.foldexpr()" 42 | opt.foldlevel = 99 43 | opt.foldmethod = "expr" 44 | opt.foldtext = "v:lua.require'util.ui'.foldtext()" 45 | opt.formatexpr = "v:lua.require'util.format'.formatexpr()" 46 | opt.formatoptions = "jcroqlnt" -- tcqj 47 | opt.grepformat = "%f:%l:%c:%m" 48 | opt.grepprg = "rg --vimgrep" 49 | opt.ignorecase = true -- Ignore case 50 | opt.inccommand = "nosplit" -- preview incremental substitute 51 | opt.jumpoptions = "view" 52 | opt.laststatus = 3 -- global statusline 53 | opt.linebreak = true -- Wrap lines at convenient points 54 | opt.list = true -- Show some invisible characters (tabs... 55 | opt.mouse = "" -- Disable mouse 56 | opt.number = true -- Print line number 57 | opt.pumblend = 10 -- Popup blend 58 | opt.pumheight = 10 -- Maximum number of entries in a popup 59 | opt.relativenumber = true -- Relative line numbers 60 | opt.ruler = false -- Disable the default ruler 61 | opt.scrolloff = 4 -- Lines of context 62 | opt.sessionoptions = { "buffers", "curdir", "tabpages", "winsize", "help", "globals", "skiprtp", "folds" } 63 | opt.shiftround = true -- Round indent 64 | opt.shiftwidth = 2 -- Size of an indent 65 | opt.shortmess:append({ W = true, I = true, c = true, C = true }) 66 | opt.showmode = false -- Dont show mode since we have a statusline 67 | opt.sidescrolloff = 8 -- Columns of context 68 | opt.signcolumn = "yes" -- Always show the signcolumn, otherwise it would shift the text each time 69 | opt.smartcase = true -- Don't ignore case with capitals 70 | opt.smartindent = true -- Insert indents automatically 71 | opt.smoothscroll = true 72 | opt.spelllang = { "en" } 73 | opt.splitbelow = true -- Put new windows below current 74 | opt.splitkeep = "screen" 75 | opt.splitright = true -- Put new windows right of current 76 | opt.statuscolumn = [[%!v:lua.require'snacks.statuscolumn'.get()]] 77 | opt.tabstop = 2 -- Number of spaces tabs count for 78 | opt.termguicolors = true -- True color support 79 | opt.timeoutlen = 300 -- Lower than default (1000) to quickly trigger which-key 80 | vim.opt.title = true -- Set terminal title 81 | opt.undofile = true 82 | opt.undolevels = 10000 83 | opt.updatetime = 200 -- Save swap file and trigger CursorHold 84 | opt.virtualedit = "block" -- Allow cursor to move where there is no text in visual block mode 85 | opt.visualbell = true -- Disable beeping 86 | opt.wildmode = "longest:full,full" -- Command-line completion mode 87 | opt.winminwidth = 5 -- Minimum window width 88 | opt.wrap = false -- Disable line wrap 89 | 90 | -- make all keymaps silent by default 91 | local keymap_set = vim.keymap.set 92 | vim.keymap.set = function(mode, lhs, rhs, opts) 93 | opts = opts or {} 94 | opts.silent = opts.silent ~= false 95 | return keymap_set(mode, lhs, rhs, opts) 96 | end 97 | -------------------------------------------------------------------------------- /nvim/lua/util/ui.lua: -------------------------------------------------------------------------------- 1 | ---@class util.ui 2 | local M = {} 3 | 4 | -- optimized treesitter foldexpr for Neovim >= 0.10.0 5 | function M.foldexpr() 6 | local buf = vim.api.nvim_get_current_buf() 7 | if vim.b[buf].ts_folds == nil then 8 | -- as long as we don't have a filetype, don't bother 9 | -- checking if treesitter is available (it won't) 10 | if vim.bo[buf].filetype == "" then 11 | return "0" 12 | end 13 | vim.b[buf].ts_folds = pcall(vim.treesitter.get_parser, buf) 14 | end 15 | return vim.b[buf].ts_folds and vim.treesitter.foldexpr() or "0" 16 | end 17 | 18 | -- Based on https://github.com/Wansmer/nvim-config/blob/main/lua/modules/foldtext.lua 19 | -- Which is based on https://www.reddit.com/r/neovim/comments/16sqyjz/finally_we_can_have_highlighted_folds/ 20 | function M.foldtext() 21 | local result = M.parse_line(vim.v.foldstart) 22 | if not result then 23 | return vim.fn.foldtext() 24 | end 25 | 26 | local line_count = vim.v.foldend - vim.v.foldstart - 1 27 | local folded = { 28 | { " +" .. line_count .. " " .. (line_count == 1 and "line" or "lines") .. " ", "FoldedText" }, 29 | } 30 | 31 | for _, item in ipairs(folded) do 32 | table.insert(result, item) 33 | end 34 | 35 | local result2 = M.parse_line(vim.v.foldend) 36 | if result2 then 37 | local first = result2[1] 38 | result2[1] = { vim.trim(first[1]), first[2] } 39 | for _, item in ipairs(result2) do 40 | table.insert(result, item) 41 | end 42 | end 43 | 44 | return result 45 | end 46 | 47 | function M.parse_line(linenr) 48 | local bufnr = vim.api.nvim_get_current_buf() 49 | 50 | local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1] 51 | if not line then 52 | return nil 53 | end 54 | 55 | local ok, parser = pcall(vim.treesitter.get_parser, bufnr) 56 | if not ok then 57 | return nil 58 | end 59 | 60 | local query = vim.treesitter.query.get(parser:lang(), "highlights") 61 | if not query then 62 | return nil 63 | end 64 | 65 | local tree = parser:parse({ linenr - 1, linenr })[1] 66 | 67 | local result = {} 68 | 69 | local line_pos = 0 70 | 71 | for id, node, metadata in query:iter_captures(tree:root(), 0, linenr - 1, linenr) do 72 | local name = query.captures[id] 73 | local start_row, start_col, end_row, end_col = node:range() 74 | 75 | local priority = tonumber(metadata.priority or vim.highlight.priorities.treesitter) 76 | 77 | if start_row == linenr - 1 and end_row == linenr - 1 then 78 | -- check for characters ignored by treesitter 79 | if start_col > line_pos then 80 | table.insert(result, { 81 | line:sub(line_pos + 1, start_col), 82 | { { "Folded", priority } }, 83 | range = { line_pos, start_col }, 84 | }) 85 | end 86 | line_pos = end_col 87 | 88 | local text = line:sub(start_col + 1, end_col) 89 | table.insert(result, { text, { { "@" .. name, priority } }, range = { start_col, end_col } }) 90 | end 91 | end 92 | 93 | local i = 1 94 | while i <= #result do 95 | -- find first capture that is not in current range and apply highlights on the way 96 | local j = i + 1 97 | while j <= #result and result[j].range[1] >= result[i].range[1] and result[j].range[2] <= result[i].range[2] do 98 | for k, v in ipairs(result[i][2]) do 99 | if not vim.tbl_contains(result[j][2], v) then 100 | table.insert(result[j][2], k, v) 101 | end 102 | end 103 | j = j + 1 104 | end 105 | 106 | -- remove the parent capture if it is split into children 107 | if j > i + 1 then 108 | table.remove(result, i) 109 | else 110 | -- highlights need to be sorted by priority, on equal prio, the deeper nested capture (earlier 111 | -- in list) should be considered higher prio 112 | if #result[i][2] > 1 then 113 | table.sort(result[i][2], function(a, b) 114 | return a[2] < b[2] 115 | end) 116 | end 117 | 118 | result[i][2] = vim.tbl_map(function(tbl) 119 | return tbl[1] 120 | end, result[i][2]) 121 | result[i] = { result[i][1], result[i][2] } 122 | 123 | i = i + 1 124 | end 125 | end 126 | 127 | return result 128 | end 129 | 130 | return M 131 | -------------------------------------------------------------------------------- /nvim/lua/util/keymaps.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M._keys = nil 4 | 5 | function M.get() 6 | if M._keys then 7 | return M._keys 8 | end 9 | 10 | local Snacks = require("snacks") 11 | -- stylua: ignore 12 | M._keys = { 13 | { "cl", "LspInfo", desc = "Lsp Info" }, 14 | { "gd", function() Snacks.picker.lsp_definitions() end, desc = "Goto Definition", has = "definition" }, 15 | { "gr", function() Snacks.picker.lsp_references() end, nowait = true, desc = "References" }, 16 | { "gI", function() Snacks.picker.lsp_implementations() end, desc = "Goto Implementation" }, 17 | { "gy", function() Snacks.picker.lsp_type_definitions() end, desc = "Goto T[y]pe Definition" }, 18 | { "gD", vim.lsp.buf.declaration, desc = "Goto Declaration" }, 19 | { "ss", function() Snacks.picker.lsp_symbols({ filter = require("config").kind_filter }) end, desc = "LSP Symbols", has = "documentSymbol" }, 20 | { "sS", function() Snacks.picker.lsp_workspace_symbols({ filter = require("config").kind_filter }) end, desc = "LSP Workspace Symbols", has = "workspace/symbols" }, 21 | { "\\", function() return vim.lsp.buf.hover() end, desc = "Hover" }, 22 | { "gK", function() return vim.lsp.buf.signature_help() end, desc = "Signature Help", has = "signatureHelp" }, 23 | { "", function () vim.lsp.buf.signature_help() end, mode = "i", desc = "Signature Help", has = "signatureHelp" }, 24 | { "ca", vim.lsp.buf.code_action, desc = "Code Action", mode = { "n", "v" }, has = "codeAction" }, 25 | { "cc", vim.lsp.codelens.run, desc = "Run Codelens", mode = { "n", "v" }, has = "codeLens" }, 26 | { "cC", vim.lsp.codelens.refresh, desc = "Refresh & Display Codelens", mode = { "n" }, has = "codeLens" }, 27 | { "cR", function() Snacks.rename.rename_file() end, desc = "Rename File", mode ={"n"}, has = { "workspace/didRenameFiles", "workspace/willRenameFiles" } }, 28 | { "cr", vim.lsp.buf.rename, desc = "Rename", has = "rename" }, 29 | { "cA", require("util.lsp").action.source, desc = "Source Action", has = "codeAction" }, 30 | { "]]", function() Snacks.words.jump(vim.v.count1) end, has = "documentHighlight", 31 | desc = "Next Reference", cond = function() return Snacks.words.is_enabled() end }, 32 | { "[[", function() Snacks.words.jump(-vim.v.count1) end, has = "documentHighlight", 33 | desc = "Prev Reference", cond = function() return Snacks.words.is_enabled() end }, 34 | { "", function() Snacks.words.jump(vim.v.count1, true) end, has = "documentHighlight", 35 | desc = "Next Reference", cond = function() return Snacks.words.is_enabled() end }, 36 | { "", function() Snacks.words.jump(-vim.v.count1, true) end, has = "documentHighlight", 37 | desc = "Prev Reference", cond = function() return Snacks.words.is_enabled() end }, 38 | } 39 | 40 | return M._keys 41 | end 42 | 43 | ---@param method string|string[] 44 | function M.has(buffer, method) 45 | if type(method) == "table" then 46 | for _, m in ipairs(method) do 47 | if M.has(buffer, m) then 48 | return true 49 | end 50 | end 51 | return false 52 | end 53 | method = method:find("/") and method or "textDocument/" .. method 54 | local clients = require("util.lsp").get_clients({ bufnr = buffer }) 55 | for _, client in ipairs(clients) do 56 | if client:supports_method(method) then 57 | return true 58 | end 59 | end 60 | return false 61 | end 62 | 63 | function M.resolve(buffer) 64 | local Keys = require("lazy.core.handler.keys") 65 | if not Keys.resolve then 66 | return {} 67 | end 68 | local spec = vim.tbl_extend("force", {}, M.get()) 69 | local opts = require("util.init").opts("nvim-lspconfig") 70 | local clients = require("util.lsp").get_clients({ bufnr = buffer }) 71 | for _, client in ipairs(clients) do 72 | local maps = opts.servers[client.name] and opts.servers[client.name].keys or {} 73 | vim.list_extend(spec, maps) 74 | end 75 | return Keys.resolve(spec) 76 | end 77 | 78 | function M.on_attach(_, buffer) 79 | local Keys = require("lazy.core.handler.keys") 80 | local keymaps = M.resolve(buffer) 81 | 82 | for _, keys in pairs(keymaps) do 83 | local has = not keys.has or M.has(buffer, keys.has) 84 | local cond = not (keys.cond == false or ((type(keys.cond) == "function") and not keys.cond())) 85 | 86 | if has and cond then 87 | local opts = Keys.opts(keys) 88 | opts.cond = nil 89 | opts.has = nil 90 | opts.silent = opts.silent ~= false 91 | opts.buffer = buffer 92 | vim.keymap.set(keys.mode or "n", keys.lhs, keys.rhs, opts) 93 | end 94 | end 95 | end 96 | 97 | return M 98 | -------------------------------------------------------------------------------- /nvim/lua/util/format.lua: -------------------------------------------------------------------------------- 1 | ---@class util.format 2 | ---@overload fun(opts?: {force?:boolean}) 3 | local M = setmetatable({}, { 4 | __call = function(m, ...) 5 | return m.format(...) 6 | end, 7 | }) 8 | 9 | ---@class LazyFormatter 10 | ---@field name string 11 | ---@field primary? boolean 12 | ---@field format fun(bufnr:number) 13 | ---@field sources fun(bufnr:number):string[] 14 | ---@field priority number 15 | 16 | M.formatters = {} ---@type LazyFormatter[] 17 | 18 | ---@param formatter LazyFormatter 19 | function M.register(formatter) 20 | M.formatters[#M.formatters + 1] = formatter 21 | table.sort(M.formatters, function(a, b) 22 | return a.priority > b.priority 23 | end) 24 | end 25 | 26 | function M.formatexpr() 27 | return require("conform").formatexpr() 28 | end 29 | 30 | ---@param buf? number 31 | ---@return (LazyFormatter|{active:boolean,resolved:string[]})[] 32 | function M.resolve(buf) 33 | buf = buf or vim.api.nvim_get_current_buf() 34 | local have_primary = false 35 | ---@param formatter LazyFormatter 36 | return vim.tbl_map(function(formatter) 37 | local sources = formatter.sources(buf) 38 | local active = #sources > 0 and (not formatter.primary or not have_primary) 39 | have_primary = have_primary or (active and formatter.primary) or false 40 | return setmetatable({ 41 | active = active, 42 | resolved = sources, 43 | }, { __index = formatter }) 44 | end, M.formatters) 45 | end 46 | 47 | ---@param buf? number 48 | function M.info(buf) 49 | buf = buf or vim.api.nvim_get_current_buf() 50 | local gaf = vim.g.autoformat == nil or vim.g.autoformat 51 | local baf = vim.b[buf].autoformat 52 | local enabled = M.enabled(buf) 53 | local lines = { 54 | "# Status", 55 | ("- [%s] global **%s**"):format(gaf and "x" or " ", gaf and "enabled" or "disabled"), 56 | ("- [%s] buffer **%s**"):format( 57 | enabled and "x" or " ", 58 | baf == nil and "inherit" or baf and "enabled" or "disabled" 59 | ), 60 | } 61 | local have = false 62 | for _, formatter in ipairs(M.resolve(buf)) do 63 | if #formatter.resolved > 0 then 64 | have = true 65 | lines[#lines + 1] = "\n# " .. formatter.name .. (formatter.active and " ***(active)***" or "") 66 | for _, line in ipairs(formatter.resolved) do 67 | lines[#lines + 1] = ("- [%s] **%s**"):format(formatter.active and "x" or " ", line) 68 | end 69 | end 70 | end 71 | if not have then 72 | lines[#lines + 1] = "\n***No formatters available for this buffer.***" 73 | end 74 | require("util.init")[enabled and "info" or "warn"]( 75 | table.concat(lines, "\n"), 76 | { title = "Format (" .. (enabled and "enabled" or "disabled") .. ")" } 77 | ) 78 | end 79 | 80 | ---@param buf? number 81 | function M.enabled(buf) 82 | buf = (buf == nil or buf == 0) and vim.api.nvim_get_current_buf() or buf 83 | local gaf = vim.g.autoformat 84 | local baf = vim.b[buf].autoformat 85 | 86 | -- If the buffer has a local value, use that 87 | if baf ~= nil then 88 | return baf 89 | end 90 | 91 | -- Otherwise use the global value if set, or true by default 92 | return gaf == nil or gaf 93 | end 94 | 95 | ---@param buf? boolean 96 | function M.toggle(buf) 97 | M.enable(not M.enabled(), buf) 98 | end 99 | 100 | ---@param enable? boolean 101 | ---@param buf? boolean 102 | function M.enable(enable, buf) 103 | if enable == nil then 104 | enable = true 105 | end 106 | if buf then 107 | vim.b.autoformat = enable 108 | else 109 | vim.g.autoformat = enable 110 | vim.b.autoformat = nil 111 | end 112 | M.info() 113 | end 114 | 115 | ---@param opts? {force?:boolean, buf?:number} 116 | function M.format(opts) 117 | opts = opts or {} 118 | local buf = opts.buf or vim.api.nvim_get_current_buf() 119 | if not ((opts and opts.force) or M.enabled(buf)) then 120 | return 121 | end 122 | 123 | local done = false 124 | for _, formatter in ipairs(M.resolve(buf)) do 125 | if formatter.active then 126 | done = true 127 | require("util.init").try(function() 128 | return formatter.format(buf) 129 | end, { msg = "Formatter `" .. formatter.name .. "` failed" }) 130 | end 131 | end 132 | 133 | if not done and opts and opts.force then 134 | require("util.init").warn("No formatter available", { title = "Format" }) 135 | end 136 | end 137 | 138 | function M.setup() 139 | -- Autoformat autocmd 140 | vim.api.nvim_create_autocmd("BufWritePre", { 141 | group = vim.api.nvim_create_augroup("TmmFormat", {}), 142 | callback = function(event) 143 | M.format({ buf = event.buf }) 144 | end, 145 | }) 146 | 147 | -- Manual format 148 | vim.api.nvim_create_user_command("TmmFormat", function() 149 | M.format({ force = true }) 150 | end, { desc = "Format selection or buffer" }) 151 | 152 | -- Format info 153 | vim.api.nvim_create_user_command("TmmFormatInfo", function() 154 | M.info() 155 | end, { desc = "Show info about the formatters for the current buffer" }) 156 | end 157 | 158 | ---@param buf? boolean 159 | function M.snacks_toggle(buf) 160 | return require("snacks").toggle({ 161 | name = "Auto Format (" .. (buf and "Buffer" or "Global") .. ")", 162 | get = function() 163 | if not buf then 164 | return vim.g.autoformat == nil or vim.g.autoformat 165 | end 166 | return M.enabled() 167 | end, 168 | set = function(state) 169 | M.enable(state, buf) 170 | end, 171 | }) 172 | end 173 | 174 | return M 175 | -------------------------------------------------------------------------------- /nvim/lua/util/lsp.lua: -------------------------------------------------------------------------------- 1 | ---@class util.lsp 2 | local M = {} 3 | 4 | function M.get_clients(opts) 5 | local ret = {} 6 | ret = vim.lsp.get_clients(opts) 7 | return opts and opts.filter and vim.tbl_filter(opts.filter, ret) or ret 8 | end 9 | 10 | function M.on_attach(on_attach, name) 11 | return vim.api.nvim_create_autocmd("LspAttach", { 12 | callback = function(args) 13 | local buffer = args.buf ---@type number 14 | local client = vim.lsp.get_client_by_id(args.data.client_id) 15 | if client and (not name or client.name == name) then 16 | return on_attach(client, buffer) 17 | end 18 | end, 19 | }) 20 | end 21 | 22 | M._supports_method = {} 23 | 24 | function M.setup() 25 | local register_capability = vim.lsp.handlers["client/registerCapability"] 26 | vim.lsp.handlers["client/registerCapability"] = function(err, res, ctx) 27 | ---@diagnostic disable-next-line: no-unknown 28 | local ret = register_capability(err, res, ctx) 29 | local client = vim.lsp.get_client_by_id(ctx.client_id) 30 | if client then 31 | for buffer in pairs(client.attached_buffers) do 32 | vim.api.nvim_exec_autocmds("User", { 33 | pattern = "LspDynamicCapability", 34 | data = { client_id = client.id, buffer = buffer }, 35 | }) 36 | end 37 | end 38 | return ret 39 | end 40 | M.on_attach(M._check_methods) 41 | M.on_dynamic_capability(M._check_methods) 42 | end 43 | 44 | function M._check_methods(client, buffer) 45 | -- don't trigger on invalid buffers 46 | if not vim.api.nvim_buf_is_valid(buffer) then 47 | return 48 | end 49 | -- don't trigger on non-listed buffers 50 | if not vim.bo[buffer].buflisted then 51 | return 52 | end 53 | -- don't trigger on nofile buffers 54 | if vim.bo[buffer].buftype == "nofile" then 55 | return 56 | end 57 | for method, clients in pairs(M._supports_method) do 58 | clients[client] = clients[client] or {} 59 | if not clients[client][buffer] then 60 | if client.supports_method and client:supports_method(method, buffer) then 61 | clients[client][buffer] = true 62 | vim.api.nvim_exec_autocmds("User", { 63 | pattern = "LspSupportsMethod", 64 | data = { client_id = client.id, buffer = buffer, method = method }, 65 | }) 66 | end 67 | end 68 | end 69 | end 70 | 71 | function M.on_dynamic_capability(fn, opts) 72 | return vim.api.nvim_create_autocmd("User", { 73 | pattern = "LspDynamicCapability", 74 | group = opts and opts.group or nil, 75 | callback = function(args) 76 | local client = vim.lsp.get_client_by_id(args.data.client_id) 77 | local buffer = args.data.buffer ---@type number 78 | if client then 79 | return fn(client, buffer) 80 | end 81 | end, 82 | }) 83 | end 84 | 85 | function M.on_supports_method(method, fn) 86 | M._supports_method[method] = M._supports_method[method] or setmetatable({}, { __mode = "k" }) 87 | return vim.api.nvim_create_autocmd("User", { 88 | pattern = "LspSupportsMethod", 89 | callback = function(args) 90 | local client = vim.lsp.get_client_by_id(args.data.client_id) 91 | local buffer = args.data.buffer ---@type number 92 | if client and method == args.data.method then 93 | return fn(client, buffer) 94 | end 95 | end, 96 | }) 97 | end 98 | 99 | function M.formatter(opts) 100 | opts = opts or {} 101 | local filter = opts.filter or {} 102 | filter = type(filter) == "string" and { name = filter } or filter 103 | local ret = { 104 | name = "LSP", 105 | primary = true, 106 | priority = 1, 107 | format = function(buf) 108 | M.format(require("util.init").merge({}, filter, { bufnr = buf })) 109 | end, 110 | sources = function(buf) 111 | local clients = M.get_clients(require("util.init").merge({}, filter, { bufnr = buf })) 112 | local ret = vim.tbl_filter(function(client) 113 | return client:supports_method("textDocument/formatting") 114 | or client:supports_method("textDocument/rangeFormatting") 115 | end, clients) 116 | return vim.tbl_map(function(client) 117 | return client.name 118 | end, ret) 119 | end, 120 | } 121 | return require("util.init").merge(ret, opts) --[[@as LazyFormatter]] 122 | end 123 | 124 | function M.format(opts) 125 | opts = vim.tbl_deep_extend( 126 | "force", 127 | {}, 128 | opts or {}, 129 | require("util.init").opts("nvim-lspconfig").format or {}, 130 | require("util.init").opts("conform.nvim").format or {} 131 | ) 132 | local ok, conform = pcall(require, "conform") 133 | -- use conform for formatting with LSP when available, 134 | -- since it has better format diffing 135 | if ok then 136 | opts.formatters = {} 137 | conform.format(opts) 138 | else 139 | vim.lsp.buf.format(opts) 140 | end 141 | end 142 | 143 | M.action = setmetatable({}, { 144 | __index = function(_, action) 145 | return function() 146 | vim.lsp.buf.code_action({ 147 | apply = true, 148 | context = { 149 | only = { action }, 150 | diagnostics = {}, 151 | }, 152 | }) 153 | end 154 | end, 155 | }) 156 | 157 | function M.execute(opts) 158 | local params = { 159 | command = opts.command, 160 | arguments = opts.arguments, 161 | } 162 | if opts.open then 163 | require("trouble").open({ 164 | mode = "lsp_command", 165 | params = params, 166 | }) 167 | else 168 | return vim.lsp.buf_request(0, "workspace/executeCommand", params, opts.handler) 169 | end 170 | end 171 | 172 | return M 173 | -------------------------------------------------------------------------------- /nvim/lua/util/cmp.lua: -------------------------------------------------------------------------------- 1 | ---@class lazyvim.util.cmp 2 | local M = {} 3 | 4 | ---@alias lazyvim.util.cmp.Action fun():boolean? 5 | ---@type table 6 | M.actions = { 7 | -- Native Snippets 8 | snippet_forward = function() 9 | if vim.snippet.active({ direction = 1 }) then 10 | vim.schedule(function() 11 | vim.snippet.jump(1) 12 | end) 13 | return true 14 | end 15 | end, 16 | snippet_stop = function() 17 | if vim.snippet then 18 | vim.snippet.stop() 19 | end 20 | end, 21 | } 22 | 23 | ---@param actions string[] 24 | ---@param fallback? string|fun() 25 | function M.map(actions, fallback) 26 | return function() 27 | for _, name in ipairs(actions) do 28 | if M.actions[name] then 29 | local ret = M.actions[name]() 30 | if ret then 31 | return true 32 | end 33 | end 34 | end 35 | return type(fallback) == "function" and fallback() or fallback 36 | end 37 | end 38 | 39 | ---@alias Placeholder {n:number, text:string} 40 | 41 | ---@param snippet string 42 | ---@param fn fun(placeholder:Placeholder):string 43 | ---@return string 44 | function M.snippet_replace(snippet, fn) 45 | return snippet:gsub("%$%b{}", function(m) 46 | local n, name = m:match("^%${(%d+):(.+)}$") 47 | return n and fn({ n = n, text = name }) or m 48 | end) or snippet 49 | end 50 | 51 | -- This function resolves nested placeholders in a snippet. 52 | ---@param snippet string 53 | ---@return string 54 | function M.snippet_preview(snippet) 55 | local ok, parsed = pcall(function() 56 | return vim.lsp._snippet_grammar.parse(snippet) 57 | end) 58 | return ok and tostring(parsed) 59 | or M.snippet_replace(snippet, function(placeholder) 60 | return M.snippet_preview(placeholder.text) 61 | end):gsub("%$0", "") 62 | end 63 | 64 | -- This function replaces nested placeholders in a snippet with LSP placeholders. 65 | function M.snippet_fix(snippet) 66 | local texts = {} ---@type table 67 | return M.snippet_replace(snippet, function(placeholder) 68 | texts[placeholder.n] = texts[placeholder.n] or M.snippet_preview(placeholder.text) 69 | return "${" .. placeholder.n .. ":" .. texts[placeholder.n] .. "}" 70 | end) 71 | end 72 | 73 | function M.auto_brackets(entry) 74 | local cmp = require("cmp") 75 | local Kind = cmp.lsp.CompletionItemKind 76 | local item = entry:get_completion_item() 77 | if vim.tbl_contains({ Kind.Function, Kind.Method }, item.kind) then 78 | local cursor = vim.api.nvim_win_get_cursor(0) 79 | local prev_char = vim.api.nvim_buf_get_text(0, cursor[1] - 1, cursor[2], cursor[1] - 1, cursor[2] + 1, {})[1] 80 | if prev_char ~= "(" and prev_char ~= ")" then 81 | local keys = vim.api.nvim_replace_termcodes("()", false, false, true) 82 | vim.api.nvim_feedkeys(keys, "i", true) 83 | end 84 | end 85 | end 86 | 87 | -- This function adds missing documentation to snippets. 88 | -- The documentation is a preview of the snippet. 89 | function M.add_missing_snippet_docs(window) 90 | local cmp = require("cmp") 91 | local Kind = cmp.lsp.CompletionItemKind 92 | local entries = window:get_entries() 93 | for _, entry in ipairs(entries) do 94 | if entry:get_kind() == Kind.Snippet then 95 | local item = entry:get_completion_item() 96 | if not item.documentation and item.insertText then 97 | item.documentation = { 98 | kind = cmp.lsp.MarkupKind.Markdown, 99 | value = string.format("```%s\n%s\n```", vim.bo.filetype, M.snippet_preview(item.insertText)), 100 | } 101 | end 102 | end 103 | end 104 | end 105 | 106 | -- This is a better implementation of `cmp.confirm`: 107 | -- * check if the completion menu is visible without waiting for running sources 108 | -- * create an undo point before confirming 109 | -- This function is both faster and more reliable. 110 | function M.confirm(opts) 111 | local cmp = require("cmp") 112 | opts = vim.tbl_extend("force", { 113 | select = true, 114 | behavior = cmp.ConfirmBehavior.Insert, 115 | }, opts or {}) 116 | return function(fallback) 117 | if cmp.core.view:visible() or vim.fn.pumvisible() == 1 then 118 | require("util.init").create_undo() 119 | if cmp.confirm(opts) then 120 | return 121 | end 122 | end 123 | return fallback() 124 | end 125 | end 126 | 127 | function M.expand(snippet) 128 | -- Native sessions don't support nested snippet sessions. 129 | -- Always use the top-level session. 130 | -- Otherwise, when on the first placeholder and selecting a new completion, 131 | -- the nested session will be used instead of the top-level session. 132 | -- See: https://github.com/LazyVim/LazyVim/issues/3199 133 | local session = vim.snippet.active() and vim.snippet._session or nil 134 | 135 | local ok, err = pcall(vim.snippet.expand, snippet) 136 | if not ok then 137 | local fixed = M.snippet_fix(snippet) 138 | ok = pcall(vim.snippet.expand, fixed) 139 | 140 | local msg = ok and "Failed to parse snippet,\nbut was able to fix it automatically." 141 | or ("Failed to parse snippet.\n" .. err) 142 | 143 | require("util.init")[ok and "warn" or "error"]( 144 | ([[%s 145 | ```%s 146 | %s 147 | ```]]):format(msg, vim.bo.filetype, snippet), 148 | { title = "vim.snippet" } 149 | ) 150 | end 151 | 152 | -- Restore top-level session when needed 153 | if session then 154 | vim.snippet._session = session 155 | end 156 | end 157 | 158 | function M.setup(opts) 159 | for _, source in ipairs(opts.sources) do 160 | source.group_index = source.group_index or 1 161 | end 162 | 163 | local parse = require("cmp.utils.snippet").parse 164 | require("cmp.utils.snippet").parse = function(input) 165 | local ok, ret = pcall(parse, input) 166 | if ok then 167 | return ret 168 | end 169 | return M.snippet_preview(input) 170 | end 171 | 172 | local cmp = require("cmp") 173 | cmp.setup(opts) 174 | cmp.event:on("confirm_done", function(event) 175 | if vim.tbl_contains(opts.auto_brackets or {}, vim.bo.filetype) then 176 | M.auto_brackets(event.entry) 177 | end 178 | end) 179 | cmp.event:on("menu_opened", function(event) 180 | M.add_missing_snippet_docs(event.window) 181 | end) 182 | end 183 | 184 | return M 185 | -------------------------------------------------------------------------------- /nvim/lua/util/root.lua: -------------------------------------------------------------------------------- 1 | ---@class util.root 2 | ---@overload fun(): string 3 | local M = setmetatable({}, { 4 | __call = function(m, ...) 5 | return m.get(...) 6 | end, 7 | }) 8 | 9 | ---@class LazyRoot 10 | ---@field paths string[] 11 | ---@field spec LazyRootSpec 12 | 13 | ---@alias LazyRootFn fun(buf: number): (string|string[]) 14 | 15 | ---@alias LazyRootSpec string|string[]|LazyRootFn 16 | 17 | ---@type LazyRootSpec[] 18 | M.spec = { "lsp", { ".git", "lua" }, "cwd" } 19 | 20 | M.detectors = {} 21 | 22 | function M.detectors.cwd() 23 | return { vim.uv.cwd() } 24 | end 25 | 26 | function M.detectors.lsp(buf) 27 | local bufpath = M.bufpath(buf) 28 | if not bufpath then 29 | return {} 30 | end 31 | local roots = {} ---@type string[] 32 | local clients = require("util.lsp").get_clients({ bufnr = buf }) 33 | clients = vim.tbl_filter(function(client) 34 | return not vim.tbl_contains(vim.g.root_lsp_ignore or {}, client.name) 35 | end, clients) 36 | for _, client in pairs(clients) do 37 | local workspace = client.config.workspace_folders 38 | for _, ws in pairs(workspace or {}) do 39 | roots[#roots + 1] = vim.uri_to_fname(ws.uri) 40 | end 41 | if client.root_dir then 42 | roots[#roots + 1] = client.root_dir 43 | end 44 | end 45 | return vim.tbl_filter(function(path) 46 | path = require("util.init").norm(path) 47 | return path and bufpath:find(path, 1, true) == 1 48 | end, roots) 49 | end 50 | 51 | function M.detectors.pattern(buf, patterns) 52 | patterns = type(patterns) == "string" and { patterns } or patterns 53 | local path = M.bufpath(buf) or vim.uv.cwd() 54 | local pattern = vim.fs.find(function(name) 55 | for _, p in ipairs(patterns) do 56 | if name == p then 57 | return true 58 | end 59 | if p:sub(1, 1) == "*" and name:find(vim.pesc(p:sub(2)) .. "$") then 60 | return true 61 | end 62 | end 63 | return false 64 | end, { path = path, upward = true })[1] 65 | return pattern and { vim.fs.dirname(pattern) } or {} 66 | end 67 | 68 | function M.bufpath(buf) 69 | return M.realpath(vim.api.nvim_buf_get_name(assert(buf))) 70 | end 71 | 72 | function M.cwd() 73 | return M.realpath(vim.uv.cwd()) or "" 74 | end 75 | 76 | function M.realpath(path) 77 | if path == "" or path == nil then 78 | return nil 79 | end 80 | path = vim.uv.fs_realpath(path) or path 81 | return require("util.init").norm(path) 82 | end 83 | 84 | ---@param spec LazyRootSpec 85 | ---@return LazyRootFn 86 | function M.resolve(spec) 87 | if M.detectors[spec] then 88 | return M.detectors[spec] 89 | elseif type(spec) == "function" then 90 | return spec 91 | end 92 | return function(buf) 93 | return M.detectors.pattern(buf, spec) 94 | end 95 | end 96 | 97 | ---@param opts? { buf?: number, spec?: LazyRootSpec[], all?: boolean } 98 | function M.detect(opts) 99 | opts = opts or {} 100 | opts.spec = opts.spec or type(vim.g.root_spec) == "table" and vim.g.root_spec or M.spec 101 | opts.buf = (opts.buf == nil or opts.buf == 0) and vim.api.nvim_get_current_buf() or opts.buf 102 | 103 | local ret = {} ---@type LazyRoot[] 104 | for _, spec in ipairs(opts.spec) do 105 | local paths = M.resolve(spec)(opts.buf) 106 | paths = paths or {} 107 | paths = type(paths) == "table" and paths or { paths } 108 | local roots = {} ---@type string[] 109 | for _, p in ipairs(paths) do 110 | local pp = M.realpath(p) 111 | if pp and not vim.tbl_contains(roots, pp) then 112 | roots[#roots + 1] = pp 113 | end 114 | end 115 | table.sort(roots, function(a, b) 116 | return #a > #b 117 | end) 118 | if #roots > 0 then 119 | ret[#ret + 1] = { spec = spec, paths = roots } 120 | if opts.all == false then 121 | break 122 | end 123 | end 124 | end 125 | return ret 126 | end 127 | 128 | function M.info() 129 | local spec = type(vim.g.root_spec) == "table" and vim.g.root_spec or M.spec 130 | 131 | local roots = M.detect({ all = true }) 132 | local lines = {} ---@type string[] 133 | local first = true 134 | for _, root in ipairs(roots) do 135 | for _, path in ipairs(root.paths) do 136 | lines[#lines + 1] = ("- [%s] `%s` **(%s)**"):format( 137 | first and "x" or " ", 138 | path, 139 | type(root.spec) == "table" and table.concat(root.spec, ", ") or root.spec 140 | ) 141 | first = false 142 | end 143 | end 144 | lines[#lines + 1] = "```lua" 145 | lines[#lines + 1] = "vim.g.root_spec = " .. vim.inspect(spec) 146 | lines[#lines + 1] = "```" 147 | require("util.init").info(lines, { title = "Roots" }) 148 | return roots[1] and roots[1].paths[1] or vim.uv.cwd() 149 | end 150 | 151 | ---@type table 152 | M.cache = {} 153 | 154 | function M.setup() 155 | vim.api.nvim_create_user_command("LazyRoot", function() 156 | M.info() 157 | end, { desc = "Roots for the current buffer" }) 158 | 159 | -- FIX: doesn't properly clear cache in neo-tree `set_root` (which should happen presumably on `DirChanged`), 160 | -- probably because the event is triggered in the neo-tree buffer, therefore add `BufEnter` 161 | -- Maybe this is too frequent on `BufEnter` and something else should be done instead?? 162 | vim.api.nvim_create_autocmd({ "LspAttach", "BufWritePost", "DirChanged", "BufEnter" }, { 163 | group = vim.api.nvim_create_augroup("lazyvim_root_cache", { clear = true }), 164 | callback = function(event) 165 | M.cache[event.buf] = nil 166 | end, 167 | }) 168 | end 169 | 170 | -- returns the root directory based on: 171 | -- * lsp workspace folders 172 | -- * lsp root_dir 173 | -- * root pattern of filename of the current buffer 174 | -- * root pattern of cwd 175 | ---@param opts? {normalize?:boolean, buf?:number} 176 | ---@return string 177 | function M.get(opts) 178 | opts = opts or {} 179 | local buf = opts.buf or vim.api.nvim_get_current_buf() 180 | local ret = M.cache[buf] 181 | if not ret then 182 | local roots = M.detect({ all = false, buf = buf }) 183 | ret = roots[1] and roots[1].paths[1] or vim.uv.cwd() 184 | M.cache[buf] = ret 185 | end 186 | if opts and opts.normalize then 187 | return ret 188 | end 189 | return require("util.init").is_win() and ret:gsub("/", "\\") or ret 190 | end 191 | 192 | function M.git() 193 | local root = M.get() 194 | local git_root = vim.fs.find(".git", { path = root, upward = true })[1] 195 | local ret = git_root and vim.fn.fnamemodify(git_root, ":h") or root 196 | return ret 197 | end 198 | 199 | ---@param opts? {hl_last?: string} 200 | function M.pretty_path(opts) 201 | return "" 202 | end 203 | 204 | return M 205 | -------------------------------------------------------------------------------- /nix/modules/home-manager.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | dotfilesDir, 4 | lib, 5 | pkgs, 6 | ... 7 | }: 8 | let 9 | colors = import ./colors.nix; 10 | in 11 | { 12 | home.packages = with pkgs; [ 13 | amber 14 | asciinema 15 | babelfish 16 | bat 17 | btop 18 | cachix 19 | delta 20 | direnv 21 | dockutil 22 | elixir 23 | erlang 24 | eza 25 | fd 26 | fnm 27 | fzf 28 | gh 29 | git 30 | httpie 31 | jq 32 | just 33 | neovim 34 | nixfmt-rfc-style 35 | ripgrep 36 | rustup 37 | starship 38 | zoxide 39 | ]; 40 | home.file = { 41 | ".ignore".source = ../files/ignore; 42 | ".ssh/tom.pub".source = ../files/tom.pub; 43 | }; 44 | home.shell.enableFishIntegration = true; 45 | home.stateVersion = "23.05"; 46 | home.sessionVariables = { 47 | EDITOR = "nvim"; 48 | DOTFILES_HOME = "${config.home.homeDirectory}/${dotfilesDir}"; 49 | SSH_AUTH_SOCK = "${config.home.homeDirectory}/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"; 50 | }; 51 | imports = [ 52 | ./git.nix 53 | ./opencode.nix 54 | ./shell.nix 55 | ]; 56 | programs.bat.enable = true; 57 | programs.direnv = { 58 | enable = true; 59 | config = { 60 | load_dotenv = true; 61 | }; 62 | }; 63 | programs.eza.enable = true; 64 | programs.gh = { 65 | enable = true; 66 | extensions = with pkgs; [ 67 | gh-copilot 68 | gh-poi 69 | ]; 70 | }; 71 | programs.ghostty = { 72 | enable = true; 73 | package = null; # TODO: Add install 74 | settings = { 75 | copy-on-select = "clipboard"; 76 | cursor-style = "block"; 77 | cursor-style-blink = false; 78 | font-family = "JetBrains Nerd Font Mono"; 79 | font-feature = "-calt"; 80 | font-size = 14; 81 | keybind = [ 82 | "global:control+grave_accent=toggle_quick_terminal" 83 | "ctrl+shift+h=goto_split:left" 84 | "ctrl+shift+j=goto_split:bottom" 85 | "ctrl+shift+k=goto_split:top" 86 | "ctrl+shift+l=goto_split:right" 87 | "ctrl+shift+enter=toggle_split_zoom" 88 | "ctrl+shift+u=scroll_page_up" 89 | "ctrl+shift+d=scroll_page_down" 90 | "super+enter=unbind" 91 | ]; 92 | macos-non-native-fullscreen = true; 93 | macos-option-as-alt = "left"; 94 | macos-titlebar-style = "tabs"; 95 | mouse-hide-while-typing = true; 96 | shell-integration-features = "no-cursor"; 97 | theme = "dark:_dark,light:_light"; 98 | unfocused-split-opacity = 1; 99 | window-height = 50; 100 | window-padding-balance = true; 101 | window-padding-x = 0; 102 | window-padding-y = 0; 103 | window-width = 178; 104 | }; 105 | themes = { 106 | # TODO: Add light theme 107 | # https://github.com/mitchellh/ghostty/issues/809 108 | _dark = { 109 | background = colors.dark.background; 110 | cursor-color = colors.dark.cursor; 111 | foreground = colors.dark.foreground; 112 | palette = [ 113 | "0=${colors.dark.black}" 114 | "1=${colors.dark.red}" 115 | "2=${colors.dark.green}" 116 | "3=${colors.dark.yellow}" 117 | "4=${colors.dark.blue}" 118 | "5=${colors.dark.orange}" 119 | "6=${colors.dark.cyan}" 120 | "7=${colors.dark.white}" 121 | ]; 122 | }; 123 | _light = { 124 | background = colors.bright.background; 125 | cursor-color = colors.bright.cursor; 126 | foreground = colors.bright.foreground; 127 | palette = [ 128 | "0=${colors.bright.black}" 129 | "1=${colors.bright.red}" 130 | "2=${colors.bright.green}" 131 | "3=${colors.bright.yellow}" 132 | "4=${colors.bright.blue}" 133 | "5=${colors.bright.orange}" 134 | "6=${colors.bright.cyan}" 135 | "7=${colors.bright.white}" 136 | "8=${colors.bright.black}" 137 | "9=${colors.bright.black}" 138 | "10=${colors.bright.black}" 139 | "11=${colors.bright.black}" 140 | "12=${colors.bright.black}" 141 | "13=${colors.bright.black}" 142 | "14=${colors.bright.black}" 143 | "15=${colors.bright.black}" 144 | ]; 145 | }; 146 | }; 147 | }; 148 | programs.home-manager.enable = true; 149 | programs.ssh = { 150 | enable = true; 151 | forwardAgent = true; 152 | extraConfig = '' 153 | IdentityAgent "${config.home.sessionVariables.SSH_AUTH_SOCK}" 154 | StrictHostKeyChecking no 155 | ''; 156 | }; 157 | programs.starship = { 158 | enable = true; 159 | settings = { 160 | add_newline = true; 161 | format = lib.concatStrings [ 162 | "$username" 163 | "$hostname" 164 | "$directory" 165 | "$git_branch" 166 | "$git_state" 167 | "$git_status" 168 | "$cmd_duration" 169 | "$line_break" 170 | "$character" 171 | ]; 172 | character = { 173 | success_symbol = "[❯](green)"; 174 | error_symbol = "[❯](red)"; 175 | vimcmd_symbol = "[❮](purple)"; 176 | }; 177 | cmd_duration = { 178 | format = "[( $duration)]($style)"; 179 | style = "yellow"; 180 | }; 181 | directory = { 182 | style = "white"; 183 | }; 184 | git_branch = { 185 | format = "[$branch]($style)"; 186 | style = "bright-black"; 187 | }; 188 | git_state = { 189 | format = lib.concatStrings [ 190 | "\\(" 191 | "[$state( $progress_current/$progress_total)]" 192 | "($style)" 193 | "\\) " 194 | ]; 195 | style = "bright-black"; 196 | merge = "■"; 197 | }; 198 | git_status = { 199 | format = lib.concatStrings [ 200 | "[(" 201 | "$conflicted" 202 | "$untracked" 203 | "$modified" 204 | "$staged" 205 | "$stashed" 206 | "$renamed" 207 | "$deleted" 208 | ")](218)" 209 | "[( " 210 | "$ahead_behind" 211 | ")](cyan)" 212 | ]; 213 | ahead = "▲"; 214 | behind = "▼"; 215 | conflicted = ""; 216 | deleted = ""; 217 | diverged = "◆"; 218 | modified = "*"; 219 | renamed = ""; 220 | staged = ""; 221 | stashed = "≡"; 222 | untracked = ""; 223 | }; 224 | }; 225 | }; 226 | programs.zoxide.enable = true; 227 | xdg = { 228 | enable = true; 229 | configFile = { 230 | nvim = { 231 | source = config.lib.file.mkOutOfStoreSymlink "${config.home.sessionVariables.DOTFILES_HOME}/nvim"; 232 | recursive = true; 233 | }; 234 | }; 235 | }; 236 | } 237 | -------------------------------------------------------------------------------- /nix/modules/opencode.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | let 3 | colors = import ./colors.nix; 4 | in 5 | { 6 | xdg.configFile."AGENTS.md".source = 7 | config.lib.file.mkOutOfStoreSymlink "${config.home.sessionVariables.DOTFILES_HOME}/nix/files/AGENTS.md"; 8 | 9 | xdg.configFile."opencode/opencode.json".text = builtins.toJSON { 10 | "$schema" = "https://opencode.ai/config.json"; 11 | disabled_providers = [ 12 | "openai" 13 | "gemini" 14 | ]; 15 | "keybinds" = { 16 | # TODO: Swap newline and submit 17 | # https://github.com/sst/opencode/issues/653 18 | # "input_newline" = "enter"; 19 | # "input_submit" = "cmd+enter"; 20 | "messages_page_up" = "ctrl+u"; 21 | "messages_page_down" = "ctrl+d"; 22 | "messages_half_page_up" = "ctrl+alt+u"; 23 | "messages_half_page_down" = "ctrl+alt+d"; 24 | # FIXME: Stopped working recently 25 | # "messages_previous" = "ctrl+alt+up"; 26 | # "messages_next" = "ctrl+alt+down"; 27 | }; 28 | theme = "tmm"; 29 | }; 30 | 31 | xdg.configFile."opencode/themes/tmm.json".text = builtins.toJSON { 32 | "$schema" = "https://opencode.ai/theme.json"; 33 | defs = { 34 | darkBg = "${colors.dark.background}"; 35 | darkBgMenu = "#272726"; 36 | darkBgSidebar = "#000000"; 37 | darkBlue = "${colors.dark.blue}"; 38 | darkCursor = "${colors.dark.cursor}"; 39 | darkCyan = "${colors.dark.cyan}"; 40 | darkFg = "${colors.dark.foreground}"; 41 | darkFgBase = "#d1d1d1"; 42 | darkGreen = "${colors.dark.green}"; 43 | darkMagenta = "${colors.dark.magenta}"; 44 | darkOrange = "${colors.dark.orange}"; 45 | darkRed = "${colors.dark.red}"; 46 | darkUnimportant = "#757575"; 47 | darkYellow = "${colors.dark.yellow}"; 48 | 49 | lightBg = "${colors.bright.background}"; 50 | lightBgMenu = "#272726"; 51 | lightBgSidebar = "#000000"; 52 | lightBlue = "${colors.bright.blue}"; 53 | lightCursor = "${colors.dark.cursor}"; 54 | lightCyan = "${colors.bright.cyan}"; 55 | lightFg = "${colors.bright.foreground}"; 56 | lightFgBase = "#d1d1d1"; 57 | lightGreen = "${colors.bright.green}"; 58 | lightMagenta = "${colors.bright.magenta}"; 59 | lightOrange = "${colors.bright.orange}"; 60 | lightRed = "${colors.bright.red}"; 61 | lightUnimportant = "#757575"; 62 | lightYellow = "${colors.bright.yellow}"; 63 | }; 64 | theme = { 65 | primary = { 66 | dark = "darkOrange"; 67 | light = "lightOrange"; 68 | }; 69 | secondary = { 70 | dark = "darkBlue"; 71 | light = "lightBlue"; 72 | }; 73 | accent = { 74 | dark = "darkCursor"; 75 | light = "lightCursor"; 76 | }; 77 | error = { 78 | dark = "darkRed"; 79 | light = "lightRed"; 80 | }; 81 | warning = { 82 | dark = "darkOrange"; 83 | light = "lightOrange"; 84 | }; 85 | success = { 86 | dark = "darkGreen"; 87 | light = "lightGreen"; 88 | }; 89 | info = { 90 | dark = "darkCyan"; 91 | light = "lightCyan"; 92 | }; 93 | text = { 94 | dark = "darkFgBase"; 95 | light = "lightFgBase"; 96 | }; 97 | textMuted = { 98 | dark = "darkUnimportant"; 99 | light = "lightUnimportant"; 100 | }; 101 | background = { 102 | dark = "darkBgSidebar"; 103 | light = "lightBgSidebar"; 104 | }; 105 | backgroundPanel = { 106 | dark = "darkBg"; 107 | light = "lightBg"; 108 | }; 109 | backgroundElement = { 110 | dark = "darkBgMenu"; 111 | light = "lightBgMenu"; 112 | }; 113 | border = { 114 | dark = "darkUnimportant"; 115 | light = "lightUnimportant"; 116 | }; 117 | borderActive = { 118 | dark = "darkUnimportant"; 119 | light = "lightUnimportant"; 120 | }; 121 | borderSubtle = { 122 | dark = "darkBg"; 123 | light = "lightBg"; 124 | }; 125 | diffAdded = { 126 | dark = "darkFg"; 127 | light = "#1e725c"; 128 | }; 129 | diffRemoved = { 130 | dark = "darkFg"; 131 | light = "#c53b53"; 132 | }; 133 | diffContext = { 134 | dark = "#828bb8"; 135 | light = "#7086b5"; 136 | }; 137 | diffHunkHeader = { 138 | dark = "#828bb8"; 139 | light = "#7086b5"; 140 | }; 141 | diffHighlightAdded = { 142 | dark = "#1d572d"; 143 | light = "#4db380"; 144 | }; 145 | diffHighlightRemoved = { 146 | dark = "#7f302f"; 147 | light = "#f52a65"; 148 | }; 149 | diffAddedBg = { 150 | dark = "#14261d"; 151 | light = "#d5e5d5"; 152 | }; 153 | diffRemovedBg = { 154 | dark = "#301b1e"; 155 | light = "#f7d8db"; 156 | }; 157 | diffContextBg = { 158 | dark = "darkBg"; 159 | light = "lightBg"; 160 | }; 161 | diffLineNumber = { 162 | dark = "darkBg"; 163 | light = "lightBg"; 164 | }; 165 | diffAddedLineNumberBg = { 166 | dark = "#1f4428"; 167 | light = "#c5d5c5"; 168 | }; 169 | diffRemovedLineNumberBg = { 170 | dark = "#552527"; 171 | light = "#e7c8cb"; 172 | }; 173 | markdownText = { 174 | dark = "darkFgBase"; 175 | light = "lightFgBase"; 176 | }; 177 | markdownHeading = { 178 | dark = "darkUnimportant"; 179 | light = "lightUnimportant"; 180 | }; 181 | markdownLink = { 182 | dark = "darkFg"; 183 | light = "lightFg"; 184 | }; 185 | markdownLinkText = { 186 | dark = "darkCyan"; 187 | light = "lightCyan"; 188 | }; 189 | markdownCode = { 190 | dark = "darkGreen"; 191 | light = "lightGreen"; 192 | }; 193 | markdownBlockQuote = { 194 | dark = "darkYellow"; 195 | light = "lightYellow"; 196 | }; 197 | markdownEmph = { 198 | dark = "darkYellow"; 199 | light = "lightYellow"; 200 | }; 201 | markdownStrong = { 202 | dark = "darkOrange"; 203 | light = "lightOrange"; 204 | }; 205 | markdownHorizontalRule = { 206 | dark = "darkUnimportant"; 207 | light = "lightUnimportant"; 208 | }; 209 | markdownListItem = { 210 | dark = "darkFg"; 211 | light = "lightFg"; 212 | }; 213 | markdownListEnumeration = { 214 | dark = "darkCyan"; 215 | light = "lightCyan"; 216 | }; 217 | markdownImage = { 218 | dark = "darkFg"; 219 | light = "lightFg"; 220 | }; 221 | markdownImageText = { 222 | dark = "darkCyan"; 223 | light = "lightCyan"; 224 | }; 225 | markdownCodeBlock = { 226 | dark = "darkFgBase"; 227 | light = "lightFgBase"; 228 | }; 229 | syntaxComment = { 230 | dark = "darkUnimportant"; 231 | light = "lightUnimportant"; 232 | }; 233 | syntaxKeyword = { 234 | dark = "darkBlue"; 235 | light = "lightBlue"; 236 | }; 237 | syntaxFunction = { 238 | dark = "darkFg"; 239 | light = "lightFg"; 240 | }; 241 | syntaxVariable = { 242 | dark = "darkFgBase"; 243 | light = "lightRed"; 244 | }; 245 | syntaxString = { 246 | dark = "darkGreen"; 247 | light = "lightGreen"; 248 | }; 249 | syntaxNumber = { 250 | dark = "darkOrange"; 251 | light = "lightOrange"; 252 | }; 253 | syntaxType = { 254 | dark = "darkFgBase"; 255 | light = "lightFgBase"; 256 | }; 257 | syntaxOperator = { 258 | dark = "darkOrange"; 259 | light = "lightOrange"; 260 | }; 261 | syntaxPunctuation = { 262 | dark = "darkUnimportant"; 263 | light = "lightUnimportant"; 264 | }; 265 | }; 266 | }; 267 | } 268 | -------------------------------------------------------------------------------- /nvim/lua/util/init.lua: -------------------------------------------------------------------------------- 1 | local LazyUtil = require("lazy.core.util") 2 | 3 | local M = {} 4 | 5 | function M.is_win() 6 | return vim.uv.os_uname().sysname:find("Windows") ~= nil 7 | end 8 | 9 | ---@param name string 10 | function M.get_plugin(name) 11 | return require("lazy.core.config").spec.plugins[name] 12 | end 13 | 14 | ---@param plugin string 15 | function M.has(plugin) 16 | return M.get_plugin(plugin) ~= nil 17 | end 18 | 19 | ---@param fn fun() 20 | function M.on_very_lazy(fn) 21 | vim.api.nvim_create_autocmd("User", { 22 | pattern = "VeryLazy", 23 | callback = function() 24 | fn() 25 | end, 26 | }) 27 | end 28 | 29 | ---@param name string 30 | function M.opts(name) 31 | local plugin = M.get_plugin(name) 32 | if not plugin then 33 | return {} 34 | end 35 | local Plugin = require("lazy.core.plugin") 36 | return Plugin.values(plugin, "opts", false) 37 | end 38 | 39 | -- delay notifications till vim.notify was replaced or after 500ms 40 | function M.lazy_notify() 41 | local notifs = {} 42 | local function temp(...) 43 | table.insert(notifs, vim.F.pack_len(...)) 44 | end 45 | 46 | local orig = vim.notify 47 | vim.notify = temp 48 | 49 | local timer = vim.uv.new_timer() 50 | local check = assert(vim.uv.new_check()) 51 | 52 | local replay = function() 53 | timer:stop() 54 | check:stop() 55 | if vim.notify == temp then 56 | vim.notify = orig -- put back the original notify if needed 57 | end 58 | vim.schedule(function() 59 | ---@diagnostic disable-next-line: no-unknown 60 | for _, notif in ipairs(notifs) do 61 | vim.notify(vim.F.unpack_len(notif)) 62 | end 63 | end) 64 | end 65 | 66 | -- wait till vim.notify has been replaced 67 | check:start(function() 68 | if vim.notify ~= temp then 69 | replay() 70 | end 71 | end) 72 | -- or if it took more than 500ms, then something went wrong 73 | timer:start(500, 0, replay) 74 | end 75 | 76 | function M.is_loaded(name) 77 | local Config = require("lazy.core.config") 78 | return Config.plugins[name] and Config.plugins[name]._.loaded 79 | end 80 | 81 | ---@param name string 82 | ---@param fn fun(name:string) 83 | function M.on_load(name, fn) 84 | if M.is_loaded(name) then 85 | fn(name) 86 | else 87 | vim.api.nvim_create_autocmd("User", { 88 | pattern = "LazyLoad", 89 | callback = function(event) 90 | if event.data == name then 91 | fn(name) 92 | return true 93 | end 94 | end, 95 | }) 96 | end 97 | end 98 | 99 | --- Gets a path to a package in the Mason registry. 100 | --- Prefer this to `get_package`, since the package might not always be 101 | --- available yet and trigger errors. 102 | ---@param pkg string 103 | ---@param path? string 104 | ---@param opts? { warn?: boolean } 105 | function M.get_pkg_path(pkg, path, opts) 106 | pcall(require, "mason") -- make sure Mason is loaded. Will fail when generating docs 107 | local root = vim.env.MASON or (vim.fn.stdpath("data") .. "/mason") 108 | opts = opts or {} 109 | opts.warn = opts.warn == nil and true or opts.warn 110 | path = path or "" 111 | local ret = root .. "/packages/" .. pkg .. "/" .. path 112 | if opts.warn and not vim.loop.fs_stat(ret) and not require("lazy.core.config").headless() then 113 | M.warn( 114 | ("Mason package path not found for **%s**:\n- `%s`\nYou may need to force update the package."):format(pkg, path) 115 | ) 116 | end 117 | return ret 118 | end 119 | 120 | -- Wrapper around vim.keymap.set that will 121 | -- not create a keymap if a lazy key handler exists. 122 | -- It will also set `silent` to true by default. 123 | function M.safe_keymap_set(mode, lhs, rhs, opts) 124 | local keys = require("lazy.core.handler").handlers.keys 125 | local modes = type(mode) == "string" and { mode } or mode 126 | 127 | modes = vim.tbl_filter(function(m) 128 | return not (keys.have and keys:have(lhs, m)) 129 | end, modes) 130 | 131 | -- do not create the keymap if a lazy keys handler exists 132 | if #modes > 0 then 133 | opts = opts or {} 134 | opts.silent = opts.silent ~= false 135 | if opts.remap and not vim.g.vscode then 136 | ---@diagnostic disable-next-line: no-unknown 137 | opts.remap = nil 138 | end 139 | vim.keymap.set(modes, lhs, rhs, opts) 140 | end 141 | end 142 | 143 | M.CREATE_UNDO = vim.api.nvim_replace_termcodes("u", true, true, true) 144 | function M.create_undo() 145 | if vim.api.nvim_get_mode().mode == "i" then 146 | vim.api.nvim_feedkeys(M.CREATE_UNDO, "n", false) 147 | end 148 | end 149 | 150 | --- Override the default title for notifications. 151 | for _, level in ipairs({ "info", "warn", "error" }) do 152 | M[level] = function(msg, opts) 153 | opts = opts or {} 154 | opts.title = opts.title or "LazyVim" 155 | return LazyUtil[level](msg, opts) 156 | end 157 | end 158 | 159 | ---@param path string 160 | function M.norm(path) 161 | path = vim.fs.normalize(path) 162 | -- Special case for Windows drive letters 163 | -- vim.fs.normalize doesn't handle them correctly 164 | if path:sub(2, 2) == ":" then 165 | path = path:sub(1, 1):lower() .. path:sub(2) 166 | end 167 | return path 168 | end 169 | 170 | -- Fast implementation to check if a table is a list 171 | ---@param t table 172 | function M.is_list(t) 173 | local i = 0 174 | ---@diagnostic disable-next-line: no-unknown 175 | for _ in pairs(t) do 176 | i = i + 1 177 | if t[i] == nil then 178 | return false 179 | end 180 | end 181 | return true 182 | end 183 | 184 | local function can_merge(v) 185 | return type(v) == "table" and (vim.tbl_isempty(v) or not M.is_list(v)) 186 | end 187 | 188 | --- Merges the values similar to vim.tbl_deep_extend with the **force** behavior, 189 | --- but the values can be any type, in which case they override the values on the left. 190 | --- Values will me merged in-place in the first left-most table. If you want the result to be in 191 | --- a new table, then simply pass an empty table as the first argument `vim.merge({}, ...)` 192 | --- Supports clearing values by setting a key to `vim.NIL` 193 | ---@generic T 194 | ---@param ... T 195 | ---@return T 196 | function M.merge(...) 197 | local ret = select(1, ...) 198 | if ret == vim.NIL then 199 | ret = nil 200 | end 201 | for i = 2, select("#", ...) do 202 | local value = select(i, ...) 203 | if can_merge(ret) and can_merge(value) then 204 | for k, v in pairs(value) do 205 | ret[k] = M.merge(ret[k], v) 206 | end 207 | elseif value == vim.NIL then 208 | ret = nil 209 | elseif value ~= nil then 210 | ret = value 211 | end 212 | end 213 | return ret 214 | end 215 | 216 | function M.try(fn, opts) 217 | opts = type(opts) == "string" and { msg = opts } or opts or {} 218 | local msg = opts.msg 219 | -- error handler 220 | local error_handler = function(err) 221 | msg = (msg and (msg .. "\n\n") or "") .. err .. M.pretty_trace() 222 | if opts.on_error then 223 | opts.on_error(msg) 224 | else 225 | vim.schedule(function() 226 | M.error(msg) 227 | end) 228 | end 229 | return err 230 | end 231 | 232 | ---@type boolean, any 233 | local ok, result = xpcall(fn, error_handler) 234 | return ok and result or nil 235 | end 236 | 237 | ---@type table? 238 | M.kind_filter = { 239 | default = { 240 | "Class", 241 | "Constructor", 242 | "Enum", 243 | "Field", 244 | "Function", 245 | "Interface", 246 | "Method", 247 | "Module", 248 | "Namespace", 249 | "Package", 250 | "Property", 251 | "Struct", 252 | "Trait", 253 | }, 254 | markdown = false, 255 | help = false, 256 | -- you can specify a different filter for each filetype 257 | lua = { 258 | "Class", 259 | "Constructor", 260 | "Enum", 261 | "Field", 262 | "Function", 263 | "Interface", 264 | "Method", 265 | "Module", 266 | "Namespace", 267 | -- "Package", -- remove package since luals uses it for control flow structures 268 | "Property", 269 | "Struct", 270 | "Trait", 271 | }, 272 | } 273 | 274 | return M 275 | -------------------------------------------------------------------------------- /nvim/lua/config/keymaps.lua: -------------------------------------------------------------------------------- 1 | local map = vim.keymap.set 2 | local Snacks = require("snacks") 3 | 4 | -- Better up/down 5 | map({ "n", "x" }, "j", "v:count == 0 ? 'gj' : 'j'", { desc = "Down", expr = true, silent = true }) 6 | map({ "n", "x" }, "k", "v:count == 0 ? 'gk' : 'k'", { desc = "Up", expr = true, silent = true }) 7 | 8 | -- Move up/down five lines 9 | map({ "n", "v" }, "J", "5j", { desc = "Down 5 Lines" }) 10 | map({ "n", "v" }, "K", "5k", { desc = "Up 5 Lines" }) 11 | 12 | -- Move to window using the hjkl keys 13 | map("n", "", "h", { desc = "Go to Left Window", remap = true }) 14 | map("n", "", "j", { desc = "Go to Lower Window", remap = true }) 15 | map("n", "", "k", { desc = "Go to Upper Window", remap = true }) 16 | map("n", "", "l", { desc = "Go to Right Window", remap = true }) 17 | 18 | -- Resize window using arrow keys 19 | map("n", "", "resize +2", { desc = "Increase Window Height" }) 20 | map("n", "", "resize -2", { desc = "Decrease Window Height" }) 21 | map("n", "", "vertical resize -2", { desc = "Decrease Window Width" }) 22 | map("n", "", "vertical resize +2", { desc = "Increase Window Width" }) 23 | 24 | -- Move Lines 25 | map("n", "", "execute 'move .+' . v:count1==", { desc = "Move Down" }) 26 | map("n", "", "execute 'move .-' . (v:count1 + 1)==", { desc = "Move Up" }) 27 | map("i", "", "m .+1==gi", { desc = "Move Down" }) 28 | map("i", "", "m .-2==gi", { desc = "Move Up" }) 29 | map("v", "", ":execute \"'<,'>move '>+\" . v:count1gv=gv", { desc = "Move Down" }) 30 | map("v", "", ":execute \"'<,'>move '<-\" . (v:count1 + 1)gv=gv", { desc = "Move Up" }) 31 | 32 | -- Visually select text that was last edited/pasted (Vimcast#26) 33 | map("n", "gV", "`[v`]", { noremap = true, silent = true }) 34 | 35 | -- Replay macro 36 | map("n", "Q", "@q", { noremap = true, silent = true }) 37 | 38 | -- Disable arrow keys 39 | map("", "", "", { noremap = true, silent = true }) 40 | map("", "", "", { noremap = true, silent = true }) 41 | map("", "", "", { noremap = true, silent = true }) 42 | map("", "", "", { noremap = true, silent = true }) 43 | 44 | -- Don't yank on delete character 45 | map("n", "x", '"_x', { noremap = true, silent = true }) 46 | map("n", "X", '"_X', { noremap = true, silent = true }) 47 | map("v", "x", '"_x', { noremap = true, silent = true }) 48 | map("v", "X", '"_X', { noremap = true, silent = true }) 49 | 50 | -- buffers 51 | map("n", "", "bprevious", { desc = "Prev Buffer" }) 52 | map("n", "", "bnext", { desc = "Next Buffer" }) 53 | map("n", "[b", "bprevious", { desc = "Prev Buffer" }) 54 | map("n", "]b", "bnext", { desc = "Next Buffer" }) 55 | map("n", "bb", "e #", { desc = "Switch to Other Buffer" }) 56 | map("n", "`", "e #", { desc = "Switch to Other Buffer" }) 57 | map("n", "bd", function() 58 | Snacks.bufdelete() 59 | end, { desc = "Delete Buffer" }) 60 | map("n", "bo", function() 61 | Snacks.bufdelete.other() 62 | end, { desc = "Delete Other Buffers" }) 63 | map("n", "bD", ":bd", { desc = "Delete Buffer and Window" }) 64 | 65 | -- Clear search and stop snippet on escape 66 | map({ "i", "n", "s" }, "", function() 67 | vim.cmd("noh") 68 | -- TODO: when switch to blink 69 | -- LazyVim.cmp.actions.snippet_stop() 70 | return "" 71 | end, { expr = true, desc = "Escape and Clear hlsearch" }) 72 | 73 | -- Clear search, diff update and redraw 74 | -- taken from runtime/lua/_editor.lua 75 | map( 76 | "n", 77 | "ur", 78 | "nohlsearchdiffupdatenormal! ", 79 | { desc = "Redraw / Clear hlsearch / Diff Update" } 80 | ) 81 | 82 | -- https://github.com/mhinz/vim-galore#saner-behavior-of-n-and-n 83 | map("n", "n", "'Nn'[v:searchforward].'zv'", { expr = true, desc = "Next Search Result" }) 84 | map("x", "n", "'Nn'[v:searchforward]", { expr = true, desc = "Next Search Result" }) 85 | map("o", "n", "'Nn'[v:searchforward]", { expr = true, desc = "Next Search Result" }) 86 | map("n", "N", "'nN'[v:searchforward].'zv'", { expr = true, desc = "Prev Search Result" }) 87 | map("x", "N", "'nN'[v:searchforward]", { expr = true, desc = "Prev Search Result" }) 88 | map("o", "N", "'nN'[v:searchforward]", { expr = true, desc = "Prev Search Result" }) 89 | 90 | -- Add undo break-points 91 | map("i", ",", ",u") 92 | map("i", ".", ".u") 93 | map("i", ";", ";u") 94 | 95 | -- save file 96 | map({ "i", "x", "n", "s" }, "", "w", { desc = "Save File" }) 97 | 98 | --keywordprg 99 | map("n", "K", "norm! K", { desc = "Keywordprg" }) 100 | 101 | -- better indenting 102 | map("v", "<", "", ">gv") 104 | 105 | -- commenting 106 | map("n", "gco", "oVcxnormal gccfxa", { desc = "Add Comment Below" }) 107 | map("n", "gcO", "OVcxnormal gccfxa", { desc = "Add Comment Above" }) 108 | 109 | -- lazy 110 | map("n", "l", "Lazy", { desc = "Lazy" }) 111 | 112 | -- new file 113 | map("n", "fn", "enew", { desc = "New File" }) 114 | 115 | -- location list 116 | map("n", "xl", function() 117 | local success, err = pcall(vim.fn.getloclist(0, { winid = 0 }).winid ~= 0 and vim.cmd.lclose or vim.cmd.lopen) 118 | if not success and err then 119 | vim.notify(err, vim.log.levels.ERROR) 120 | end 121 | end, { desc = "Location List" }) 122 | 123 | -- quickfix list 124 | map("n", "xq", function() 125 | local success, err = pcall(vim.fn.getqflist({ winid = 0 }).winid ~= 0 and vim.cmd.cclose or vim.cmd.copen) 126 | if not success and err then 127 | vim.notify(err, vim.log.levels.ERROR) 128 | end 129 | end, { desc = "Quickfix List" }) 130 | 131 | map("n", "[q", vim.cmd.cprev, { desc = "Previous Quickfix" }) 132 | map("n", "]q", vim.cmd.cnext, { desc = "Next Quickfix" }) 133 | 134 | -- formatting 135 | map({ "n", "v" }, "cf", function() 136 | require("util.format").format({ force = true }) 137 | end, { desc = "Format" }) 138 | 139 | -- diagnostic 140 | local diagnostic_goto = function(next, severity) 141 | local go = next and vim.diagnostic.goto_next or vim.diagnostic.goto_prev 142 | severity = severity and vim.diagnostic.severity[severity] or nil 143 | return function() 144 | go({ severity = severity }) 145 | end 146 | end 147 | map("n", "cd", vim.diagnostic.open_float, { desc = "Line Diagnostics" }) 148 | map("n", "]d", diagnostic_goto(true), { desc = "Next Diagnostic" }) 149 | map("n", "[d", diagnostic_goto(false), { desc = "Prev Diagnostic" }) 150 | map("n", "]e", diagnostic_goto(true, "ERROR"), { desc = "Next Error" }) 151 | map("n", "[e", diagnostic_goto(false, "ERROR"), { desc = "Prev Error" }) 152 | map("n", "]w", diagnostic_goto(true, "WARN"), { desc = "Next Warning" }) 153 | map("n", "[w", diagnostic_goto(false, "WARN"), { desc = "Prev Warning" }) 154 | 155 | -- toggle options 156 | local format = require("util.format") 157 | format.snacks_toggle():map("uf") 158 | format.snacks_toggle(true):map("uF") 159 | Snacks.toggle.option("spell", { name = "Spelling" }):map("us") 160 | Snacks.toggle.option("wrap", { name = "Wrap" }):map("uw") 161 | Snacks.toggle.option("relativenumber", { name = "Relative Number" }):map("uL") 162 | Snacks.toggle.diagnostics():map("ud") 163 | Snacks.toggle.line_number():map("ul") 164 | Snacks.toggle 165 | .option("conceallevel", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2, name = "Conceal Level" }) 166 | :map("uc") 167 | Snacks.toggle.treesitter():map("uT") 168 | Snacks.toggle.option("background", { off = "light", on = "dark", name = "Dark Background" }):map("ub") 169 | Snacks.toggle.dim():map("uD") 170 | Snacks.toggle.indent():map("uG") 171 | if vim.lsp.inlay_hint then 172 | Snacks.toggle.inlay_hints():map("uh") 173 | end 174 | Snacks.toggle({ 175 | name = "Git Blame Line", 176 | get = function() 177 | return require("gitsigns.config").config.current_line_blame 178 | end, 179 | set = function() 180 | require("gitsigns").toggle_current_line_blame() 181 | end, 182 | }):map("ug") 183 | 184 | -- TODO: lazygit 185 | map("n", "gb", function() 186 | Snacks.git.blame_line() 187 | end, { desc = "Git Blame Line" }) 188 | map({ "n", "x" }, "gB", function() 189 | Snacks.gitbrowse() 190 | end, { desc = "Git Browse (open)" }) 191 | map({ "n", "x" }, "gY", function() 192 | Snacks.gitbrowse({ 193 | notify = false, 194 | open = function(url) 195 | vim.fn.setreg("+", url) 196 | end, 197 | }) 198 | end, { desc = "Git Browse (copy)" }) 199 | 200 | -- quit 201 | map("n", "qq", "qa", { desc = "Quit All" }) 202 | 203 | -- highlights under cursor 204 | map("n", "ui", vim.show_pos, { desc = "Inspect Pos" }) 205 | map("n", "uI", function() 206 | vim.treesitter.inspect_tree() 207 | vim.api.nvim_input("I") 208 | end, { desc = "Inspect Tree" }) 209 | 210 | -- windows 211 | map("n", "-", "s", { desc = "Split Window Below", remap = true }) 212 | map("n", "|", "v", { desc = "Split Window Right", remap = true }) 213 | map("n", "wd", "c", { desc = "Delete Window", remap = true }) 214 | Snacks.toggle.zoom():map("wm"):map("uZ") 215 | Snacks.toggle.zen():map("uz") 216 | 217 | -- tabs 218 | map("n", "l", "tablast", { desc = "Last Tab" }) 219 | map("n", "o", "tabonly", { desc = "Close Other Tabs" }) 220 | map("n", "f", "tabfirst", { desc = "First Tab" }) 221 | map("n", "", "tabnew", { desc = "New Tab" }) 222 | map("n", "]", "tabnext", { desc = "Next Tab" }) 223 | map("n", "d", "tabclose", { desc = "Close Tab" }) 224 | map("n", "[", "tabprevious", { desc = "Previous Tab" }) 225 | -------------------------------------------------------------------------------- /nix/files/AGENTS.md: -------------------------------------------------------------------------------- 1 | # Global Agent Guidelines 2 | 3 | This guide provides comprehensive instructions for agents working with team members on development projects. It synthesizes best practices, workflows, and critical guidelines to ensure effective and safe code contributions. 4 | 5 | ## Directory Structure and Purpose 6 | 7 | Your projects directory contains various git repositories for reference and development. Each subdirectory is typically an independent git repository used for finding code examples, implementations, and reference material. 8 | 9 | **CRITICAL**: Reference repositories are for REFERENCE ONLY. Do not modify git configurations or remotes in these repositories. 10 | 11 | ## Essential Workflow Rules 12 | 13 | ### 1. Working Directory Guidelines 14 | 15 | - **Read/explore**: Your main projects directory (reference repositories) 16 | - **Modify/experiment**: A dedicated workspace directory for isolated changes 17 | 18 | **ALWAYS check if a repository is already cloned locally before attempting to fetch from the web!** Use `ls` or check the directory structure to see if the project you need is already present before cloning it for reference. 19 | 20 | ### 2. Code Modification Workflow 21 | 22 | When working on code changes: 23 | 24 | 1. **Always work in a dedicated workspace directory** 25 | 2. **Use git worktrees** for creating isolated workspaces from existing repos 26 | 3. **Only use git clone if the repository doesn't exist locally** 27 | 4. **NEVER modify the remotes of existing reference repositories** 28 | 29 | #### Git Worktree Workflow (Preferred) 30 | 31 | Git worktrees allow multiple working directories from a single repository, perfect for parallel work: 32 | 33 | ```bash 34 | # First, check if the repo exists in your workspace 35 | cd ~/projects # or your designated workspace directory 36 | ls -la | grep REPO_NAME 37 | 38 | # If repo exists, create a worktree 39 | cd ~/projects/REPO_NAME 40 | git worktree add ../REPO_NAME-FEATURE-PURPOSE -b feature-branch 41 | 42 | # If repo doesn't exist, clone it first (check for your fork) 43 | gh repo view YOUR_USERNAME/REPO_NAME --web 2>/dev/null || echo "No fork found" 44 | 45 | # Clone from your fork if it exists 46 | git clone git@github.com:YOUR_USERNAME/REPO_NAME.git REPO_NAME 47 | cd REPO_NAME 48 | git remote add upstream git@github.com:ORIGINAL_ORG/REPO_NAME.git 49 | 50 | # Or clone from original if no fork 51 | git clone git@github.com:ORIGINAL_ORG/REPO_NAME.git REPO_NAME 52 | ``` 53 | 54 | #### Worktree Management 55 | 56 | ```bash 57 | # List all worktrees for a repo 58 | git worktree list 59 | 60 | # Create a new worktree for a feature 61 | git worktree add ../repo-optimization -b optimize-feature 62 | 63 | # Remove a worktree when done 64 | git worktree remove ../repo-optimization 65 | 66 | # Clean up worktree references 67 | git worktree prune 68 | ``` 69 | 70 | Use descriptive worktree names that indicate purpose: 71 | - `gitchat-add-message-notification-queue` 72 | - `repo-name-issue-123` 73 | - `project-feature-description` 74 | 75 | **Benefits of worktrees**: 76 | - Share the same git history and objects (saves disk space) 77 | - Switch between features instantly without stashing 78 | - Keep multiple experiments running in parallel 79 | - Easier cleanup - just remove the worktree directory 80 | 81 | #### Workspace Maintenance 82 | 83 | **Clean up build artifacts** when disk space is needed: 84 | ```bash 85 | # For Rust projects 86 | cd ~/projects/repo-name 87 | pnpm clean # check package.json for specific clean script 88 | rm -rf dist/ # can delete dist or build output dir directly 89 | ``` 90 | 91 | **When to clean up workspaces**: 92 | - After PR has been merged 93 | - When changes have been abandoned 94 | - Before removing a worktree 95 | 96 | ```bash 97 | # Clean and remove a worktree 98 | cd ~/projects/repo-name 99 | git worktree remove ../repo-name-issue-123 100 | ``` 101 | 102 | ### 3. Git Workflow 103 | 104 | When working on code: 105 | 106 | 1. Create feature branches for your work 107 | 2. Commit changes with clear messages 108 | 3. Use descriptive branch names: `name/fix-something`, `name/add-feature`, `name/make-biome-happy-123456` 109 | 110 | ### 4. GitHub CLI (gh) Usage 111 | 112 | The `gh` CLI tool is available for exploring GitHub repositories and understanding code context. 113 | 114 | #### Allowed Operations 115 | 116 | ```bash 117 | # Repository exploration 118 | gh repo view owner/repo 119 | gh repo clone owner/repo # For initial clones 120 | 121 | # Issues and PRs 122 | gh issue list --repo owner/repo 123 | gh issue view 123 --repo owner/repo 124 | gh pr list --repo owner/repo 125 | gh pr view 456 --repo owner/repo 126 | gh pr diff 456 --repo owner/repo 127 | gh pr checkout 456 # To examine PR branches locally 128 | 129 | # API queries 130 | gh api repos/owner/repo/pulls/123/comments 131 | gh api repos/owner/repo/issues/123/comments 132 | 133 | # Search operations 134 | gh search issues "query" --repo owner/repo 135 | gh search prs "query" --repo owner/repo 136 | 137 | # Status and authentication 138 | gh auth status 139 | gh status 140 | 141 | # Releases and workflows (read-only) 142 | gh release list --repo owner/repo 143 | gh release view v1.0.0 --repo owner/repo 144 | gh workflow list --repo owner/repo 145 | gh run list --workflow=ci.yml --repo owner/repo 146 | ``` 147 | #### Common Use Cases 148 | 149 | 1. **Examining PR discussions**: 150 | ```bash 151 | gh pr view 123 --comments 152 | gh api repos/wevm/wagmi/pulls/123/comments | jq '.[].body' 153 | ``` 154 | 155 | 2. **Finding related issues**: 156 | ```bash 157 | gh search issues "performance" --repo wevm/wagmi --state open 158 | ``` 159 | 160 | 3. **Checking PR changes**: 161 | ```bash 162 | gh pr diff 456 --repo owner/repo 163 | ``` 164 | 165 | ## Task-Specific Guidelines 166 | 167 | ### Writing Code 168 | 169 | 1. **Follow existing patterns** - Study how the project structures similar code 170 | 2. **Check dependencies first** - Never assume a library is available 171 | 3. **Maintain consistency** - Use the project's naming conventions and style 172 | 4. **Security first** - Never expose secrets or keys in code 173 | 174 | ### Commit Message Best Practices 175 | 176 | Writing excellent commit messages is crucial - they become the permanent record of why changes were made. 177 | 178 | #### Commit Title Format 179 | 180 | Use semantic commit format with a clear, specific title: 181 | - `feat:` - New features 182 | - `fix:` - Bug fixes 183 | - `perf:` - Performance improvements 184 | - `chore:` - Maintenance tasks 185 | - `docs:` - Documentation 186 | - `test:` - Test changes 187 | - `refactor:` - Code restructuring 188 | - `ci:` - CI/CD changes 189 | 190 | **Title guidelines**: 191 | - Be specific: `perf: add specialized multiplication for 8 limbs` not `perf: optimize mul` 192 | - Use imperative mood: "add" not "adds" or "added" 193 | - Keep under 50 characters when possible 194 | - Don't end with a period 195 | 196 | #### Commit Description (Body) 197 | 198 | The commit body is where you provide context and details about a specific commit. **This is different from PR descriptions**. Many commits do not have 199 | descriptions. 200 | 201 | **When to add a body**: 202 | - Breaking changes (note the impact) 203 | - Non-obvious changes (explain why, not what) 204 | - When a commit is very complex or cannot be split up, and is not the only distinct change in the branch or PR 205 | 206 | **Format**: 207 | ``` 208 | 209 | <blank line> 210 | <body> 211 | <blank line> 212 | <footer> 213 | ``` 214 | 215 | #### Examples 216 | 217 | **Performance improvement** (body required): 218 | ``` 219 | perf: add specialized multiplication for 8 limbs 220 | 221 | Benchmarks show ~2.7x speedup for 512-bit operations: 222 | - Before: ~41ns 223 | - After: ~15ns 224 | 225 | This follows the existing pattern of specialized implementations 226 | for sizes 1-4, extending to size 8 which is commonly used. 227 | ``` 228 | 229 | **Bug fix** (explain the issue): 230 | ``` 231 | fix: correct carry propagation in uint addition 232 | 233 | The carry bit was not properly propagated when the first limb 234 | overflowed but subsequent limbs were at MAX-1. This caused 235 | incorrect results for specific input patterns. 236 | 237 | Added test case that reproduces the issue. 238 | ``` 239 | 240 | **Simple feature** (title often sufficient): 241 | ``` 242 | feat: add From<u128> implementation for Uint<256> 243 | ``` 244 | 245 | **Complex change** (needs explanation): 246 | ``` 247 | refactor: split trie updates into parallel work queues 248 | 249 | Previous implementation processed all trie updates sequentially, 250 | creating a bottleneck during state root calculation. This change: 251 | 252 | - Partitions updates by key prefix 253 | - Processes non-conflicting updates in parallel 254 | - Falls back to sequential for conflicts 255 | - Maintains deterministic ordering 256 | 257 | Reduces state root time from 120ms to 35ms on 16-core machines. 258 | ``` 259 | 260 | #### What NOT to Do 261 | 262 | - Don't write generic descriptions: "Update code", "Fix bug" 263 | - Don't use many bullet points unless listing multiple distinct changes 264 | - Don't make up metrics without measurements 265 | - Don't write essays - be concise but complete 266 | 267 | #### Key Principles 268 | 269 | 1. **The title should make sense in a changelog** 270 | 2. **The body should explain to a future developer why this change was necessary** 271 | 3. **Include concrete measurements for performance claims** 272 | 4. **Reference issues when fixing bugs**: `Fixes #12345` 273 | 5. **Let improvements stand on their own merit** - don't invent generic justifications 274 | 6. **Match detail to complexity** - Simple changes need simple descriptions 275 | 276 | ## Critical Reminders 277 | 278 | ### DO NOT 279 | 280 | - Start work if git has uncommitted changes 281 | - Make up performance numbers or generic justifications for changes 282 | - Commit secrets, API keys, or sensitive information 283 | - Break existing functionality without proper testing 284 | - Ignore linting or formatting tools when available 285 | - Never use emojis unless asked to do so 286 | 287 | ### ALWAYS 288 | 289 | - Ask before committing code 290 | - Follow project patterns and conventions 291 | - Read relevant documentation before starting tasks 292 | - Test changes thoroughly before committing 293 | - Use version control best practices 294 | - Consider security implications of changes 295 | 296 | ## Project-Specific Overrides 297 | 298 | This file provides universal guidelines. Projects may have their own AGENTS.md file that overrides or extends these guidelines with project-specific requirements. 299 | 300 | This file should be updated when major architectural patterns change across multiple projects. 301 | -------------------------------------------------------------------------------- /nvim/lua/plugins.lua: -------------------------------------------------------------------------------- 1 | local icons = require("config").icons 2 | 3 | return { 4 | -- amp.nvim (https://github.com/sourcegraph/amp.nvim) 5 | { 6 | "sourcegraph/amp.nvim", 7 | branch = "main", 8 | lazy = false, 9 | opts = { auto_start = true, log_level = "info" }, 10 | }, 11 | 12 | -- blink.cmp (https://github.com/saghen/blink.cmp) 13 | { 14 | "saghen/blink.cmp", 15 | event = { "InsertEnter", "CmdlineEnter" }, 16 | version = not vim.g.lazyvim_blink_main and "*", 17 | build = vim.g.lazyvim_blink_main and "cargo build --release", 18 | opts_extend = { 19 | "sources.completion.enabled_providers", 20 | "sources.compat", 21 | "sources.default", 22 | }, 23 | dependencies = { 24 | "rafamadriz/friendly-snippets", 25 | }, 26 | opts = { 27 | appearance = { 28 | -- sets the fallback highlight groups to nvim-cmp's highlight groups 29 | -- useful for when your theme doesn't support blink.cmp 30 | -- will be removed in a future release, assuming themes add support 31 | use_nvim_cmp_as_default = false, 32 | -- set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' 33 | -- adjusts spacing to ensure icons are aligned 34 | nerd_font_variant = "mono", 35 | }, 36 | cmdline = { 37 | enabled = false, 38 | keymap = { 39 | preset = "cmdline", 40 | ["<Right>"] = false, 41 | ["<Left>"] = false, 42 | }, 43 | completion = { 44 | list = { selection = { preselect = false } }, 45 | menu = { 46 | auto_show = function(_ctx) 47 | return vim.fn.getcmdtype() == ":" 48 | end, 49 | }, 50 | ghost_text = { enabled = true }, 51 | }, 52 | }, 53 | completion = { 54 | accept = { 55 | -- experimental auto-brackets support 56 | auto_brackets = { 57 | enabled = true, 58 | }, 59 | }, 60 | documentation = { 61 | auto_show = true, 62 | auto_show_delay_ms = 200, 63 | }, 64 | ghost_text = { 65 | enabled = vim.g.ai_cmp, 66 | }, 67 | menu = { 68 | draw = { 69 | treesitter = { "lsp" }, 70 | }, 71 | }, 72 | }, 73 | keymap = { 74 | ["<C-space>"] = { "show", "show_documentation", "hide_documentation" }, 75 | ["<CR>"] = { "accept", "fallback" }, 76 | ["<C-k>"] = { "select_prev", "fallback" }, 77 | ["<C-j>"] = { "select_next", "fallback" }, 78 | ["<C-b>"] = { "scroll_documentation_up", "fallback" }, 79 | ["<C-f>"] = { "scroll_documentation_down", "fallback" }, 80 | ["<Tab>"] = { "snippet_forward", "fallback" }, 81 | ["<S-Tab>"] = { "snippet_backward", "fallback" }, 82 | }, 83 | snippets = { 84 | expand = function(snippet, _) 85 | return require("util.cmp").expand(snippet) 86 | end, 87 | }, 88 | -- experimental signature help support 89 | signature = { enabled = true }, 90 | sources = { 91 | -- adding any nvim-cmp sources here will enable them 92 | -- with blink.compat 93 | compat = {}, 94 | default = { "lsp", "path", "snippets", "buffer" }, 95 | }, 96 | }, 97 | config = function(_, opts) 98 | -- setup compat sources 99 | local enabled = opts.sources.default 100 | for _, source in ipairs(opts.sources.compat or {}) do 101 | opts.sources.providers[source] = vim.tbl_deep_extend( 102 | "force", 103 | { name = source, module = "blink.compat.source" }, 104 | opts.sources.providers[source] or {} 105 | ) 106 | if type(enabled) == "table" and not vim.tbl_contains(enabled, source) then 107 | table.insert(enabled, source) 108 | end 109 | end 110 | 111 | -- Unset custom prop to pass blink.cmp validation 112 | opts.sources.compat = nil 113 | 114 | -- check if we need to override symbol kinds 115 | for _, provider in pairs(opts.sources.providers or {}) do 116 | if provider.kind then 117 | local CompletionItemKind = require("blink.cmp.types").CompletionItemKind 118 | local kind_idx = #CompletionItemKind + 1 119 | 120 | CompletionItemKind[kind_idx] = provider.kind 121 | ---@diagnostic disable-next-line: no-unknown 122 | CompletionItemKind[provider.kind] = kind_idx 123 | 124 | local transform_items = provider.transform_items 125 | provider.transform_items = function(ctx, items) 126 | items = transform_items and transform_items(ctx, items) or items 127 | for _, item in ipairs(items) do 128 | item.kind = kind_idx or item.kind 129 | item.kind_icon = icons.kinds[item.kind_name] or item.kind_icon or nil 130 | end 131 | return items 132 | end 133 | 134 | -- Unset custom prop to pass blink.cmp validation 135 | provider.kind = nil 136 | end 137 | end 138 | 139 | opts.appearance.kind_icons = vim.tbl_extend("force", opts.appearance.kind_icons or {}, icons.kinds) 140 | 141 | require("blink.cmp").setup(opts) 142 | end, 143 | }, 144 | 145 | -- conform.nvim (https://github.com/stevearc/conform.nvim) 146 | { 147 | "stevearc/conform.nvim", 148 | dependencies = { "mason.nvim" }, 149 | lazy = true, 150 | cmd = "ConformInfo", 151 | keys = { 152 | { 153 | "<leader>cF", 154 | function() 155 | require("conform").format({ formatters = { "injected" }, timeout_ms = 3000 }) 156 | end, 157 | mode = { "n", "v" }, 158 | desc = "Format Injected Langs", 159 | }, 160 | }, 161 | init = function() 162 | -- Install the conform formatter on VeryLazy 163 | require("util.init").on_very_lazy(function() 164 | require("util.format").register({ 165 | name = "conform.nvim", 166 | priority = 100, 167 | primary = true, 168 | format = function(buf) 169 | require("conform").format({ bufnr = buf }) 170 | end, 171 | sources = function(buf) 172 | local ret = require("conform").list_formatters(buf) 173 | return vim.tbl_map(function(v) 174 | return v.name 175 | end, ret) 176 | end, 177 | }) 178 | end) 179 | end, 180 | opts = function() 181 | local opts = { 182 | default_format_opts = { 183 | timeout_ms = 3000, 184 | async = false, -- not recommended to change 185 | quiet = false, -- not recommended to change 186 | lsp_format = "fallback", -- not recommended to change 187 | }, 188 | formatters_by_ft = { 189 | css = { "biome-check" }, 190 | eex = { "mix" }, 191 | elixir = { "mix" }, 192 | fish = { "fish_indent" }, 193 | heex = { "mix" }, 194 | json = { "biome-check" }, 195 | jsonc = { "biome-check" }, 196 | lua = { "stylua" }, 197 | nix = { "nixfmt" }, 198 | sh = { "shfmt" }, 199 | svelte = { "biome-check" }, 200 | typescript = { "biome-check" }, 201 | typescriptreact = { "biome-check" }, 202 | vue = { "biome-check" }, 203 | }, 204 | -- The options you set here will be merged with the builtin formatters. 205 | -- You can also define any custom formatters here. 206 | formatters = { 207 | biome = { require_cwd = true }, 208 | injected = { options = { ignore_errors = true } }, 209 | -- # Example of using dprint only when a dprint.json file is present 210 | -- dprint = { 211 | -- condition = function(ctx) 212 | -- return vim.fs.find({ "dprint.json" }, { path = ctx.filename, upward = true })[1] 213 | -- end, 214 | -- }, 215 | -- 216 | -- # Example of using shfmt with extra args 217 | -- shfmt = { 218 | -- prepend_args = { "-i", "2", "-ci" }, 219 | -- }, 220 | }, 221 | } 222 | return opts 223 | end, 224 | }, 225 | 226 | -- gitsigns.nvim (https://github.com/lewis6991/gitsigns.nvim) 227 | { 228 | "lewis6991/gitsigns.nvim", 229 | event = { "BufReadPre", "BufNewFile" }, 230 | opts = { 231 | current_line_blame_opts = { delay = 500 }, 232 | on_attach = function(buffer) 233 | local gs = package.loaded.gitsigns 234 | 235 | local function map(mode, l, r, desc) 236 | vim.keymap.set(mode, l, r, { buffer = buffer, desc = desc }) 237 | end 238 | 239 | -- stylua: ignore start 240 | map("n", "]h", function() 241 | if vim.wo.diff then 242 | vim.cmd.normal({ "]c", bang = true }) 243 | else 244 | gs.nav_hunk("next") 245 | end 246 | end, "Next Hunk") 247 | map("n", "[h", function() 248 | if vim.wo.diff then 249 | vim.cmd.normal({ "[c", bang = true }) 250 | else 251 | gs.nav_hunk("prev") 252 | end 253 | end, "Prev Hunk") 254 | map("n", "]H", function() gs.nav_hunk("last") end, "Last Hunk") 255 | map("n", "[H", function() gs.nav_hunk("first") end, "First Hunk") 256 | map({ "n", "v" }, "<leader>ghs", ":Gitsigns stage_hunk<CR>", "Stage Hunk") 257 | map({ "n", "v" }, "<leader>ghr", ":Gitsigns reset_hunk<CR>", "Reset Hunk") 258 | map("n", "<leader>ghS", gs.stage_buffer, "Stage Buffer") 259 | map("n", "<leader>ghu", gs.undo_stage_hunk, "Undo Stage Hunk") 260 | map("n", "<leader>ghR", gs.reset_buffer, "Reset Buffer") 261 | map("n", "<leader>ghp", gs.preview_hunk_inline, "Preview Hunk Inline") 262 | map("n", "<leader>ghb", function() gs.blame_line({ full = true }) end, "Blame Line") 263 | map("n", "<leader>ghB", function() gs.blame() end, "Blame Buffer") 264 | map("n", "<leader>ghd", gs.diffthis, "Diff This") 265 | map("n", "<leader>ghD", function() gs.diffthis("~") end, "Diff This ~") 266 | map({ "o", "x" }, "ih", ":<C-U>Gitsigns select_hunk<CR>", "GitSigns Select Hunk") 267 | end, 268 | signs = { 269 | add = { text = "▎" }, 270 | change = { text = "▎" }, 271 | changedelete = { text = "▎" }, 272 | delete = { text = "" }, 273 | topdelete = { text = "" }, 274 | untracked = { text = "▎" }, 275 | }, 276 | signs_staged = { 277 | add = { text = "▎" }, 278 | change = { text = "▎" }, 279 | changedelete = { text = "▎" }, 280 | delete = { text = "" }, 281 | topdelete = { text = "" }, 282 | }, 283 | }, 284 | }, 285 | 286 | -- flash.nvim (https://github.com/folke/flash.nvim) 287 | { 288 | "folke/flash.nvim", 289 | event = "VeryLazy", 290 | opts = {}, 291 | -- stylua: ignore 292 | keys = { 293 | { "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" }, 294 | { "S", mode = { "n", "o", "x" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" }, 295 | { "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" }, 296 | { "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" }, 297 | { "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" }, 298 | }, 299 | }, 300 | 301 | -- grug-far.nvim (https://github.com/MagicDuck/grug-far.nvim) 302 | { 303 | "MagicDuck/grug-far.nvim", 304 | opts = { headerMaxWidth = 80 }, 305 | cmd = "GrugFar", 306 | keys = { 307 | { 308 | "<leader>sr", 309 | function() 310 | local grug = require("grug-far") 311 | local ext = vim.bo.buftype == "" and vim.fn.expand("%:e") 312 | grug.open({ 313 | transient = true, 314 | prefills = { 315 | filesFilter = ext and ext ~= "" and "*." .. ext or nil, 316 | }, 317 | }) 318 | end, 319 | mode = { "n", "v" }, 320 | desc = "Search and Replace", 321 | }, 322 | }, 323 | config = function(_, opts) 324 | require("grug-far").setup(opts) 325 | 326 | -- add highlight group for grug-far 327 | vim.api.nvim_create_autocmd("FileType", { 328 | group = vim.api.nvim_create_augroup("grug_far_hl", { clear = true }), 329 | pattern = { "grug-far" }, 330 | callback = function() 331 | vim.wo.winhighlight = "Normal:GrugFarNormal" 332 | end, 333 | }) 334 | end, 335 | }, 336 | 337 | -- lualine.nvim (https://github.com/nvim-lualine/lualine.nvim) 338 | { 339 | "nvim-lualine/lualine.nvim", 340 | event = "VeryLazy", 341 | init = function() 342 | vim.g.lualine_laststatus = vim.o.laststatus 343 | if vim.fn.argc(-1) > 0 then 344 | -- set an empty statusline till lualine loads 345 | vim.o.statusline = " " 346 | else 347 | -- hide the statusline on the starter page 348 | vim.o.laststatus = 0 349 | end 350 | end, 351 | config = function() 352 | local conditions = { 353 | buffer_not_empty = function() 354 | return vim.fn.empty(vim.fn.expand("%:t")) ~= 1 355 | end, 356 | } 357 | 358 | require("lualine").setup({ 359 | extensions = { "lazy", "fzf" }, 360 | options = { 361 | always_divide_middle = true, 362 | component_separators = "", 363 | disabled_filetypes = { 364 | "gitsigns-blame", 365 | }, 366 | globalstatus = false, 367 | icons_enabled = true, 368 | section_separators = "", 369 | }, 370 | sections = { 371 | lualine_a = {}, 372 | lualine_b = {}, 373 | lualine_c = { 374 | { "mode", color = "MsgArea" }, 375 | { 376 | "filename", 377 | color = "MsgArea", 378 | cond = conditions.buffer_not_empty, 379 | symbols = { modified = "", readonly = "", unnamed = "" }, 380 | }, 381 | { "branch", color = "MsgArea", icon = icons.git.Branch }, 382 | { 383 | "diff", 384 | color = "MsgArea", 385 | symbols = { 386 | added = icons.git.Added, 387 | modified = icons.git.Modified, 388 | removed = icons.git.Removed, 389 | }, 390 | source = function() 391 | local gitsigns = vim.b.gitsigns_status_dict 392 | if gitsigns then 393 | return { 394 | added = gitsigns.added, 395 | modified = gitsigns.changed, 396 | removed = gitsigns.removed, 397 | } 398 | end 399 | end, 400 | }, 401 | }, 402 | lualine_x = { 403 | { 404 | "diagnostics", 405 | color = "MsgArea", 406 | sources = { "nvim_diagnostic" }, 407 | symbols = { 408 | error = icons.diagnostics.Error, 409 | hint = icons.diagnostics.Hint, 410 | info = icons.diagnostics.Info, 411 | warn = icons.diagnostics.Warn, 412 | }, 413 | }, 414 | { 415 | require("noice").api.status.search.get, 416 | cond = require("noice").api.status.search.has, 417 | color = "MsgArea", 418 | }, 419 | -- stylua: ignore 420 | { require("lazy.status").updates, cond = require("lazy.status").has_updates, color = "MsgArea" }, 421 | { "progress", color = "MsgArea" }, 422 | { "location", color = "MsgArea" }, 423 | }, 424 | lualine_y = {}, 425 | lualine_z = {}, 426 | }, 427 | inactive_sections = { 428 | lualine_a = {}, 429 | lualine_b = {}, 430 | lualine_c = { 431 | { "filename", color = "StatusLine" }, 432 | }, 433 | lualine_x = {}, 434 | lualine_y = {}, 435 | lualine_z = {}, 436 | }, 437 | tabline = {}, 438 | }) 439 | end, 440 | }, 441 | 442 | -- mason.nvim (https://github.com/mason-org/mason.nvim) 443 | { 444 | "mason-org/mason.nvim", 445 | version = "^1.0.0", 446 | cmd = "Mason", 447 | keys = { 448 | { "<leader>cm", "<cmd>Mason<cr>", desc = "Mason" }, 449 | }, 450 | build = ":MasonUpdate", 451 | opts_extend = { "ensure_installed" }, 452 | opts = { 453 | ensure_installed = { 454 | "biome", 455 | "shfmt", 456 | "stylua", 457 | "svelte-language-server", 458 | "tailwindcss-language-server", 459 | "vue-language-server", 460 | }, 461 | }, 462 | config = function(_, opts) 463 | require("mason").setup(opts) 464 | local mr = require("mason-registry") 465 | 466 | mr:on("package:install:success", function() 467 | vim.defer_fn(function() 468 | -- trigger FileType event to possibly load this newly installed LSP server 469 | require("lazy.core.handler.event").trigger({ 470 | event = "FileType", 471 | buf = vim.api.nvim_get_current_buf(), 472 | }) 473 | end, 100) 474 | end) 475 | 476 | mr.refresh(function() 477 | for _, tool in ipairs(opts.ensure_installed) do 478 | local p = mr.get_package(tool) 479 | if not p:is_installed() then 480 | p:install() 481 | end 482 | end 483 | end) 484 | end, 485 | }, 486 | 487 | -- mini.nvim (https://github.com/nvim-mini/mini.nvim) 488 | { 489 | "nvim-mini/mini.icons", 490 | event = "VeryLazy", 491 | init = function() 492 | package.preload["nvim-web-devicons"] = function() 493 | require("mini.icons").mock_nvim_web_devicons() 494 | return package.loaded["nvim-web-devicons"] 495 | end 496 | end, 497 | }, 498 | { 499 | "nvim-mini/mini.pairs", 500 | event = "VeryLazy", 501 | opts = { 502 | -- TODO: https://github.com/LazyVim/LazyVim/blob/3dbace941ee935c89c73fd774267043d12f57fe2/lua/lazyvim/util/mini.lua#L123 503 | modes = { insert = true, command = true, terminal = false }, 504 | }, 505 | }, 506 | { 507 | "nvim-mini/mini.surround", 508 | event = "VeryLazy", 509 | opts = { 510 | mappings = { 511 | add = "gsa", -- Add surrounding in Normal and Visual modes 512 | delete = "gsd", -- Delete surrounding 513 | find = "gsf", -- Find surrounding (to the right) 514 | find_left = "gsF", -- Find surrounding (to the left) 515 | highlight = "gsh", -- Highlight surrounding 516 | replace = "gsr", -- Replace surrounding 517 | }, 518 | }, 519 | }, 520 | 521 | -- noice.nvim (https://github.com/folke/noice.nvim) 522 | { 523 | "folke/noice.nvim", 524 | event = "VeryLazy", 525 | dependencies = { 526 | "MunifTanjim/nui.nvim", 527 | }, 528 | -- stylua: ignore 529 | keys = { 530 | { "<leader>sn", "", desc = "+noice"}, 531 | { "<S-Enter>", function() require("noice").redirect(vim.fn.getcmdline()) end, mode = "c", desc = "Redirect Cmdline" }, 532 | { "<leader>snl", function() require("noice").cmd("last") end, desc = "Noice Last Message" }, 533 | { "<leader>snh", function() require("noice").cmd("history") end, desc = "Noice History" }, 534 | { "<leader>sna", function() require("noice").cmd("all") end, desc = "Noice All" }, 535 | { "<leader>snt", function() require("noice").cmd("pick") end, desc = "Noice Picker (FzfLua)" }, 536 | { "<c-f>", function() if not require("noice.lsp").scroll(4) then return "<c-f>" end end, silent = true, expr = true, desc = "Scroll Forward", mode = {"i", "n", "s"} }, 537 | { "<c-b>", function() if not require("noice.lsp").scroll(-4) then return "<c-b>" end end, silent = true, expr = true, desc = "Scroll Backward", mode = {"i", "n", "s"}}, 538 | }, 539 | opts = { 540 | cmdline = { 541 | format = { 542 | cmdline = { icon = icons.misc.PromptPrefix }, 543 | search_down = { icon = " " }, 544 | search_up = { icon = " " }, 545 | }, 546 | }, 547 | lsp = { 548 | -- override markdown rendering so plugins use **Treesitter** 549 | override = { 550 | ["vim.lsp.util.convert_input_to_markdown_lines"] = true, 551 | ["vim.lsp.util.stylize_markdown"] = true, 552 | }, 553 | }, 554 | presets = { 555 | bottom_search = true, 556 | command_palette = true, 557 | long_message_to_split = true, 558 | lsp_doc_border = false, 559 | }, 560 | routes = { 561 | { 562 | filter = { 563 | event = "msg_show", 564 | any = { 565 | { find = "%d+L, %d+B" }, 566 | { find = "; after #%d+" }, 567 | { find = "; before #%d+" }, 568 | { find = "%d fewer lines" }, 569 | { find = "%d more lines" }, 570 | }, 571 | }, 572 | view = "mini", 573 | opts = { skip = true }, 574 | }, 575 | }, 576 | }, 577 | config = function(_, opts) 578 | -- HACK: noice shows messages from before it was enabled, 579 | -- but this is not ideal when Lazy is installing plugins, 580 | -- so clear the messages in this case. 581 | if vim.o.filetype == "lazy" then 582 | vim.cmd([[messages clear]]) 583 | end 584 | require("noice").setup(opts) 585 | end, 586 | }, 587 | 588 | -- nvim-lspconfig (https://github.com/neovim/nvim-lspconfig) 589 | { 590 | "neovim/nvim-lspconfig", 591 | event = { "BufReadPre", "BufNewFile", "BufWritePre" }, 592 | dependencies = { 593 | -- https://github.com/mason-org/mason.nvim 594 | { "mason-org/mason.nvim", version = "^1.0.0" }, 595 | { "mason-org/mason-lspconfig.nvim", version = "^1.0.0", config = function() end }, 596 | }, 597 | opts = function() 598 | local ret = { 599 | diagnostics = { 600 | underline = true, 601 | update_in_insert = false, 602 | virtual_text = { 603 | spacing = 4, 604 | source = "if_many", 605 | prefix = "●", 606 | -- this will set set the prefix to a function that returns the diagnostics icon based on the severity 607 | -- this only works on a recent 0.10.0 build. Will be set to "●" when not supported 608 | -- prefix = "icons", 609 | }, 610 | severity_sort = true, 611 | signs = { 612 | text = { 613 | [vim.diagnostic.severity.ERROR] = icons.diagnostics.Error, 614 | [vim.diagnostic.severity.HINT] = icons.diagnostics.Hint, 615 | [vim.diagnostic.severity.INFO] = icons.diagnostics.Info, 616 | [vim.diagnostic.severity.WARN] = icons.diagnostics.Warn, 617 | }, 618 | }, 619 | }, 620 | -- Enable this to enable the builtin LSP inlay hints on Neovim >= 0.10.0 621 | -- Be aware that you also will need to properly configure your LSP server to 622 | -- provide the inlay hints. 623 | inlay_hints = { 624 | enabled = false, 625 | exclude = { "vue" }, -- filetypes for which you don't want to enable inlay hints 626 | }, 627 | -- Enable this to enable the builtin LSP code lenses on Neovim >= 0.10.0 628 | -- Be aware that you also will need to properly configure your LSP server to 629 | -- provide the code lenses. 630 | codelens = { 631 | enabled = false, 632 | }, 633 | -- add any global capabilities here 634 | capabilities = { 635 | workspace = { 636 | fileOperations = { 637 | didRename = true, 638 | willRename = true, 639 | }, 640 | }, 641 | }, 642 | -- options for vim.lsp.buf.format 643 | -- `bufnr` and `filter` is handled by the LazyVim formatter, 644 | -- but can be also overridden when specified 645 | format = { 646 | formatting_options = nil, 647 | timeout_ms = nil, 648 | }, 649 | -- LSP Server Settings 650 | servers = { 651 | biome = { 652 | settings = {}, 653 | }, 654 | elixirls = { 655 | keys = { 656 | { 657 | "<leader>cp", 658 | function() 659 | local params = vim.lsp.util.make_position_params() 660 | require("util.lsp").execute({ 661 | command = "manipulatePipes:serverid", 662 | arguments = { "toPipe", params.textDocument.uri, params.position.line, params.position.character }, 663 | }) 664 | end, 665 | desc = "To Pipe", 666 | }, 667 | { 668 | "<leader>cP", 669 | function() 670 | local params = vim.lsp.util.make_position_params() 671 | require("util.lsp").execute({ 672 | command = "manipulatePipes:serverid", 673 | arguments = { "fromPipe", params.textDocument.uri, params.position.line, params.position.character }, 674 | }) 675 | end, 676 | desc = "From Pipe", 677 | }, 678 | }, 679 | settings = { 680 | elixirLS = { 681 | fetchDeps = false, 682 | }, 683 | }, 684 | }, 685 | lua_ls = { 686 | -- mason = false, -- set to false if you don't want this server to be installed with mason 687 | -- Use this to add any additional keymaps 688 | -- for specific lsp servers 689 | -- keys = {}, 690 | settings = { 691 | Lua = { 692 | diagnostics = { 693 | globals = { 694 | "after_each", 695 | "assert", 696 | "before_each", 697 | "describe", 698 | "it", 699 | "require", 700 | "use", 701 | "vim", 702 | }, 703 | }, 704 | workspace = { 705 | checkThirdParty = false, 706 | }, 707 | codeLens = { 708 | enable = true, 709 | }, 710 | completion = { 711 | callSnippet = "Replace", 712 | }, 713 | doc = { 714 | privateName = { "^_" }, 715 | }, 716 | hint = { 717 | enable = true, 718 | setType = false, 719 | paramType = true, 720 | paramName = "Disable", 721 | semicolon = "Disable", 722 | arrayIndex = "Disable", 723 | }, 724 | }, 725 | }, 726 | }, 727 | nil_ls = {}, 728 | rust_analyzer = { enabled = false }, 729 | volar = { enabled = false }, 730 | tailwindcss = { 731 | filetypes_exclude = { "markdown" }, 732 | filetypes_include = {}, 733 | }, 734 | vue_ls = { 735 | init_options = { 736 | vue = { 737 | hybridMode = true, 738 | }, 739 | }, 740 | }, 741 | vtsls = { 742 | filetypes = { 743 | "javascript", 744 | "javascriptreact", 745 | "javascript.jsx", 746 | "svelte", 747 | "typescript", 748 | "typescriptreact", 749 | "typescript.tsx", 750 | "vue", 751 | }, 752 | init_options = {}, 753 | settings = { 754 | complete_function_calls = true, 755 | vtsls = { 756 | enableMoveToFileCodeAction = true, 757 | autoUseWorkspaceTsdk = true, 758 | experimental = { 759 | completion = { 760 | enableServerSideFuzzyMatch = true, 761 | }, 762 | }, 763 | tsserver = { 764 | globalPlugins = { 765 | { 766 | name = "@vue/typescript-plugin", 767 | location = require("util.init").get_pkg_path( 768 | "vue-language-server", 769 | "/node_modules/@vue/language-server" 770 | ), 771 | languages = { "vue" }, 772 | configNamespace = "typescript", 773 | enableForWorkspaceTypeScriptVersions = true, 774 | }, 775 | { 776 | name = "typescript-svelte-plugin", 777 | location = require("util.init").get_pkg_path( 778 | "svelte-language-server", 779 | "/node_modules/typescript-svelte-plugin" 780 | ), 781 | languages = { "svelte" }, 782 | enableForWorkspaceTypeScriptVersions = true, 783 | }, 784 | }, 785 | }, 786 | }, 787 | typescript = { 788 | updateImportsOnFileMove = { enabled = "always" }, 789 | suggest = { 790 | completeFunctionCalls = true, 791 | }, 792 | inlayHints = { 793 | enumMemberValues = { enabled = true }, 794 | functionLikeReturnTypes = { enabled = true }, 795 | parameterNames = { enabled = "literals" }, 796 | parameterTypes = { enabled = true }, 797 | propertyDeclarationTypes = { enabled = true }, 798 | variableTypes = { enabled = false }, 799 | }, 800 | }, 801 | }, 802 | keys = { 803 | { 804 | "gD", 805 | function() 806 | local params = vim.lsp.util.make_position_params() 807 | require("util.lsp").execute({ 808 | command = "typescript.goToSourceDefinition", 809 | arguments = { params.textDocument.uri, params.position }, 810 | open = true, 811 | }) 812 | end, 813 | desc = "Goto Source Definition", 814 | }, 815 | { 816 | "gR", 817 | function() 818 | require("util.lsp").execute({ 819 | command = "typescript.findAllFileReferences", 820 | arguments = { vim.uri_from_bufnr(0) }, 821 | open = true, 822 | }) 823 | end, 824 | desc = "File References", 825 | }, 826 | -- stylua: ignore 827 | { "<leader>co", require("util.lsp").action["source.organizeImports"], desc = "Organize Imports" }, 828 | { "<leader>cM", require("util.lsp").action["source.addMissingImports.ts"], desc = "Add missing imports" }, 829 | { "<leader>cu", require("util.lsp").action["source.removeUnused.ts"], desc = "Remove unused imports" }, 830 | { "<leader>cD", require("util.lsp").action["source.fixAll.ts"], desc = "Fix all diagnostics" }, 831 | { 832 | "<leader>cV", 833 | function() 834 | require("util.lsp").execute({ command = "typescript.selectTypeScriptVersion" }) 835 | end, 836 | desc = "Select TS workspace version", 837 | }, 838 | { "<C-\\>", "<cmd>TwoslashQueriesInspect<CR>", desc = "Twoslash Inspect" }, 839 | }, 840 | }, 841 | }, 842 | -- you can do any additional lsp server setup here 843 | -- return true if you don't want this server to be setup with lspconfig 844 | setup = { 845 | -- example to setup with typescript.nvim 846 | -- ts_ls = function(_, opts) 847 | -- require("typescript").setup({ server = opts }) 848 | -- return true 849 | -- end, 850 | -- Specify * to use this function as a fallback for any server 851 | -- ["*"] = function(server, opts) end, 852 | tailwindcss = function(_, opts) 853 | opts.filetypes = opts.filetypes or {} 854 | 855 | -- Add default filetypes 856 | local default_config = vim.lsp.config["tailwindcss"] 857 | if default_config and default_config.filetypes then 858 | vim.list_extend(opts.filetypes, default_config.filetypes) 859 | end 860 | 861 | -- Remove excluded filetypes 862 | --- @param ft string 863 | opts.filetypes = vim.tbl_filter(function(ft) 864 | return not vim.tbl_contains(opts.filetypes_exclude or {}, ft) 865 | end, opts.filetypes) 866 | 867 | -- Additional settings for Phoenix projects 868 | opts.settings = { 869 | tailwindCSS = { 870 | includeLanguages = { 871 | elixir = "html-eex", 872 | eelixir = "html-eex", 873 | heex = "html-eex", 874 | }, 875 | }, 876 | } 877 | 878 | -- Add additional filetypes 879 | vim.list_extend(opts.filetypes, opts.filetypes_include or {}) 880 | end, 881 | vtsls = function(_, opts) 882 | require("util.lsp").on_attach(function(client, _buf) 883 | client.commands["_typescript.moveToFileRefactoring"] = function(command, _ctx) 884 | ---@diagnostic disable-next-line: deprecated 885 | local action, uri, range = unpack(command.arguments) 886 | 887 | local function move(newf) 888 | client:request("workspace/executeCommand", { 889 | command = command.command, 890 | arguments = { action, uri, range, newf }, 891 | }) 892 | end 893 | 894 | local fname = vim.uri_to_fname(uri) 895 | client:request("workspace/executeCommand", { 896 | command = "typescript.tsserverRequest", 897 | arguments = { 898 | "getMoveToRefactoringFileSuggestions", 899 | { 900 | file = fname, 901 | startLine = range.start.line + 1, 902 | startOffset = range.start.character + 1, 903 | endLine = range["end"].line + 1, 904 | endOffset = range["end"].character + 1, 905 | }, 906 | }, 907 | }, function(_, result) 908 | ---@type string[] 909 | local files = result.body.files 910 | table.insert(files, 1, "Enter new path...") 911 | vim.ui.select(files, { 912 | prompt = "Select move destination:", 913 | format_item = function(f) 914 | return vim.fn.fnamemodify(f, ":~:.") 915 | end, 916 | }, function(f) 917 | if f and f:find("^Enter new path") then 918 | vim.ui.input({ 919 | prompt = "Enter move destination:", 920 | default = vim.fn.fnamemodify(fname, ":h") .. "/", 921 | completion = "file", 922 | }, function(newf) 923 | return newf and move(newf) 924 | end) 925 | elseif f then 926 | move(f) 927 | end 928 | end) 929 | end) 930 | end 931 | end, "vtsls") 932 | -- copy typescript settings to javascript 933 | opts.settings.javascript = 934 | vim.tbl_deep_extend("force", {}, opts.settings.typescript, opts.settings.javascript or {}) 935 | end, 936 | }, 937 | } 938 | return ret 939 | end, 940 | config = function(_, opts) 941 | local lsp = require("util.lsp") 942 | -- setup autoformat 943 | require("util.format").register(lsp.formatter()) 944 | 945 | -- setup keymaps 946 | lsp.on_attach(function(client, buffer) 947 | require("util.keymaps").on_attach(client, buffer) 948 | end) 949 | 950 | lsp.setup() 951 | lsp.on_dynamic_capability(require("util.keymaps").on_attach) 952 | 953 | -- diagnostics signs 954 | if vim.fn.has("nvim-0.10.0") == 0 then 955 | if type(opts.diagnostics.signs) ~= "boolean" then 956 | for severity, icon in pairs(opts.diagnostics.signs.text) do 957 | local name = vim.diagnostic.severity[severity]:lower():gsub("^%l", string.upper) 958 | name = "DiagnosticSign" .. name 959 | vim.fn.sign_define(name, { text = icon, texthl = name, numhl = "" }) 960 | end 961 | end 962 | end 963 | 964 | -- inlay hints 965 | if opts.inlay_hints.enabled then 966 | lsp.on_supports_method("textDocument/inlayHint", function(_, buffer) 967 | if 968 | vim.api.nvim_buf_is_valid(buffer) 969 | and vim.bo[buffer].buftype == "" 970 | and not vim.tbl_contains(opts.inlay_hints.exclude, vim.bo[buffer].filetype) 971 | then 972 | vim.lsp.inlay_hint.enable(true, { bufnr = buffer }) 973 | end 974 | end) 975 | end 976 | 977 | -- code lens 978 | if opts.codelens.enabled and vim.lsp.codelens then 979 | lsp.on_supports_method("textDocument/codeLens", function(_, buffer) 980 | vim.lsp.codelens.refresh() 981 | vim.api.nvim_create_autocmd({ "BufEnter", "CursorHold", "InsertLeave" }, { 982 | buffer = buffer, 983 | callback = vim.lsp.codelens.refresh, 984 | }) 985 | end) 986 | end 987 | 988 | if type(opts.diagnostics.virtual_text) == "table" and opts.diagnostics.virtual_text.prefix == "icons" then 989 | opts.diagnostics.virtual_text.prefix = function(diagnostic) 990 | for d, icon in pairs(icons.diagnostics) do 991 | if diagnostic.severity == vim.diagnostic.severity[d:upper()] then 992 | return icon 993 | end 994 | end 995 | end 996 | end 997 | 998 | vim.diagnostic.config(vim.deepcopy(opts.diagnostics)) 999 | 1000 | local servers = opts.servers 1001 | local has_blink, blink = pcall(require, "blink.cmp") 1002 | local capabilities = vim.tbl_deep_extend( 1003 | "force", 1004 | {}, 1005 | vim.lsp.protocol.make_client_capabilities(), 1006 | has_blink and blink.get_lsp_capabilities() or {}, 1007 | opts.capabilities or {} 1008 | ) 1009 | 1010 | local function setup(server) 1011 | local server_opts = vim.tbl_deep_extend("force", { 1012 | capabilities = vim.deepcopy(capabilities), 1013 | }, servers[server] or {}) 1014 | if server_opts.enabled == false then 1015 | return 1016 | end 1017 | 1018 | if opts.setup[server] then 1019 | if opts.setup[server](server, server_opts) then 1020 | return 1021 | end 1022 | elseif opts.setup["*"] then 1023 | if opts.setup["*"](server, server_opts) then 1024 | return 1025 | end 1026 | end 1027 | vim.lsp.config(server, server_opts) 1028 | vim.lsp.enable(server) 1029 | end 1030 | 1031 | -- get all the servers that are available through mason-lspconfig 1032 | local have_mason, mlsp = pcall(require, "mason-lspconfig") 1033 | local all_mslp_servers = {} 1034 | if have_mason then 1035 | all_mslp_servers = vim.tbl_keys(require("mason-lspconfig.mappings.server").lspconfig_to_package) 1036 | end 1037 | 1038 | local ensure_installed = {} ---@type string[] 1039 | for server, server_opts in pairs(servers) do 1040 | if server_opts then 1041 | server_opts = server_opts == true and {} or server_opts 1042 | if server_opts.enabled ~= false then 1043 | -- run manual setup if mason=false or if this is a server that cannot be installed with mason-lspconfig 1044 | if server_opts.mason == false or not vim.tbl_contains(all_mslp_servers, server) then 1045 | setup(server) 1046 | else 1047 | ensure_installed[#ensure_installed + 1] = server 1048 | end 1049 | end 1050 | end 1051 | end 1052 | 1053 | if have_mason then 1054 | mlsp.setup({ 1055 | ensure_installed = vim.tbl_deep_extend( 1056 | "force", 1057 | ensure_installed, 1058 | require("util.init").opts("mason-lspconfig.nvim").ensure_installed or {} 1059 | ), 1060 | handlers = { setup }, 1061 | }) 1062 | end 1063 | end, 1064 | }, 1065 | 1066 | -- nvim-treesitter (https://github.com/nvim-treesitter/nvim-treesitter) 1067 | { 1068 | "nvim-treesitter/nvim-treesitter", 1069 | build = ":TSUpdate", 1070 | event = { "VeryLazy" }, 1071 | lazy = vim.fn.argc(-1) == 0, -- load treesitter early when opening a file from the cmdline 1072 | dependencies = { 1073 | -- https://github.com/windwp/nvim-ts-autotag 1074 | { "windwp/nvim-ts-autotag", opts = {} }, 1075 | }, 1076 | cmd = { "TSUpdateSync", "TSUpdate", "TSInstall" }, 1077 | config = function(_, opts) 1078 | require("nvim-treesitter.configs").setup(opts) 1079 | end, 1080 | init = function(plugin) 1081 | -- PERF: add nvim-treesitter queries to the rtp and it's custom query predicates early 1082 | -- This is needed because a bunch of plugins no longer `require("nvim-treesitter")`, which 1083 | -- no longer trigger the **nvim-treesitter** module to be loaded in time. 1084 | -- Luckily, the only things that those plugins need are the custom queries, which we make available 1085 | -- during startup. 1086 | require("lazy.core.loader").add_to_rtp(plugin) 1087 | require("nvim-treesitter.query_predicates") 1088 | end, 1089 | keys = { 1090 | { "<c-space>", desc = "Increment Selection" }, 1091 | { "<bs>", desc = "Decrement Selection", mode = "x" }, 1092 | }, 1093 | opts = { 1094 | ensure_installed = { 1095 | "bash", 1096 | "c", 1097 | "css", 1098 | "elixir", 1099 | "eex", 1100 | "fish", 1101 | "gitignore", 1102 | "heex", 1103 | "html", 1104 | "javascript", 1105 | "jsdoc", 1106 | "json", 1107 | "lua", 1108 | "markdown", 1109 | "markdown_inline", 1110 | "nix", 1111 | "regex", 1112 | "rust", 1113 | "ron", 1114 | "svelte", 1115 | "toml", 1116 | "tsx", 1117 | "typescript", 1118 | "vim", 1119 | "vimdoc", 1120 | "vue", 1121 | "yaml", 1122 | }, 1123 | highlight = { enable = true }, 1124 | indent = { enable = true }, 1125 | incremental_selection = { 1126 | enable = true, 1127 | keymaps = { 1128 | init_selection = "<C-space>", 1129 | node_incremental = "<C-space>", 1130 | scope_incremental = false, 1131 | node_decremental = "<bs>", 1132 | }, 1133 | }, 1134 | textobjects = { 1135 | -- stylua: ignore 1136 | move = { 1137 | enable = true, 1138 | goto_next_start = { ["]f"] = "@function.outer", ["]c"] = "@class.outer", ["]a"] = "@parameter.inner" }, 1139 | goto_next_end = { ["]F"] = "@function.outer", ["]C"] = "@class.outer", ["]A"] = "@parameter.inner" }, 1140 | goto_previous_start = { ["[f"] = "@function.outer", ["[c"] = "@class.outer", ["[a"] = "@parameter.inner" }, 1141 | goto_previous_end = { ["[F"] = "@function.outer", ["[C"] = "@class.outer", ["[A"] = "@parameter.inner" }, 1142 | }, 1143 | }, 1144 | }, 1145 | }, 1146 | 1147 | -- nvim-treesitter-context (https://github.com/nvim-treesitter/nvim-treesitter-context) 1148 | { 1149 | "nvim-treesitter/nvim-treesitter-context", 1150 | event = "VeryLazy", 1151 | opts = function() 1152 | local tsc = require("treesitter-context") 1153 | require("snacks") 1154 | .toggle({ 1155 | name = "Treesitter Context", 1156 | get = tsc.enabled, 1157 | set = function(state) 1158 | if state then 1159 | tsc.enable() 1160 | else 1161 | tsc.disable() 1162 | end 1163 | end, 1164 | }) 1165 | :map("<leader>ut") 1166 | return { 1167 | max_lines = 3, 1168 | mode = "cursor", 1169 | multiwindow = true, 1170 | } 1171 | end, 1172 | keys = { 1173 | { 1174 | "[c", 1175 | function() 1176 | require("treesitter-context").go_to_context(vim.v.count1) 1177 | end, 1178 | desc = "Jump to context", 1179 | }, 1180 | }, 1181 | }, 1182 | 1183 | -- oil.nvim (https://github.com/stevearc/oil.nvim) 1184 | { 1185 | "stevearc/oil.nvim", 1186 | lazy = false, 1187 | dependencies = { { "nvim-mini/mini.icons", opts = {} } }, 1188 | keys = { 1189 | { "<leader>e", "<cmd>Oil<cr>", desc = "Explorer Oil", remap = true }, 1190 | }, 1191 | opts = { 1192 | keymaps = { 1193 | ["<C-h>"] = false, 1194 | ["<C-l>"] = false, 1195 | ["<C-r>"] = "actions.refresh", 1196 | ["<C-s>"] = { "actions.select", opts = { horizontal = true } }, 1197 | ["<C-v>"] = { "actions.select", opts = { vertical = true } }, 1198 | }, 1199 | }, 1200 | config = function(_, opts) 1201 | -- helper function to parse output 1202 | local function parse_output(proc) 1203 | local result = proc:wait() 1204 | local ret = {} 1205 | if result.code == 0 then 1206 | for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do 1207 | -- Remove trailing slash 1208 | line = line:gsub("/$", "") 1209 | ret[line] = true 1210 | end 1211 | end 1212 | return ret 1213 | end 1214 | 1215 | -- build git status cache 1216 | local function new_git_status() 1217 | return setmetatable({}, { 1218 | __index = function(self, key) 1219 | local ignore_proc = vim.system( 1220 | { "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" }, 1221 | { 1222 | cwd = key, 1223 | text = true, 1224 | } 1225 | ) 1226 | local tracked_proc = vim.system({ "git", "ls-tree", "HEAD", "--name-only" }, { 1227 | cwd = key, 1228 | text = true, 1229 | }) 1230 | local ret = { 1231 | ignored = parse_output(ignore_proc), 1232 | tracked = parse_output(tracked_proc), 1233 | } 1234 | rawset(self, key, ret) 1235 | return ret 1236 | end, 1237 | }) 1238 | end 1239 | local git_status = new_git_status() 1240 | 1241 | -- Clear git status cache on refresh 1242 | local refresh = require("oil.actions").refresh 1243 | local orig_refresh = refresh.callback 1244 | refresh.callback = function(...) 1245 | git_status = new_git_status() 1246 | orig_refresh(...) 1247 | end 1248 | 1249 | opts.view_options = { 1250 | is_hidden_file = function(name, bufnr) 1251 | local dir = require("oil").get_current_dir(bufnr) 1252 | local is_dotfile = vim.startswith(name, ".") and name ~= ".." 1253 | -- if no local directory (e.g. for ssh connections), just hide dotfiles 1254 | if not dir then 1255 | return is_dotfile 1256 | end 1257 | -- dotfiles are considered hidden unless tracked 1258 | if is_dotfile then 1259 | return not git_status[dir].tracked[name] 1260 | else 1261 | -- Check if file is gitignored 1262 | return git_status[dir].ignored[name] 1263 | end 1264 | end, 1265 | } 1266 | 1267 | require("oil").setup(opts) 1268 | end, 1269 | }, 1270 | 1271 | -- rustaceanvim (https://github.com/mrcjkb/rustaceanvim) 1272 | { 1273 | "mrcjkb/rustaceanvim", 1274 | ft = { "rust" }, 1275 | opts = { 1276 | server = { 1277 | on_attach = function(_, bufnr) 1278 | vim.keymap.set("n", "<leader>cR", function() 1279 | vim.cmd.RustLsp("codeAction") 1280 | end, { desc = "Code Action", buffer = bufnr }) 1281 | vim.keymap.set("n", "<leader>dr", function() 1282 | vim.cmd.RustLsp("debuggables") 1283 | end, { desc = "Rust Debuggables", buffer = bufnr }) 1284 | end, 1285 | default_settings = { 1286 | -- rust-analyzer language server configuration 1287 | ["rust-analyzer"] = { 1288 | cargo = { 1289 | allFeatures = true, 1290 | loadOutDirsFromCheck = true, 1291 | buildScripts = { 1292 | enable = true, 1293 | }, 1294 | }, 1295 | checkOnSave = true, 1296 | diagnostics = { 1297 | enable = true, 1298 | }, 1299 | procMacro = { 1300 | enable = true, 1301 | }, 1302 | files = { 1303 | exclude = { 1304 | ".direnv", 1305 | ".git", 1306 | ".jj", 1307 | ".github", 1308 | ".gitlab", 1309 | "bin", 1310 | "node_modules", 1311 | "target", 1312 | "venv", 1313 | ".venv", 1314 | }, 1315 | -- Avoid Roots Scanned hanging, see https://github.com/rust-lang/rust-analyzer/issues/12613#issuecomment-2096386344 1316 | watcher = "client", 1317 | }, 1318 | }, 1319 | }, 1320 | }, 1321 | }, 1322 | config = function(_, opts) 1323 | -- if LazyVim.has("mason.nvim") then 1324 | -- local codelldb = vim.fn.exepath("codelldb") 1325 | -- local codelldb_lib_ext = io.popen("uname"):read("*l") == "Linux" and ".so" or ".dylib" 1326 | -- local library_path = vim.fn.expand("$MASON/opt/lldb/lib/liblldb" .. codelldb_lib_ext) 1327 | -- opts.dap = { 1328 | -- adapter = require("rustaceanvim.config").get_codelldb_adapter(codelldb, library_path), 1329 | -- } 1330 | -- end 1331 | vim.g.rustaceanvim = vim.tbl_deep_extend("keep", vim.g.rustaceanvim or {}, opts or {}) 1332 | if vim.fn.executable("rust-analyzer") == 0 then 1333 | require("util.init").error( 1334 | "**rust-analyzer** not found in PATH, please install it.\nhttps://rust-analyzer.github.io/", 1335 | { title = "rustaceanvim" } 1336 | ) 1337 | end 1338 | end, 1339 | }, 1340 | 1341 | -- snacks.nvim (https://github.com/folke/snacks.nvim) 1342 | { 1343 | "folke/snacks.nvim", 1344 | lazy = false, 1345 | priority = 1001, 1346 | opts = { 1347 | bigfile = { enabled = true }, 1348 | dim = { 1349 | animate = { enabled = false }, 1350 | }, 1351 | indent = { 1352 | enabled = true, 1353 | animate = { enabled = false }, 1354 | }, 1355 | input = { enabled = true }, 1356 | notifier = { 1357 | enabled = true, 1358 | icons = { 1359 | error = icons.diagnostics.Error, 1360 | warn = icons.diagnostics.Warn, 1361 | info = icons.diagnostics.Info, 1362 | debug = icons.misc.Bug, 1363 | trace = " ", 1364 | }, 1365 | }, 1366 | picker = { 1367 | actions = { 1368 | flash = function(picker) 1369 | require("flash").jump({ 1370 | pattern = "^", 1371 | label = { after = { 0, 0 } }, 1372 | search = { 1373 | mode = "search", 1374 | exclude = { 1375 | function(win) 1376 | return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= "snacks_picker_list" 1377 | end, 1378 | }, 1379 | }, 1380 | action = function(match) 1381 | local idx = picker.list:row2idx(match.pos[1]) 1382 | picker.list:_move(idx, true, true) 1383 | end, 1384 | }) 1385 | end, 1386 | toggle_cwd = function(p) 1387 | local root = require("util.root").get({ buf = p.input.filter.current_buf, normalize = true }) 1388 | local cwd = vim.fs.normalize((vim.uv or vim.loop).cwd() or ".") 1389 | local current = p:cwd() 1390 | p:set_cwd(current == root and cwd or root) 1391 | p:find() 1392 | end, 1393 | trouble_open = function(...) 1394 | return require("trouble.sources.snacks").actions.trouble_open.action(...) 1395 | end, 1396 | }, 1397 | formatters = { 1398 | file = { 1399 | truncate = 80, 1400 | }, 1401 | }, 1402 | win = { 1403 | input = { 1404 | keys = { 1405 | ["s"] = { "flash" }, 1406 | ["<a-c>"] = { "toggle_cwd", mode = { "n", "i" } }, 1407 | ["<a-s>"] = { "flash", mode = { "n", "i" } }, 1408 | ["<a-t>"] = { "trouble_open", mode = { "n", "i" } }, 1409 | }, 1410 | }, 1411 | }, 1412 | }, 1413 | quickfile = { enabled = true }, 1414 | scope = { enabled = true }, 1415 | toggle = { 1416 | enabled = true, 1417 | map = require("util.init").safe_keymap_set, 1418 | notify = true, 1419 | which_key = true, 1420 | }, 1421 | words = { enabled = true }, 1422 | }, 1423 | keys = function() 1424 | local snacks = require("snacks") 1425 | local pick = require("util.pick") 1426 | 1427 | -- stylua: ignore 1428 | return { 1429 | { "<leader>un", function() snacks.notifier.hide() end, desc = "Dismiss All Notifications" }, 1430 | 1431 | { "<leader>,", function() snacks.picker.buffers() end, desc = "Buffers" }, 1432 | { "<leader>/", pick.open("grep"), desc = "Grep (Root Dir)" }, 1433 | { "<leader>:", function() snacks.picker.command_history() end, desc = "Command History" }, 1434 | { "<leader><space>", pick.open("files"), desc = "Find Files (Root Dir)" }, 1435 | { "<leader>n", function() snacks.picker.notifications() end, desc = "Notification History" }, 1436 | -- find 1437 | { "<leader>fb", function() snacks.picker.buffers() end, desc = "Buffers" }, 1438 | { "<leader>fB", function() snacks.picker.buffers({ hidden = true, nofile = true }) end, desc = "Buffers (all)" }, 1439 | { "<leader>fc", pick.config_files(), desc = "Find Config File" }, 1440 | { "<leader>ff", pick.open("files"), desc = "Find Files (Root Dir)" }, 1441 | { "<leader>fF", pick.open("files", { root = false }), desc = "Find Files (cwd)" }, 1442 | { "<leader>fg", function() snacks.picker.git_files() end, desc = "Find Files (git-files)" }, 1443 | { "<leader>fr", pick.open("recent"), desc = "Recent" }, 1444 | { "<leader>fR", function() snacks.picker.recent({ filter = { cwd = true }}) end, desc = "Recent (cwd)" }, 1445 | { "<leader>fp", function() snacks.picker.projects() end, desc = "Projects" }, 1446 | -- git 1447 | { "<leader>gd", function() snacks.picker.git_diff() end, desc = "Git Diff (hunks)" }, 1448 | { "<leader>gs", function() snacks.picker.git_status() end, desc = "Git Status" }, 1449 | { "<leader>gS", function() snacks.picker.git_stash() end, desc = "Git Stash" }, 1450 | -- Grep 1451 | { "<leader>sb", function() snacks.picker.lines() end, desc = "Buffer Lines" }, 1452 | { "<leader>sB", function() snacks.picker.grep_buffers() end, desc = "Grep Open Buffers" }, 1453 | { "<leader>sg", pick.open("grep"), desc = "Grep (Root Dir)" }, 1454 | { "<leader>sG", pick.open("grep", { root = false }), desc = "Grep (cwd)" }, 1455 | { "<leader>sp", function() snacks.picker.lazy() end, desc = "Search for Plugin Spec" }, 1456 | { "<leader>sw", pick.open("grep_word"), desc = "Visual selection or word (Root Dir)", mode = { "n", "x" } }, 1457 | { "<leader>sW", pick.open("grep_word", { root = false }), desc = "Visual selection or word (cwd)", mode = { "n", "x" } }, 1458 | -- search 1459 | { '<leader>s"', function() snacks.picker.registers() end, desc = "Registers" }, 1460 | { '<leader>s/', function() snacks.picker.search_history() end, desc = "Search History" }, 1461 | { "<leader>sa", function() snacks.picker.autocmds() end, desc = "Autocmds" }, 1462 | { "<leader>sc", function() snacks.picker.command_history() end, desc = "Command History" }, 1463 | { "<leader>sC", function() snacks.picker.commands() end, desc = "Commands" }, 1464 | { "<leader>sd", function() snacks.picker.diagnostics() end, desc = "Diagnostics" }, 1465 | { "<leader>sD", function() snacks.picker.diagnostics_buffer() end, desc = "Buffer Diagnostics" }, 1466 | { "<leader>sh", function() snacks.picker.help() end, desc = "Help Pages" }, 1467 | { "<leader>sH", function() snacks.picker.highlights() end, desc = "Highlights" }, 1468 | { "<leader>si", function() snacks.picker.icons() end, desc = "Icons" }, 1469 | { "<leader>sj", function() snacks.picker.jumps() end, desc = "Jumps" }, 1470 | { "<leader>sk", function() snacks.picker.keymaps() end, desc = "Keymaps" }, 1471 | { "<leader>sl", function() snacks.picker.loclist() end, desc = "Location List" }, 1472 | { "<leader>sM", function() snacks.picker.man() end, desc = "Man Pages" }, 1473 | { "<leader>sm", function() snacks.picker.marks() end, desc = "Marks" }, 1474 | { "<leader>sR", function() snacks.picker.resume() end, desc = "Resume" }, 1475 | { "<leader>sq", function() snacks.picker.qflist() end, desc = "Quickfix List" }, 1476 | { "<leader>su", function() snacks.picker.undo() end, desc = "Undotree" }, 1477 | -- ui 1478 | { "<leader>uC", function() snacks.picker.colorschemes() end, desc = "Colorschemes" }, 1479 | } 1480 | end, 1481 | config = function(_, opts) 1482 | local notify = vim.notify 1483 | require("snacks").setup(opts) 1484 | -- HACK: restore vim.notify after snacks setup and let noice.nvim take over 1485 | -- this is needed to have early notifications show up in noice history 1486 | if require("util.init").has("noice.nvim") then 1487 | vim.notify = notify 1488 | end 1489 | end, 1490 | }, 1491 | 1492 | -- trouble.nvim (https://github.com/folke/trouble.nvim) 1493 | { 1494 | "folke/trouble.nvim", 1495 | cmd = { "Trouble" }, 1496 | opts = { 1497 | modes = { 1498 | lsp = { 1499 | win = { position = "right" }, 1500 | }, 1501 | }, 1502 | }, 1503 | keys = { 1504 | { "<leader>xx", "<cmd>Trouble diagnostics toggle<cr>", desc = "Diagnostics (Trouble)" }, 1505 | { "<leader>xX", "<cmd>Trouble diagnostics toggle filter.buf=0<cr>", desc = "Buffer Diagnostics (Trouble)" }, 1506 | { "<leader>cs", "<cmd>Trouble symbols toggle<cr>", desc = "Symbols (Trouble)" }, 1507 | { "<leader>cS", "<cmd>Trouble lsp toggle<cr>", desc = "LSP references/definitions/... (Trouble)" }, 1508 | { "<leader>xL", "<cmd>Trouble loclist toggle<cr>", desc = "Location List (Trouble)" }, 1509 | { "<leader>xQ", "<cmd>Trouble qflist toggle<cr>", desc = "Quickfix List (Trouble)" }, 1510 | { 1511 | "[q", 1512 | function() 1513 | if require("trouble").is_open() then 1514 | require("trouble").prev({ skip_groups = true, jump = true }) 1515 | else 1516 | local ok, err = pcall(vim.cmd.cprev) 1517 | if not ok then 1518 | vim.notify(err, vim.log.levels.ERROR) 1519 | end 1520 | end 1521 | end, 1522 | desc = "Previous Trouble/Quickfix Item", 1523 | }, 1524 | { 1525 | "]q", 1526 | function() 1527 | if require("trouble").is_open() then 1528 | require("trouble").next({ skip_groups = true, jump = true }) 1529 | else 1530 | local ok, err = pcall(vim.cmd.cnext) 1531 | if not ok then 1532 | vim.notify(err, vim.log.levels.ERROR) 1533 | end 1534 | end 1535 | end, 1536 | desc = "Next Trouble/Quickfix Item", 1537 | }, 1538 | }, 1539 | }, 1540 | 1541 | -- ts-comments.nvim (https://github.com/folke/ts-comments.nvim) 1542 | { 1543 | "folke/ts-comments.nvim", 1544 | event = "VeryLazy", 1545 | opts = {}, 1546 | }, 1547 | 1548 | -- twoslash-queries.nvim (https://github.com/marilari88/twoslash-queries.nvim) 1549 | { 1550 | "marilari88/twoslash-queries.nvim", 1551 | opts = { 1552 | highlight = "Type", 1553 | multi_line = true, 1554 | }, 1555 | config = function(_, opts) 1556 | local twoslash_queries = require("twoslash-queries") 1557 | twoslash_queries.setup(opts) 1558 | require("util.lsp").on_attach(function(client, bufnr) 1559 | require("twoslash-queries").attach(client, bufnr) 1560 | end, "vtsls") 1561 | end, 1562 | }, 1563 | 1564 | -- vim-repeat (https://github.com/tpope/vim-repeat) 1565 | { 1566 | "tpope/vim-repeat", 1567 | event = "VeryLazy", 1568 | }, 1569 | 1570 | -- which-key.nvim (https://github.com/folke/which-key.nvim) 1571 | { 1572 | "folke/which-key.nvim", 1573 | event = "VeryLazy", 1574 | opts_extend = { "spec" }, 1575 | opts = { 1576 | defaults = {}, 1577 | icons = { 1578 | -- mappings = false, 1579 | colors = true, 1580 | rules = false, 1581 | }, 1582 | preset = "helix", 1583 | spec = { 1584 | { 1585 | mode = { "n", "v" }, 1586 | { "<leader><tab>", group = "tabs" }, 1587 | { "<leader>c", group = "code" }, 1588 | { "<leader>f", group = "file/find" }, 1589 | { "<leader>g", group = "git" }, 1590 | { "<leader>gh", group = "hunks" }, 1591 | { "<leader>q", group = "quit/session" }, 1592 | { "<leader>s", group = "search" }, 1593 | { "<leader>u", group = "ui" }, 1594 | { "<leader>x", group = "diagnostics/quickfix" }, 1595 | { "[", group = "prev" }, 1596 | { "]", group = "next" }, 1597 | { "g", group = "goto" }, 1598 | { "gs", group = "surround" }, 1599 | { "z", group = "fold" }, 1600 | { 1601 | "<leader>b", 1602 | group = "buffer", 1603 | expand = function() 1604 | return require("which-key.extras").expand.buf() 1605 | end, 1606 | }, 1607 | { 1608 | "<leader>w", 1609 | group = "windows", 1610 | proxy = "<c-w>", 1611 | expand = function() 1612 | return require("which-key.extras").expand.win() 1613 | end, 1614 | }, 1615 | -- better descriptions 1616 | { "gx", desc = "Open with system app" }, 1617 | }, 1618 | }, 1619 | }, 1620 | keys = { 1621 | { 1622 | "<leader>?", 1623 | function() 1624 | require("which-key").show({ global = false }) 1625 | end, 1626 | desc = "Buffer Keymaps (which-key)", 1627 | }, 1628 | { 1629 | "<c-w><space>", 1630 | function() 1631 | require("which-key").show({ keys = "<c-w>", loop = true }) 1632 | end, 1633 | desc = "Window Hydra Mode (which-key)", 1634 | }, 1635 | }, 1636 | config = function(_, opts) 1637 | local wk = require("which-key") 1638 | wk.setup(opts) 1639 | end, 1640 | }, 1641 | 1642 | -- rsms (https://github.com/tmm/rsms) 1643 | { 1644 | "tmm/rsms", 1645 | dev = true, 1646 | dependencies = { "rktjmp/lush.nvim" }, 1647 | lazy = false, 1648 | priority = 1000, 1649 | config = function() 1650 | vim.cmd([[colorscheme rsms]]) 1651 | end, 1652 | }, 1653 | } 1654 | --------------------------------------------------------------------------------