diff --git a/.config/nvim/after/ftplugin/lua.lua b/.config/nvim/after/ftplugin/lua.lua new file mode 100644 index 0000000..f69dff1 --- /dev/null +++ b/.config/nvim/after/ftplugin/lua.lua @@ -0,0 +1,2 @@ +-- when opening a .lua file, this file gets executed +vim.opt.expandtab = false diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua new file mode 100644 index 0000000..ec33304 --- /dev/null +++ b/.config/nvim/init.lua @@ -0,0 +1,9 @@ +require("core") +require("packages").setup({ + require("ui"), + require("selection"), + require("editor"), + require("development"), + require("git"), +}) +require("keybinds").setup() diff --git a/.config/nvim/lazy-lock.json b/.config/nvim/lazy-lock.json new file mode 100644 index 0000000..cd78127 --- /dev/null +++ b/.config/nvim/lazy-lock.json @@ -0,0 +1,39 @@ +{ + "Comment.nvim": { "branch": "master", "commit": "0236521ea582747b58869cb72f70ccfa967d2e89" }, + "LuaSnip": { "branch": "master", "commit": "2dbef19461198630b3d7c39f414d09fb07d1fdd2" }, + "auto-save.nvim": { "branch": "main", "commit": "e98cafef75271ec83dc84c933f124ab1bb675ef8" }, + "cmp-buffer": { "branch": "main", "commit": "3022dbc9166796b644a841a02de8dd1cc1d311fa" }, + "cmp-nvim-lsp": { "branch": "main", "commit": "5af77f54de1b16c34b23cba810150689a3a90312" }, + "cmp-nvim-lua": { "branch": "main", "commit": "f12408bdb54c39c23e67cab726264c10db33ada8" }, + "cmp-path": { "branch": "main", "commit": "91ff86cd9c29299a64f968ebb45846c485725f23" }, + "cmp_luasnip": { "branch": "master", "commit": "05a9ab28b53f71d1aece421ef32fee2cb857a843" }, + "dashboard-nvim": { "branch": "master", "commit": "04a48b2e230bc5e50dd099d839443703023b0472" }, + "diffview.nvim": { "branch": "main", "commit": "3dc498c9777fe79156f3d32dddd483b8b3dbd95f" }, + "friendly-snippets": { "branch": "main", "commit": "b8fae73a479ae0a1c54f5c98fa687ae8a0addc53" }, + "gitsigns.nvim": { "branch": "main", "commit": "2c2463dbd82eddd7dbab881c3a62cfbfbe3c67ae" }, + "lazy.nvim": { "branch": "main", "commit": "aedcd79811d491b60d0a6577a9c1701063c2a609" }, + "lspkind.nvim": { "branch": "master", "commit": "1735dd5a5054c1fb7feaf8e8658dbab925f4f0cf" }, + "lualine.nvim": { "branch": "master", "commit": "7d131a8d3ba5016229e8a1d08bf8782acea98852" }, + "neodev.nvim": { "branch": "main", "commit": "b49b976cf2c28cd8283e9d74cb10885f6dd9e3d0" }, + "neogit": { "branch": "master", "commit": "536b4cfc009fc6d8bd771f010f04d48204533fae" }, + "nvim-base16": { "branch": "master", "commit": "2349e8357864bf0ef45d40f491f8e1e6465485b0" }, + "nvim-cmp": { "branch": "main", "commit": "04e0ca376d6abdbfc8b52180f8ea236cbfddf782" }, + "nvim-lspconfig": { "branch": "master", "commit": "41f40dc4b86f3e166cf08115f621001972565a20" }, + "nvim-treesitter": { "branch": "master", "commit": "6444286cbf3a37862feaa7ff86456dd948f5cf8b" }, + "nvim-treesitter-textobjects": { "branch": "master", "commit": "dd0b2036c3a27cb6e6486f8bd24188c6ca43af0b" }, + "nvim-web-devicons": { "branch": "master", "commit": "7f30f2da3c3641841ceb0e2c150281f624445e8f" }, + "omnisharp-extended-lsp.nvim": { "branch": "main", "commit": "4be2e8689067494ed7e5a4f1221adc31d1a07783" }, + "plenary.nvim": { "branch": "master", "commit": "4f71c0c4a196ceb656c824a70792f3df3ce6bb6d" }, + "project.nvim": { "branch": "main", "commit": "8c6bad7d22eef1b71144b401c9f74ed01526a4fb" }, + "rainbow-delimiters.nvim": { "branch": "master", "commit": "ca8d5ee2b4ee1eec491040a7601d366ddc8a2e02" }, + "sqlite.lua": { "branch": "master", "commit": "b7e28c8463254c46a8e61c52d27d6a2040492fc3" }, + "telescope-all-recent.nvim": { "branch": "main", "commit": "c1a83b96de07ed4c27af5a46cf8b10c8ee802973" }, + "telescope-fzf-native.nvim": { "branch": "main", "commit": "6c921ca12321edaa773e324ef64ea301a1d0da62" }, + "telescope-recent-files": { "branch": "main", "commit": "6893cda11625254cc7dc2ea76e0a100c7deeb028" }, + "telescope.nvim": { "branch": "0.1.x", "commit": "d90956833d7c27e73c621a61f20b29fdb7122709" }, + "todo-comments.nvim": { "branch": "main", "commit": "833d8dd8b07eeda37a09e99460f72a02616935cb" }, + "trouble.nvim": { "branch": "main", "commit": "f1168feada93c0154ede4d1fe9183bf69bac54ea" }, + "ultimate-autopair.nvim": { "branch": "development", "commit": "07c9da3e7722107163b68ecc5e0141fdd825449d" }, + "vim-sleuth": { "branch": "master", "commit": "1cc4557420f215d02c4d2645a748a816c220e99b" }, + "which-key.nvim": { "branch": "main", "commit": "4433e5ec9a507e5097571ed55c02ea9658fb268a" } +} \ No newline at end of file diff --git a/.config/nvim/lua/core.lua b/.config/nvim/lua/core.lua new file mode 100644 index 0000000..97a9d5f --- /dev/null +++ b/.config/nvim/lua/core.lua @@ -0,0 +1,4 @@ +require("core.leader-key") +require("core.config") +require("core.autocommands") +require("core.globals") diff --git a/.config/nvim/lua/core/autocommands.lua b/.config/nvim/lua/core/autocommands.lua new file mode 100644 index 0000000..6656d5f --- /dev/null +++ b/.config/nvim/lua/core/autocommands.lua @@ -0,0 +1,29 @@ +-------------------------------------------- +--- Commands --- + +-- Prevent typo +vim.cmd [[ + cnoreabbrev W (getcmdtype() is# ":" && getcmdline() is# "W") ? ("w") : ("W") + cnoreabbrev Q (getcmdtype() is# ":" && getcmdline() is# "Q") ? ("q") : ("Q") + cnoreabbrev WQ (getcmdtype() is# ":" && getcmdline() is# "WQ") ? ("wq") : ("WQ") + cnoreabbrev Wq (getcmdtype() is# ":" && getcmdline() is# "Wq") ? ("wq") : ("Wq") +]] + +-------------------------------------------- +--- Autocommands --- + +-- Cut off trailing whitespace and trailing blank lines +local core = require("core.functions") +vim.api.nvim_create_autocmd({ "BufWritePre" }, { + pattern = "*", + callback = core.trim_buffer, +}) + +-- Highlight on yank +-- See `:help vim.highlight.on_yank()` +local highlight_group = vim.api.nvim_create_augroup("YankHighlight", { clear = true }) +vim.api.nvim_create_autocmd("TextYankPost", { + pattern = "*", + group = highlight_group, + callback = function() vim.highlight.on_yank() end, +}) diff --git a/.config/nvim/lua/core/config.lua b/.config/nvim/lua/core/config.lua new file mode 100644 index 0000000..7815415 --- /dev/null +++ b/.config/nvim/lua/core/config.lua @@ -0,0 +1,48 @@ +--- Behavior --- + +-- vim.opt.autochdir = true +vim.opt.clipboard = "unnamedplus" +vim.opt.encoding = "utf-8" +vim.opt.fileencoding = "utf-8" +vim.opt.mouse = "a" +vim.opt.scrolloff = 8 +vim.opt.signcolumn = "yes" -- always show gutter/fringe +vim.opt.ttimeoutlen = 0 + +-- Case-insensitive searching UNLESS \C or capital in search +vim.opt.ignorecase = true +vim.opt.smartcase = true + +--- Editing --- + +vim.opt.number = true +vim.opt.breakindent = true + +vim.opt.backspace = "indent,eol,start" +vim.opt.expandtab = false +vim.opt.shiftround = true +vim.opt.shiftwidth = 4 +vim.opt.softtabstop = 4 +vim.opt.tabstop = 4 + +vim.opt.smartindent = true + +vim.opt.wrap = false + +--- Files + +vim.opt.backup = true +vim.opt.backupdir = vim.fn.stdpath("cache") .. "/backup" +vim.opt.swapfile = false +vim.opt.undofile = true +vim.opt.undodir = vim.fn.stdpath("cache") .. "/undodir" + +vim.opt.shadafile = vim.fn.stdpath("cache") .. "/netrwhist" + +--- UI --- + +vim.opt.colorcolumn = "81" +vim.opt.completeopt = "menuone,noselect" +vim.opt.cursorline = true +vim.opt.termguicolors = true +vim.opt.title = true diff --git a/.config/nvim/lua/core/functions.lua b/.config/nvim/lua/core/functions.lua new file mode 100644 index 0000000..b311a6d --- /dev/null +++ b/.config/nvim/lua/core/functions.lua @@ -0,0 +1,72 @@ +local M = {} + +M.is_buffer_a_file = function() + local buffer_name = vim.fn.bufname() + + return buffer_name ~= "" and vim.fn.filereadable(buffer_name) == 1 +end + +M.get_file_path = function() + if not M.is_buffer_a_file() then + return nil + end + local file_path = vim.fn.expand("%:p") + + return vim.fn.fnamemodify(file_path, ":h") .. "/" +end + +M.get_netrw_path = function() -- b:netrw_curdir + if vim.fn.expand("#" .. vim.fn.bufnr()) == "0" then + return nil + end + + return vim.fn.fnamemodify(vim.fn.bufname(), ":p") +end + +M.get_current_directory = function() + return M.get_file_path() or M.get_netrw_path() or vim.fn.getcwd() +end + +M.find_project_root = function() + local current_directory = M.get_current_directory() + + local directory = current_directory + while directory ~= "/" do + local git_directory = directory .. "/.git" + local project_file = directory .. "/.project" + + if vim.fn.isdirectory(git_directory) == 1 or vim.fn.filereadable(project_file) == 1 then + return directory + end + + directory = vim.fn.fnamemodify(directory, ":h") + end + + return nil, current_directory +end + +-- This will merge tables with index-value pairs and keep the unique values +M.table_merge_unique = function(...) + local result = {} + local seen_values = {} + for _, value in ipairs(vim.tbl_flatten(...)) do + if not seen_values[value] then + seen_values[value] = true + table.insert(result, value) + end + end + + return result +end + +-- Cut off trailing whitespace and trailing blank lines +-- https://vi.stackexchange.com/questions/37421/how-to-remove-neovim-trailing-white-space +-- https://stackoverflow.com/questions/7495932/how-can-i-trim-blank-lines-at-the-end-of-file-in-vim +M.trim_buffer = function() + local save_cursor = vim.fn.getpos(".") + pcall(function() vim.cmd([[%s/\s\+$//e]]) end) + pcall(function() vim.cmd([[%s#\($\n\s*\)\+\%$##]]) end) + vim.fn.setpos(".", save_cursor) +end + +return M diff --git a/.config/nvim/lua/core/globals.lua b/.config/nvim/lua/core/globals.lua new file mode 100644 index 0000000..29f89c5 --- /dev/null +++ b/.config/nvim/lua/core/globals.lua @@ -0,0 +1,19 @@ +-- Usage: +-- :lua P("Hello World!") +P = function(v) + print(vim.inspect(v)) + return v +end + +RELOAD = function(...) + return require("plenary.reload").reload_module(...) +end + +R = function(name) + RELOAD(name) + return require(name) +end + +LOG = function(v) + vim.fn.writefile({ vim.inspect(v) }, "/tmp/nvim-log", "a") +end diff --git a/.config/nvim/lua/core/leader-key.lua b/.config/nvim/lua/core/leader-key.lua new file mode 100644 index 0000000..5a3f9be --- /dev/null +++ b/.config/nvim/lua/core/leader-key.lua @@ -0,0 +1,6 @@ +-- Set as the leader key +-- See `:help mapleader` +-- NOTE: Must happen before plugins are required (otherwise wrong leader will be used) +vim.keymap.set({ "n", "v" }, "", "", { silent = true }) +vim.g.mapleader = " " +vim.g.maplocalleader = " " diff --git a/.config/nvim/lua/development.lua b/.config/nvim/lua/development.lua new file mode 100644 index 0000000..98bd3a6 --- /dev/null +++ b/.config/nvim/lua/development.lua @@ -0,0 +1,169 @@ +return { + + -- Autocompletion + { + "hrsh7th/nvim-cmp", + lazy = true, -- defer + dependencies = { + -- Snippet Engine & its associated nvim-cmp source + "L3MON4D3/LuaSnip", + "saadparwaiz1/cmp_luasnip", + + -- Adds LSP completion capabilities + "hrsh7th/cmp-nvim-lsp", -- source for neovim"s built-in LSP client + "hrsh7th/cmp-nvim-lua", -- source for neovim"s Lua API + "hrsh7th/cmp-buffer", + "hrsh7th/cmp-path", + + -- Adds a number of user-friendly snippets + "rafamadriz/friendly-snippets", + + -- Icons in the completion menu + "onsails/lspkind.nvim", + }, + config = function() + local luasnip = require("luasnip") + require("luasnip.loaders.from_vscode").lazy_load() + luasnip.config.setup({}) + + require("cmp").setup({ + snippet = { + expand = function(args) + luasnip.lsp_expand(args.body) + end, + }, + completion = { + completeopt = "menu,menuone,noinsert", + }, + sources = { + { name = "nvim_lsp" }, + { name = "nvim_lua" }, + { name = "luasnip" }, + { name = "buffer" }, + { name = "path" }, + }, + formatting = { + fields = { "kind", "abbr", "menu" }, + -- https://github.com/hrsh7th/nvim-cmp/wiki/Menu-Appearance#how-to-get-types-on-the-left-and-offset-the-menu + -- https://github.com/onsails/lspkind.nvim#option-2-nvim-cmp + format = function(entry, vim_item) + local kind = require("lspkind").cmp_format({ mode = "symbol_text", maxwidth = 50 })(entry, + vim_item) + local strings = vim.split(kind.kind, "%s", { trimempty = true }) + kind.kind = strings[1] or "" + kind.menu = " (" .. (strings[2] or "") .. ")" + + return kind + end, + expandable_indicator = true, + }, + mapping = require("keybinds").nvim_cmp(), + }) + end, + }, + + -- LSP Configuration & Plugins + { -- https://github.com/neovim/nvim-lspconfig + "neovim/nvim-lspconfig", + dependencies = { + -- Additional lua configuration, makes Nvim stuff amazing! + "folke/neodev.nvim", + -- C# "Goto Definition" with decompilation support + "Hoffs/omnisharp-extended-lsp.nvim", + }, + config = function() + -- Setup neovim Lua configuration + require("neodev").setup() + + -- Vim process + local pid = vim.fn.getpid() + + -- Setup language servers + -- https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md + local servers = { + clangd = {}, -- C/C++ + intelephense = {}, -- PHP + lua_ls = { -- Lua, via lua-language-server + settings = { + Lua = { + workspace = { checkThirdParty = false }, + telemetry = { enable = false }, + diagnostics = { + -- Ignore Lua_LS's noisy `missing-fields` warnings + disable = { "missing-fields" }, + -- Ignore Lua_LS's noise `undefined global` warnings + globals = { "vim" }, + }, + }, + }, + }, + omnisharp = { -- C#, via omnisharp-roslyn-bin + cmd = { "/usr/bin/omnisharp", "--languageserver", "--hostPID", tostring(pid) }, + handlers = { + ["textDocument/definition"] = require('omnisharp_extended').handler, + }, + }, + } + + -- nvim-cmp supports additional completion capabilities, so broadcast that to servers + local capabilities = vim.lsp.protocol.make_client_capabilities() + capabilities = require("cmp_nvim_lsp").default_capabilities(capabilities) + + for server, _ in pairs(servers) do + local opts = vim.tbl_extend("keep", servers[server], { + capabilities = capabilities, + on_attach = require("keybinds").lspconfig_on_attach, + }) + require("lspconfig")[server].setup(opts) + end + end, + }, + + -- Project + { + "ahmedkhalf/project.nvim", + opts = { + detection_methods = { "lsp", "pattern" }, + patterns = { ".git", ".project" }, + datapath = vim.fn.stdpath("cache"), + }, + config = function(_, opts) + -- Add syncing between project.nvim and dashboard-nvim project entries. + -- NOTE: Must come before the project.nvim setup(), + -- so the glue's VimLeavePre autocmd is defined before it. + local F = require("core.functions") + vim.api.nvim_create_autocmd("VimLeavePre", { + pattern = "*", + callback = function() + -- Fetch project.nvim projects + local project = require("project_nvim.project") + local history = require("project_nvim.utils.history") + local recent = history.recent_projects or {} + local session = history.session_projects or {} + local recent_plus_session = F.table_merge_unique(recent, session) + + -- Fetch dashboard-nvim projects + local utils = require("dashboard.utils") + local path = utils.path_join(vim.fn.stdpath("cache"), "dashboard/cache") + local projects = utils.read_project_cache(path) or {} + + -- Add any projects that project.nvim uniquely knows about to dashboard-nvim + local all_projects = F.table_merge_unique(recent_plus_session, projects) + vim.fn.writefile({ "return " .. vim.inspect(all_projects) }, path) + + -- Add any projects that dashboard-nvim uniquely knows about to project.nvim + for _, value in ipairs(projects) do + if not vim.tbl_contains(recent_plus_session, value) then + pcall(project.set_pwd, value, "manual") -- skip non-existent directories, dont error + end + end + end, + }) + + require("project_nvim").setup(opts) + require("telescope").load_extension("projects") + end + } + + +} diff --git a/.config/nvim/lua/editor.lua b/.config/nvim/lua/editor.lua new file mode 100644 index 0000000..05eb9f8 --- /dev/null +++ b/.config/nvim/lua/editor.lua @@ -0,0 +1,90 @@ +return { + + -- Detect tabstop and shiftwidth automatically + "tpope/vim-sleuth", + + -- "gc" to comment visual regions/lines + "numToStr/Comment.nvim", + + -- Highlight, edit, and navigate code + { + "nvim-treesitter/nvim-treesitter", + dependencies = { + "nvim-treesitter/nvim-treesitter-textobjects", + }, + build = ":TSUpdate", + opts = { + ensure_installed = { + "bash", "c", "cmake", "cpp", "c_sharp", "css", "go", + "haskell", "html", "java", "javascript", "jsdoc", "json", + "latex", "lua", "make", "markdown", "php", "python", + "query", "regex", "rust", "toml", "tsx", "typescript", + "vim", "vimdoc", "yaml", + }, + sync_install = false, + auto_install = true, + + -- Default install directory is /parser + -- parser_install_dir + + highlight = { + enable = true, + additional_vim_regex_highlighting = false, + }, + indent = { + enable = true, + }, + } + }, + + -- Auto-save + { + "okuuva/auto-save.nvim", + cmd = "ASToggle", -- defer, until run command + event = { "InsertLeave", "TextChanged" }, -- defer, until event trigger + opts = { + execution_message = { + enabled = true, + message = function() + return [["]] .. vim.fn.bufname() .. [[" written]] + end, + }, + debounce_delay = 5000, -- delay for `defer_save`, in ms + condition = function(buf) + -- Dont save special-buffers + return vim.fn.getbufvar(buf, "&buftype") == "" + end, + }, + config = function(_, opts) + require("auto-save").setup(opts) + + -- Cut off trailing whitespace and trailing blank lines + local group = vim.api.nvim_create_augroup("AutoSave", { clear = false }) + local core = require("core.functions") + vim.api.nvim_create_autocmd("User", { + pattern = "AutoSaveWritePre", + group = group, + callback = function(event) + if event.data.saved_buffer ~= nil then + core.trim_buffer() + end + end, + }) + end, + }, + + -- Autopair / electric pair + { -- https://github.com/altermo/ultimate-autopair.nvim + "altermo/ultimate-autopair.nvim", + event = { "InsertEnter", "CmdlineEnter" }, -- defer + branch = "development", + opts = { + bs = { -- See: `:h ultimate-autopair-map-backspace-config` + -- Call the backspace logic from the config, instead of automap + -- https://github.com/altermo/ultimate-autopair.nvim/issues/60 + map = "ultimate-autopair-BS", + }, + }, + }, + +} diff --git a/.config/nvim/lua/git.lua b/.config/nvim/lua/git.lua new file mode 100644 index 0000000..a530508 --- /dev/null +++ b/.config/nvim/lua/git.lua @@ -0,0 +1,33 @@ +return { + + -- Adds git related signs to the gutter/fringe + { + "lewis6991/gitsigns.nvim", + opts = { + -- See `:help gitsigns.txt` + signs = { + add = { text = "█" }, + change = { text = "█" }, + delete = { text = "█" }, + topdelete = { text = "█" }, + changedelete = { text = "█" }, + untracked = { text = "┆" }, + }, + }, + }, + + -- Magit clone + { + "NeogitOrg/neogit", + cmd = "Neogit", -- defer + lazy = true, + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-telescope/telescope.nvim", + + "sindrets/diffview.nvim", -- diff integration + }, + opts = {}, + }, + +} diff --git a/.config/nvim/lua/keybind-functions.lua b/.config/nvim/lua/keybind-functions.lua new file mode 100644 index 0000000..7fe7592 --- /dev/null +++ b/.config/nvim/lua/keybind-functions.lua @@ -0,0 +1,309 @@ +local builtin +local telescope + +local F = require("core.functions") + +local M = {} + +M.setup = function() + -- Prevent dependency loop + builtin = require("telescope.builtin") + telescope = require("telescope") +end + +-- Set which-key key section description +M.wk = function(keybind, description, bufnr) + require("which-key").register({ + [keybind] = { name = description, _ = "which_key_ignore" }, + }, { mode = { "n", "v" }, buffer = bufnr }) -- bufnr = nil means global +end + +M.buffer_close = function() + -- Check if there are more than 2 buffers and alternate file is set + local command = vim.fn.expand("#") ~= "" and #vim.fn.getbufinfo({ buflisted = 1 }) > 1 + and "e #|bd #|bwipeout #" -- see `:h c_#` + or "bd" + vim.api.nvim_command(command) +end + +M.buffer_dashboard = function() + vim.api.nvim_command("Dashboard") +end + +M.file_config = function() + builtin.find_files({ + prompt_title = "Nvim Config", + cwd = "~/.config/nvim", + hidden = true, + no_ignore = true, + -- path_display = { "shorten" }, + }) +end + +M.file_find = function() + builtin.find_files({ + cwd = F.get_current_directory(), + hidden = true, + no_ignore = true, + }) +end + +M.file_find_home = function() + builtin.find_files({ + cwd = "~", + hidden = true, + no_ignore = true, + }) +end + +M.file_find_root = function() + local cwd = F.get_current_directory():gsub("^/", "") + builtin.find_files({ default_text = cwd, cwd = "/" }) +end + +M.file_find_recent = function() + telescope.extensions.recent_files.pick() +end + +M.file_save = function() + vim.api.nvim_command("w") +end + +M.goto_trouble = function() + vim.api.nvim_command("Trouble") +end + +M.goto_todos_telescope = function() + vim.api.nvim_command("TodoTelescope") +end + +M.goto_todos_trouble = function() + vim.api.nvim_command("TodoTrouble") +end + +-- Smart delete +M.hungry_delete = function() + return function() + local start_line = vim.fn.line(".") + local start_col = vim.fn.col(".") + local start_line_content = vim.fn.getline(start_line) + local line_length = start_line_content:len() + + -- Check if there are multiple whitespace characters after the cursor + local is_1_whitespace = start_line_content:sub(start_col, start_col):match("%s") + local is_2_whitespace = start_line_content:sub(start_col + 1, start_col + 1):match("%s") + if (start_col <= line_length - 1 and is_1_whitespace and is_2_whitespace) + or (start_col == line_length and is_1_whitespace) + or start_col - 1 == line_length then -- in insert mode, the column is 1 higher + -- Hungry delete + + -- Iterate forwards through each line and character + local last_line = vim.fn.line("$") + for line = start_line, last_line do + local line_content = (line == start_line) and start_line_content or vim.fn.getline(line) + local col = (line == start_line) and start_col or 1 + + for c = col, #line_content do + local char = line_content:sub(c, c) + + -- Look until it hits a non-whitespace character + if not vim.fn.strcharpart(char, 0, 1):match("%s") then + -- Delete empty lines, from the starting line to the current line + vim.api.nvim_buf_set_lines(0, start_line - 1, line - 1, false, {}) + + -- Keep the content that was before the cursor at the start_line, + -- plus the content from the current column to the end of the current line + vim.fn.setline(start_line, + start_line_content:sub(1, start_col - 1) .. line_content:sub(c)) + + vim.fn.cursor({ start_line, start_col }) + return + end + end + end + + -- Delete empty lines, if the end of the buffer was only whitespace + vim.api.nvim_buf_set_lines(0, start_line - 1, last_line, false, {}) + + -- Keep the content that was before the cursor + vim.fn.setline(start_line, start_line_content:sub(1, start_col - 1)) + + vim.fn.cursor({ start_line, start_col }) + else + -- Normal delete + vim.cmd("normal! x") + end + end +end + +-- Smart backspace +M.hungry_delete_backspace = function() + -- Cache some values + local core = require("ultimate-autopair.core") + local keycode = vim.api.nvim_replace_termcodes("ultimate-autopair-BS", true, false, true) + local fallback = vim.api.nvim_replace_termcodes("", true, false, true) -- => , \b + + return function() + local start_line = vim.fn.line(".") + local start_col = vim.fn.col(".") + local start_line_content = vim.fn.getline(start_line) + + -- Check if there are multiple whitespace characters before the cursor + local is_1_whitespace = start_line_content:sub(start_col - 1, start_col - 1):match("%s") + local is_2_whitespace = start_line_content:sub(start_col - 2, start_col - 2):match("%s") + if (start_col >= 3 and is_1_whitespace and is_2_whitespace) + or (start_col == 2 and is_1_whitespace) + or start_col == 1 then + -- Hungry delete + + -- Iterate backwards through each line and character + for line = start_line, 1, -1 do + local line_content = (line == start_line) and start_line_content or vim.fn.getline(line) + local col = (line == start_line) and (start_col - 1) or #line_content + + for c = col, 1, -1 do + local char = line_content:sub(c, c) + + -- Look until it hits a non-whitespace character + if not vim.fn.strcharpart(char, 0, 1):match("%s") then + -- Delete empty lines, from the starting line to the current line + vim.api.nvim_buf_set_lines(0, line - 1, start_line - 1, false, {}) + + -- Keep the content from the start of the current line to the current column, + -- plus the content that was after the cursor at the start_line + vim.fn.setline(line, line_content:sub(1, c) .. (line == start_line + and line_content:sub(start_col) -- found non-whitespace on the same line it started + or start_line_content:sub(start_col))) -- jumped through empty lines + + vim.fn.cursor({ line, c + 1 }) + return + end + end + end + + -- Delete empty lines, if the start of the buffer was only whitespace + vim.api.nvim_buf_set_lines(0, 0, start_line - 1, false, {}) + + -- Keep the content that was after the cursor + vim.fn.setline(1, "" .. start_line_content:sub(start_col)) + else + -- Normal backspace, prevent run_run recursive mapping via fallback to \b + local actions = core.run_run(keycode) + vim.api.nvim_feedkeys(actions ~= keycode and actions or fallback, "n", false) -- equivalent to { expr = true } + end + end +end + +M.lsp_format_buffer = function() + vim.lsp.buf.format({ + formatting_options = { + tabSize = 4, + insertSpaces = false, + }, + }) +end + +M.project_file = function() + local res, err = F.find_project_root() + if res then + builtin.git_files({ cwd = res }) + else -- fall-back to find_files, if current buffer isnt in a project + builtin.find_files({ cwd = err }) + end +end + +M.project_grep = function() + local res, err = F.find_project_root() + builtin.live_grep({ cwd = res or err }) +end + +M.project_list = function() + telescope.extensions.projects.projects() +end + +M.search_buffer = function() + builtin.current_buffer_fuzzy_find({ + previewer = false, + sorting_strategy = "ascending", + }) +end + +-- TODO: Temporary copy/pasted until project_nvim exposes this function +-- https://github.com/ahmedkhalf/project.nvim/issues/145 +-- TODO: Create a Telescope extension out of this, for telescope-all-recent +local function create_finder() + local results = require("project_nvim").get_recent_projects() + local entry_display = require("telescope.pickers.entry_display") + local finders = require("telescope.finders") + + -- Reverse results + for i = 1, math.floor(#results / 2) do + results[i], results[#results - i + 1] = results[#results - i + 1], results[i] + end + local displayer = entry_display.create({ + separator = " ", + items = { + { + width = 30, + }, + { + remaining = true, + }, + }, + }) + + local function make_display(entry) + return displayer({ entry.name, { entry.value, "Comment" } }) + end + + return finders.new_table({ + results = results, + entry_maker = function(entry) + local name = vim.fn.fnamemodify(entry, ":t") + return { + display = make_display, + name = name, + value = entry, + ordinal = name .. " " .. entry, + } + end, + }) +end + +M.vc_select_repo = function() + -- local projects = require("project_nvim").get_recent_projects() + + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + local conf = require("telescope.config").values + -- local finders = require("telescope.finders") + local pickers = require("telescope.pickers") + pickers.new({}, { + prompt_title = "Select Project", + finder = create_finder(), + -- finder = telescope.extensions.projects.create_finder(), + -- finder = finders.new_table { + -- results = projects, + -- }, + previewer = false, + sorter = conf.generic_sorter({}), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + + local selection = action_state.get_selected_entry() + -- require("neogit").open({ cwd = selection[1] }) + require("neogit").open({ cwd = selection.value }) + end) + + return true + end, + }):find() +end + +M.vc_status = function() + -- Open the repository of the current file + require("neogit").open({ cwd = "%:p:h" }) +end + +return M diff --git a/.config/nvim/lua/keybinds.lua b/.config/nvim/lua/keybinds.lua new file mode 100644 index 0000000..764dc0d --- /dev/null +++ b/.config/nvim/lua/keybinds.lua @@ -0,0 +1,231 @@ +local K = vim.keymap.set + +local builtin +local wk = require("which-key") + +local F = require("keybind-functions") + +local M = {} + +-------------------------------------------- +--- Keybindings --- + +M.setup = function() + F.setup() + + -- Prevent dependency loop + builtin = require("telescope.builtin") + + -- Change default behavior + K("n", "Y", "y$") + K("v", "p", "pgvy") + + K("n", "J", "mzJ`z") -- dont move cursor to end of line + + K("n", "", "zz") -- stay in the middle + K("n", "", "zz") + K("n", "n", "nzzzv") + K("n", "N", "Nzzzv") + + K("x", "p", "\"_dP") -- dont overwrite clipboard when pasting visually + + -- Remap for dealing with word wrap + K("n", "k", [[v:count == 0 ? "gk" : "k"]], { expr = true, silent = true }) + K("n", "j", [[v:count == 0 ? "gj" : "j"]], { expr = true, silent = true }) + + -- Tab/Shift+Tab functionality + K("n", "", ">>_") + K("n", "", "<<_") + K("i", "", "") + K("v", "", ">gv") + K("v", "", "+1gv=gv") + K("v", "K", ":m '<-2gv=gv") + + -- Switch to previous buffer + K("n", "", "") + + -- Center buffer with Ctrl+L + K("n", "", "zz") + + -- Close buffer with Alt-w + K("n", "", F.buffer_close) + + -- Hungry delete + K("i", "", F.hungry_delete_backspace()) + K("i", "", F.hungry_delete()) + + ---------------------------------------- + --- Leader keys --- + + + K("n", "", builtin.commands, { desc = "Execute command" }) + + + -- F.wk("b", "buffer/bookmark") + K("n", "bb", builtin.buffers, { desc = "Switch buffer" }) + K("n", "bd", F.buffer_dashboard, { desc = "Dashboard" }) + + + F.wk("c", "comment") + -- numToStr/Comment.nvim + K("n", "cc", "(comment_toggle_linewise_current)", { desc = "Comment toggle linewise" }) + K("n", "cp", "(comment_toggle_blockwise_current)", { desc = "Comment toggle blockwise" }) + K("x", "cc", "(comment_toggle_linewise_visual)", { desc = "Comment toggle linewise (visual)" }) + K("x", "cp", "(comment_toggle_blockwise_visual)", { desc = "Comment toggle blockwise (visual)" }) + + + F.wk("e", "eval") + K("n", "eb", ":w:source %", { desc = "Evaluate buffer" }) + + + F.wk("f", "file") + K("n", "fc", F.file_config, { desc = "Config file" }) + K("n", "ff", F.file_find, { desc = "Find file" }) + K("n", "fh", F.file_find_home, { desc = "Find file in ~" }) + K("n", "f/", F.file_find_root, { desc = "Find file in root" }) + K("n", "fr", F.file_find_recent, { desc = "Find recent file" }) + K("n", "fs", F.file_save, { desc = "Save file" }) + + + F.wk("h", "help") + K("n", "hh", builtin.help_tags, { desc = "Describe" }) + K("n", "hk", builtin.keymaps, { desc = "Describe key" }) + K("n", "hm", builtin.man_pages, { desc = "Describe manpage" }) + + + F.wk("g", "goto") + K("n", "ge", F.goto_trouble, { desc = "Goto trouble" }) + K("n", "gt", F.goto_todos_telescope, { desc = "Goto todos (Telescope)" }) + K("n", "gT", F.goto_todos_trouble, { desc = "Goto todos (Trouble)" }) + + + F.wk("p", "project") + K("n", "pf", F.project_file, { desc = "Find project file" }) + K("n", "pg", F.project_grep, { desc = "Find in project" }) + K("n", "pp", F.project_list, { desc = "List projects" }) + + + F.wk("s", "search") + K("n", "ss", F.search_buffer, { desc = "Search buffer" }) + K("n", "sq", ":nohlsearch", { desc = "Stop search", silent = true }) + + + F.wk("v", "git") -- version control + K("n", "vr", F.vc_select_repo, { desc = "Select repo" }) + K("n", "vv", F.vc_status, { desc = "Neogit status" }) + + + K({ "n", "v" }, "w", "", { remap = true }) -- keymap and which-key should *both* be triggered + wk.register({ + w = { + name = "window", + s = "Split window", + v = "Split window vertically", + w = "Switch windows", + q = "Quit a window", + o = "Close all other windows", + T = "Break out into a new tab", + x = "Swap current with next", + ["-"] = "Decrease height", + ["+"] = "Increase height", + [""] = "Decrease width", + [">"] = "Increase width", + ["|"] = "Max out the width", + ["_"] = "Max out the height", + ["="] = "Equally high and wide", + h = "Go to the left window", + l = "Go to the right window", + k = "Go to the up window", + j = "Go to the down window", + }, + }, { mode = { "n", "v" }, prefix = "", preset = true }) + -- https://github.com/folke/which-key.nvim/issues/270 + -- https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/misc.lua +end + +-------------------------------------------- +--- Plugin specific --- + +-- This function gets run when an LSP connects to a particular buffer. +M.lspconfig_on_attach = function(_, bufnr) + local nnoremap = function(keys, func, desc) + vim.keymap.set("n", keys, func, { buffer = bufnr, desc = desc }) + end + + F.wk("l", "lsp", bufnr) + nnoremap("la", vim.lsp.buf.code_action, "Code action") + nnoremap("lf", F.lsp_format_buffer, "Format buffer") + nnoremap("lr", vim.lsp.buf.rename, "Rename") + + F.wk("lg", "goto", bufnr) + nnoremap("lga", builtin.lsp_dynamic_workspace_symbols, "Workspace symbols") + nnoremap("lgd", vim.lsp.buf.declaration, "Declaration") + nnoremap("lgg", vim.lsp.buf.definition, "Definition") -- builtin.lsp_definitions + nnoremap("lgi", builtin.lsp_implementations, "Implementation") + nnoremap("lgr", builtin.lsp_references, "References") + nnoremap("lgs", builtin.lsp_document_symbols, "Document symbols") + nnoremap("lgt", builtin.lsp_type_definitions, "Type definition") + + -- See `:help K` for why this keymap + nnoremap("K", vim.lsp.buf.hover, "Hover Documentation") + nnoremap("", vim.lsp.buf.signature_help, "Signature Documentation") +end + +-- Keybindings for nvim-cmp popup +M.nvim_cmp = function() + local cmp = require("cmp") + local luasnip = require("luasnip") + return cmp.mapping.preset.insert({ + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.select_next_item(), + [""] = cmp.mapping.select_prev_item(), + [""] = cmp.mapping.confirm({ + behavior = cmp.ConfirmBehavior.Replace, + select = true, + }), + + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm({ + behavior = cmp.ConfirmBehavior.Replace, + select = true, + }), + [""] = cmp.mapping.complete(), + + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_locally_jumpable() then + luasnip.expand_or_jump() + else + fallback() + end + end, { "i", "s" }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.locally_jumpable(-1) then + luasnip.jump(-1) + else + fallback() + end + end, { "i", "s" }), + }) +end + +-- Telescope picker generic mappings +M.telescope_default_mappings = function() + local actions = require("telescope.actions") + return { + i = { + [""] = actions.close, + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + [""] = actions.select_default, + } + } +end + +return M diff --git a/.config/nvim/lua/packages.lua b/.config/nvim/lua/packages.lua new file mode 100644 index 0000000..7650945 --- /dev/null +++ b/.config/nvim/lua/packages.lua @@ -0,0 +1,32 @@ +local M = {} + +local plugin_path = vim.fn.stdpath("cache") .. "/lazy" + +-- Install lazy.nvim plugin manager +-- See `:help lazy.nvim.txt` +M.install = function() + local lazy_path = plugin_path .. "/lazy.nvim" + if not vim.loop.fs_stat(lazy_path) then + vim.fn.system { + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", -- latest stable release + lazy_path, + } + end + vim.opt.rtp:prepend(lazy_path) +end + +M.setup = function(modules) + M.install() + + local options = { + root = plugin_path, -- directory where plugins will be installed + } + + require("lazy").setup(modules, options) +end + +return M diff --git a/.config/nvim/lua/selection.lua b/.config/nvim/lua/selection.lua new file mode 100644 index 0000000..1e1e3ff --- /dev/null +++ b/.config/nvim/lua/selection.lua @@ -0,0 +1,122 @@ +return { + + -- Fuzzy Finder (files, LSP, etc) + { + "nvim-telescope/telescope.nvim", + branch = "0.1.x", + dependencies = { + "nvim-lua/plenary.nvim", + -- Fuzzy Finder Algorithm which requires local dependencies to be built. + { + "nvim-telescope/telescope-fzf-native.nvim", + build = "make", + cond = function() + return vim.fn.executable "make" == 1 + end, + }, + -- Extensions + "smartpde/telescope-recent-files", + }, + extensions = { + "fzf", + }, + config = function() + require("telescope").setup({ + defaults = { + sorting_strategy = "ascending", + + layout_strategy = "config", + layout_config = { + height = 10, -- amount of results + search_condensed = true, + }, + border = true, + + history = { + path = vim.fn.stdpath("cache") .. "/telescope_history", + }, + + mappings = require("keybinds").telescope_default_mappings() + }, + }) + require("telescope").load_extension("fzf") + require("telescope").load_extension("recent_files") + + --- ┌──────────────────────────────────────────────────┐ + --- │ │ + --- │ ┌───────────────────┐ │ + --- │ │ │ │ + --- │ │ │ │ + --- │ │ Preview │ │ + --- │ │ │ │ + --- │ │ │ │ + --- │ └───────────────────┘ │ + --- │ │ + --- ├──────────────────────────────────────────────────┤ + --- │ Prompt │ + --- ├──────────────────────────────────────────────────┤ + --- │ Results │ + --- │ │ + --- └──────────────────────────────────────────────────┘ + require("telescope.pickers.layout_strategies").config = function(picker, max_columns, max_lines, + layout_config) + local p_window = require "telescope.pickers.window" + local initial_options = p_window.get_initial_window_options(picker) + local results = initial_options.results + local prompt = initial_options.prompt + local preview = initial_options.preview + -- Layout config isnt filled by Telescope automatically + layout_config = require("telescope.config").values.layout_config or {} + + results.title = "" + results.borderchars = { "─", "│", "─", "│", "├", "┤", "╯", "╰" } + + local bs = picker.window.border and 2 or 0 + local search_condensed = bs ~= 0 and layout_config.search_condensed and 2 or 0 + + -- Height + prompt.height = 1 + results.height = layout_config.height or 10 + local search_height = (prompt.height + results.height + (bs * 3)) + preview.height = math.floor((max_lines - search_height) * 0.8) + + -- Width + prompt.width = max_columns - (bs ~= 0 and search_condensed == 0 and bs or 0) + results.width = max_columns - (bs ~= 0 and search_condensed == 0 and bs or 0) + preview.width = math.floor(max_columns * 0.5) + + -- Line (position), coordinates start at top-left + prompt.line = max_lines - results.height + search_condensed - bs -- take overlapping border into account + results.line = max_lines - results.height + search_condensed - bs + (bs / 2) + prompt.height + preview.line = math.floor((max_lines - search_height - preview.height + bs) / 2) + 1 + + return { + preview = picker.previewer and preview.width > 0 and preview, + prompt = prompt, + results = results, + } + end + end, + }, + + -- Telescope pickers sorted by recency and frequency + { + "prochri/telescope-all-recent.nvim", + dependencies = { + "kkharji/sqlite.lua", + }, + opts = { + database = { + folder = vim.fn.stdpath("cache"), + }, + pickers = { -- extension_name#extension_method + ["projects#projects"] = { + disable = false, + use_cwd = false, + sorting = "frecency", + }, + }, + } + }, + +} diff --git a/.config/nvim/lua/ui.lua b/.config/nvim/lua/ui.lua new file mode 100644 index 0000000..193ae08 --- /dev/null +++ b/.config/nvim/lua/ui.lua @@ -0,0 +1,157 @@ +return { + + -- Theme + { + "RRethy/nvim-base16", + lazy = false, -- make sure to load this during startup if it is your main colorscheme + priority = 1000, + config = function() + require("base16-colorscheme").with_config({ + -- TODO: maybe make Telescope borders visible? + -- telescope = false, + }) + vim.cmd.colorscheme "base16-tomorrow-night" + + local colors = require("base16-colorscheme").colors + local blue = colors.base0D -- #81a2be + local cyan = colors.base0C -- #8abeb7 + local fg = colors.base05 -- #c5c8c6 + local green_dark = "#8c9440" + local red = colors.base08 -- #cc6666 + local yellow = colors.base0A -- #f0c674 + + -- Cursor + vim.api.nvim_command("highlight CursorLineNr guifg=" .. yellow .. " gui=bold") + -- Git gutter + vim.api.nvim_command("highlight GitSignsAdd guifg=" .. green_dark) + vim.api.nvim_command("highlight GitSignsChange guifg=" .. yellow) + -- Rainbow delimiters + vim.api.nvim_command("highlight RainbowDelimiterBlue guifg=" .. blue) + vim.api.nvim_command("highlight RainbowDelimiterCyan guifg=" .. cyan) + vim.api.nvim_command("highlight RainbowDelimiterGreen guifg=" .. green_dark) + vim.api.nvim_command("highlight RainbowDelimiterOrange guifg=" .. fg) + vim.api.nvim_command("highlight RainbowDelimiterRed guifg=" .. red) + vim.api.nvim_command("highlight RainbowDelimiterYellow guifg=" .. yellow) + end, + }, + + -- Set lualine as statusline + { + -- See `:help lualine.txt` + "nvim-lualine/lualine.nvim", + opts = { + options = { + icons_enabled = false, + theme = "base16", + component_separators = { left = "", right = "" }, + section_separators = { left = "", right = "" }, + globalstatus = true, + }, + sections = { + lualine_b = { "branch" }, + lualine_x = { "diagnostics", "encoding", "fileformat", "filetype" }, + }, + }, + }, + + -- Add file type icons to plugins + { "nvim-tree/nvim-web-devicons", lazy = true }, + + -- Dashboard + { + "nvimdev/dashboard-nvim", + event = "VimEnter", + opts = { + config = { + header = { + -- " ", + -- " ███╗ ██╗███████╗ ██████╗ ██╗ ██╗██╗███╗ ███╗ ", + -- " ████╗ ██║██╔════╝██╔═══██╗██║ ██║██║████╗ ████║ ", + -- " ██╔██╗ ██║█████╗ ██║ ██║██║ ██║██║██╔████╔██║ ", + -- " ██║╚██╗██║██╔══╝ ██║ ██║╚██╗ ██╔╝██║██║╚██╔╝██║ ", + -- " ██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║ ", + -- " ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ ", + -- "____________________________________________________", + " . . ", + " ';;,. ::' ", + " ,:::;,, :ccc, ", + " ,::c::,,,,. :cccc, ", + " ,cccc:;;;;;. cllll, ", + " ,cccc;.;;;;;, cllll; ", + " :cccc; .;;;;;;. coooo; ", + " ;llll; ,:::::'loooo; ", + " ;llll: ':::::loooo: ", + " :oooo: .::::llodd: ", + " .;ooo: ;cclooo:. ", + " .;oc 'coo;. ", + " .' .,. ", + "____________________________________________________", + "", + }, + shortcut = { + { desc = "Neovim master race!" }, + }, + footer = {}, + }, + } + }, + + -- Show project errors + { + "folke/trouble.nvim", + cmd = "Trouble", -- defer + opts = { + mode = "lsp_references", + use_diagnostic_signs = true, + }, + config = function() + -- Gutter/fringe icons + -- https://github.com/folke/trouble.nvim/issues/52 + local signs = { + Error = "»", + Warn = "»", + Hint = "»", + Info = "»", + Other = "»", + } + for type, icon in pairs(signs) do + local hl = "DiagnosticSign" .. type + vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl }) + end + end, + }, + + -- Show project TODOs + { + "folke/todo-comments.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + opts = {}, + }, + + -- Useful plugin to show you pending keybinds. + "folke/which-key.nvim", + + -- Rainbow delimiters + { + "HiPhish/rainbow-delimiters.nvim", + opts = { + highlight = { + "RainbowDelimiterOrange", -- white + "RainbowDelimiterCyan", + "RainbowDelimiterYellow", + "RainbowDelimiterGreen", + "RainbowDelimiterBlue", + "RainbowDelimiterOrange", -- white + "RainbowDelimiterCyan", + "RainbowDelimiterYellow", + "RainbowDelimiterGreen", + "RainbowDelimiterBlue", + "RainbowDelimiterRed", + }, + }, + config = function(_, opts) + require("rainbow-delimiters.setup").setup(opts) + end, + }, + +} diff --git a/.config/zsh/.zprofile b/.config/zsh/.zprofile index 40e54a4..6482c6d 100644 --- a/.config/zsh/.zprofile +++ b/.config/zsh/.zprofile @@ -92,7 +92,7 @@ export SUDO_ASKPASS="$HOME/.local/bin/rofipass" export TERMINAL="urxvt" # Vim -export VIMINIT="source $XDG_CONFIG_HOME/vim/vimrc" +export VIMINIT="source $XDG_CONFIG_HOME/nvim/init.lua" # Web browser export BROWSER="firefox"