diff --git a/nvim b/nvim
deleted file mode 160000
index 652e95b..0000000
--- a/nvim
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 652e95b4c17c4737ef6a214fbe4473e29342da9a
diff --git a/nvim/.gitignore b/nvim/.gitignore
new file mode 100644
index 0000000..f8e46c0
--- /dev/null
+++ b/nvim/.gitignore
@@ -0,0 +1,8 @@
+tags
+en.utf-8.add.spl
+*.exe
+.netrwhist
+*.log
+.DS_Store
+lazy-lock.json
+.env*
diff --git a/nvim/.stylua.toml b/nvim/.stylua.toml
new file mode 100644
index 0000000..92bda2a
--- /dev/null
+++ b/nvim/.stylua.toml
@@ -0,0 +1,8 @@
+# config for stylua, ref: https://github.com/JohnnyMorganz/StyLua
+syntax = "Lua51"
+column_width = 100
+line_endings = "Unix"
+indent_type = "Spaces"
+indent_width = 2
+quote_style = "AutoPreferDouble"
+call_parentheses= "NoSingleTable"
diff --git a/nvim/LICENSE b/nvim/LICENSE
new file mode 100644
index 0000000..eebf2a3
--- /dev/null
+++ b/nvim/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2023 jdhao
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/nvim/README.md b/nvim/README.md
new file mode 100644
index 0000000..c836712
--- /dev/null
+++ b/nvim/README.md
@@ -0,0 +1,209 @@
+
+
+# Introduction
+
+This repo hosts my Nvim configuration for Linux, macOS, and Windows.
+`init.lua` is the config entry point for terminal Nvim,
+and `ginit.vim` is the additional config file for [GUI client of Nvim](https://github.com/neovim/neovim/wiki/Related-projects#gui).
+
+My configurations are heavily documented to make it as clear as possible.
+While you can clone the whole repository and use it, it is not recommended though.
+Good configurations are personal. Everyone should have his or her unique config file.
+You are encouraged to copy from this repo the part you want and add it to your own config.
+
+To reduce the possibility of breakage, **this config is only maintained for [the latest nvim stable release](https://github.com/neovim/neovim/releases/tag/stable).
+No effort is spent on maintaining backward compatibility.**
+
+# Install and setup
+
+See [doc here](docs/README.md) on how to install Nvim's dependencies, Nvim itself,
+and how to set up on different platforms (Linux, macOS, and Windows).
+
+# Features #
+
++ Plugin management via [Lazy.nvim](https://github.com/folke/lazy.nvim).
++ Code, snippet, word auto-completion via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp).
++ Language server protocol (LSP) support via [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig).
++ Git integration via [vim-fugitive](https://github.com/tpope/vim-fugitive).
++ Better escaping from insert mode via [better-escape.vim](https://github.com/nvim-zh/better-escape.vim).
++ Ultra-fast project-wide fuzzy searching via [fzf-lua](https://github.com/ibhagwan/fzf-lua).
++ Faster code commenting via [vim-commentary](https://github.com/tpope/vim-commentary).
++ Faster matching pair insertion and jump via [nvim-autopairs](https://github.com/windwp/nvim-autopairs).
++ Smarter and faster matching pair management (add, replace or delete) via [vim-sandwich](https://github.com/machakann/vim-sandwich).
++ Fast buffer jump via [hop.nvim](https://github.com/smoka7/hop.nvim).
++ Powerful snippet insertion via [Ultisnips](https://github.com/SirVer/ultisnips).
++ Beautiful statusline via [lualine.nvim](https://github.com/nvim-lualine/lualine.nvim).
++ File tree explorer via [nvim-tree.lua](https://github.com/nvim-tree/nvim-tree.lua).
++ Better quickfix list with [nvim-bqf](https://github.com/kevinhwang91/nvim-bqf).
++ Show search index and count with [nvim-hlslens](https://github.com/kevinhwang91/nvim-hlslens).
++ User-defined mapping hint via [which-key.nvim](https://github.com/folke/which-key.nvim).
++ Asynchronous code execution via [asyncrun.vim](https://github.com/skywind3000/asyncrun.vim).
++ Code highlighting via [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter).
++ Code editing using true nvim inside browser via [firenvim](https://github.com/glacambre/firenvim).
++ Beautiful colorscheme via [sainnhe/gruvbox-material](https://github.com/sainnhe/gruvbox-material) and other colorschemes.
++ Markdown previewing via [render-markdown.nvim](https://github.com/MeanderingProgrammer/render-markdown.nvim)
++ LaTeX editing and previewing via [vimtex](https://github.com/lervag/vimtex)
++ Animated GUI style notification via [nvim-notify](https://github.com/rcarriga/nvim-notify).
++ Tags navigation via [vista](https://github.com/liuchengxu/vista.vim).
++ Code folding with [nvim-ufo](https://github.com/kevinhwang91/nvim-ufo) and [statuscol.nvim](https://github.com/luukvbaal/statuscol.nvim)
++ ......
+
+# UI Demo
+
+For more UI demos, check [here](https://github.com/jdhao/nvim-config/issues/15).
+
+## Start screen with dashboard-nvim
+
+
+
+
+
+## File fuzzy finding using fzf-lua
+
+
+
+
+
+## Code autocompletion with nvim-cmp
+
+
+
+
+
+## Git add, commit and push via fugitive.vim
+
+
+
+
+
+## Tags
+
+
+
+
+
+## Cursor jump via hop.nvim
+
+Go to a string starting with `se`
+
+
+
+
+
+## GUI-style notification with nvim-notify
+
+
+
+
+
+## code folding with nvim-ufo and statuscol.nvim
+
+
+
+
+
+# Shortcuts
+
+Some of the shortcuts I use frequently are listed here. In the following shortcuts, `` represents ASCII character `,`.
+
+| Shortcut | Mode | platform | Description |
+|-------------------|---------------|-----------------|--------------------------------------------------------------------------|
+| `ff` | Normal | Linux/macOS/Win | Fuzzy file searching in a floating window |
+| `fh` | Normal | Linux/macOS/Win | Fuzzy help file grepping in a floating window |
+| `fg` | Normal | Linux/macOS/Win | Fuzzy project-wide grepping in a floating window |
+| `ft` | Normal | Linux/macOS/Win | Fuzzy buffer tag searching in a floating window |
+| `fb` | Normal | Linux/macOS/Win | Fuzzy buffer switching in a floating window |
+| `` | Normal | Linux/macOS/Win | Remove trailing white spaces |
+| `v` | Normal | Linux/macOS/Win | Reselect last pasted text |
+| `ev` | Normal | Linux/macOS/Win | Edit Nvim config in a new tabpage |
+| `sv` | Normal | Linux/macOS/Win | Reload Nvim config |
+| `q` | Normal | Linux/macOS/Win | Quit current window |
+| `Q` | Normal | Linux/macOS/Win | Quit all window and close Nvim |
+| `w` | Normal | Linux/macOS/Win | Save current buffer content |
+| `y` | Normal | Linux/macOS/Win | Copy the content of entire buffer to default register |
+| `cl` | Normal | Linux/macOS/Win | Toggle cursor column |
+| `cd` | Normal | Linux/macOS/Win | Change current working directory to to the dir of current buffer |
+| `t` | Normal | Linux/macOS/Win | Toggle tag window (show project tags in the right window) |
+| `gs` | Normal | Linux/macOS/Win | Show Git status result |
+| `gw` | Normal | Linux/macOS/Win | Run Git add for current file |
+| `gc` | Normal | Linux/macOS/Win | Run git commit |
+| `gpl` | Normal | Linux/macOS/Win | Run git pull |
+| `gpu` | Normal | Linux/macOS/Win | Run git push |
+| `gbd` | Normal | Linux/macOS/Win | Delete a branch |
+| `gbn` | Normal | Linux/macOS/Win | Create a new branch |
+| `gl` | Normal/Visual | Linux/macOS/Win | Get perm link for current/visually-select lines |
+| `gbr` | Normal | macOS | Browse current git repo in browser |
+| `gb` | Visual | macOS | Blame current line |
+| `` | Normal | Linux/macOS/Win | Compile&run current source file (for C++, LaTeX, Lua, Python) |
+| `` | Normal | Linux/macOS/Win | Toggle spell checking |
+| `` | Normal | Linux/macOS/Win | Toggle paste mode |
+| `\x` | Normal | Linux/macOS/Win | Close location or quickfix window |
+| `\d` | Normal | Linux/macOS/Win | Close current buffer and go to previous buffer |
+| `{count}gb` | Normal | Linux/macOS/Win | Go to buffer `{count}` or next buffer in the buffer list. |
+| `{operator}iB` | Normal | Linux/macOS/Win | Operate in the whole buffer, `{operator}` can be `v`, `y`, `c`, `d` etc. |
+| `Alt-k` | Normal | Linux/macOS/Win | Move current line or selected lines up |
+| `Alt-j` | Normal | Linux/macOS/Win | Move current line or selected lines down |
+| `ctrl-u` | Insert | Linux/macOS/Win | Turn word under cursor to upper case |
+| `ctrl-t` | Insert | Linux/macOS/Win | Turn word under cursor to title case |
+| `jk` | Insert | Linux/macOS/Win | Return to Normal mode without lagging |
+| `ZR` | Normal | Linux/macOS/Win | Restart nvim without quitting |
+
+
+# Custom commands
+
+In addition to commands provided by various plugins, I have also created some custom commands for personal use.
+
+| command | description | example |
+|--------------|-------------------------------------------------------------------------|--------------------------------|
+| `Redir` | capture command output to a tabpage for easier inspection. | `Redir hi` |
+| `Edit` | edit multiple files at the same time, supports globing | `Edit *.vim` |
+| `Datetime` | print current date and time or convert Unix time stamp to date and time | `Datetime 12345` or `Datetime` |
+| `JSONFormat` | format a JSON file | `JSONFormat` |
+| `CopyPath` | copy current file path to clipboard | `CopyPath relative` |
+
+# Contributing
+
+If you find anything that needs improving, do not hesitate to point it out or create a PR.
+
+If you come across an issue, you can first use `:checkhealth` command provided by `nvim` to trouble-shoot yourself.
+Please read carefully the messages provided by health check.
+
+If you still have an issue, [open a new issue](https://github.com/jdhao/nvim-config/issues).
+
+# Further readings
+
+Some of the resources that I find helpful in mastering Nvim is documented [here](docs/nvim_resources.md).
+You may also be interested in my posts on configuring Nvim [here](https://jdhao.github.io/categories/Nvim/).
diff --git a/nvim/_config.yml b/nvim/_config.yml
new file mode 100644
index 0000000..9da9a02
--- /dev/null
+++ b/nvim/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-dinky
\ No newline at end of file
diff --git a/nvim/after/ftplugin/cpp.vim b/nvim/after/ftplugin/cpp.vim
new file mode 100644
index 0000000..2e1cde0
--- /dev/null
+++ b/nvim/after/ftplugin/cpp.vim
@@ -0,0 +1,36 @@
+set commentstring=//\ %s
+
+" Disable inserting comment leader after hitting o or O or
+set formatoptions-=o
+set formatoptions-=r
+
+nnoremap :call compile_run_cpp()
+
+function! s:compile_run_cpp() abort
+ let src_path = expand('%:p:~')
+ let src_noext = expand('%:p:~:r')
+ " The building flags
+ let _flag = '-Wall -Wextra -std=c++11 -O2'
+
+ if executable('clang++')
+ let prog = 'clang++'
+ elseif executable('g++')
+ let prog = 'g++'
+ else
+ echoerr 'No C++ compiler found on the system!'
+ endif
+ call s:create_term_buf('h', 20)
+ execute printf('term %s %s %s -o %s && %s', prog, _flag, src_path, src_noext, src_noext)
+ startinsert
+endfunction
+
+function s:create_term_buf(_type, size) abort
+ set splitbelow
+ set splitright
+ if a:_type ==# 'v'
+ vnew
+ else
+ new
+ endif
+ execute 'resize ' . a:size
+endfunction
diff --git a/nvim/after/ftplugin/gitconfig.vim b/nvim/after/ftplugin/gitconfig.vim
new file mode 100644
index 0000000..9c2e80a
--- /dev/null
+++ b/nvim/after/ftplugin/gitconfig.vim
@@ -0,0 +1 @@
+set commentstring=#\ %s
diff --git a/nvim/after/ftplugin/go.lua b/nvim/after/ftplugin/go.lua
new file mode 100644
index 0000000..d28b7db
--- /dev/null
+++ b/nvim/after/ftplugin/go.lua
@@ -0,0 +1,20 @@
+local opt = vim.opt
+
+-- General tab settings
+opt.tabstop = 4 -- Number of visual spaces per TAB
+opt.softtabstop = 4 -- Number of spaces in tab when editing
+opt.shiftwidth = 4 -- Number of spaces to use for autoindent
+opt.expandtab = false -- Expand tab to spaces so that tabs are spaces
+
+-- gofumpt adds more rule to gofmt, and is compatible with gofmt,
+-- so also https://github.com/mvdan/gofumpt
+vim.keymap.set("n", "f", function()
+ vim.cmd([[silent !gofumpt -w %]])
+end, { buffer = true, silent = true })
+
+vim.keymap.set("n", "", function()
+ vim.cmd([[!go run %]])
+end, {
+ buffer = true,
+ silent = true,
+})
diff --git a/nvim/after/ftplugin/help.lua b/nvim/after/ftplugin/help.lua
new file mode 100644
index 0000000..2422dc6
--- /dev/null
+++ b/nvim/after/ftplugin/help.lua
@@ -0,0 +1,9 @@
+local win_width = vim.api.nvim_win_get_width(0)
+
+-- do not change layout if the screen is not wide enough
+if win_width < 200 then
+ return
+end
+
+-- L means to put window to leftmost
+vim.cmd.wincmd([[L]])
diff --git a/nvim/after/ftplugin/javascript.vim b/nvim/after/ftplugin/javascript.vim
new file mode 100644
index 0000000..3e27ee8
--- /dev/null
+++ b/nvim/after/ftplugin/javascript.vim
@@ -0,0 +1,3 @@
+" Disable inserting comment leader after hitting o or O or
+set formatoptions-=o
+set formatoptions-=r
diff --git a/nvim/after/ftplugin/json.lua b/nvim/after/ftplugin/json.lua
new file mode 100644
index 0000000..3bcf705
--- /dev/null
+++ b/nvim/after/ftplugin/json.lua
@@ -0,0 +1,4 @@
+vim.keymap.set({ "n", "v" }, "f", ":JSONFormat", {
+ buffer = true,
+ silent = true,
+})
diff --git a/nvim/after/ftplugin/lua.lua b/nvim/after/ftplugin/lua.lua
new file mode 100644
index 0000000..ce4f180
--- /dev/null
+++ b/nvim/after/ftplugin/lua.lua
@@ -0,0 +1,11 @@
+-- Disable inserting comment leader after hitting o/O/
+vim.opt_local.formatoptions:remove { "o", "r" }
+
+vim.keymap.set("n", "", "luafile %", {
+ buffer = true,
+ silent = true,
+})
+vim.keymap.set("n", "f", "silent !stylua %", {
+ buffer = true,
+ silent = true,
+})
diff --git a/nvim/after/ftplugin/man.lua b/nvim/after/ftplugin/man.lua
new file mode 100644
index 0000000..b4806d1
--- /dev/null
+++ b/nvim/after/ftplugin/man.lua
@@ -0,0 +1,4 @@
+-- q to quit quickly
+vim.keymap.set("n", "q", "q", {
+ desc = "quit nvim",
+})
diff --git a/nvim/after/ftplugin/markdown.lua b/nvim/after/ftplugin/markdown.lua
new file mode 100644
index 0000000..6fb7463
--- /dev/null
+++ b/nvim/after/ftplugin/markdown.lua
@@ -0,0 +1,107 @@
+local function add_reference_at_end(label, url, title)
+ vim.schedule(function()
+ local bufnr = vim.api.nvim_get_current_buf()
+ local line_count = vim.api.nvim_buf_line_count(bufnr)
+
+ -- Prepare reference definition
+ local ref_def = "[" .. label .. "]: " .. url
+ if title and title ~= "" then
+ ref_def = ref_def .. ' "' .. title .. '"'
+ end
+
+ -- Check if references section exists
+ local buffer_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+
+ local has_ref_section = false
+ for _, line in ipairs(buffer_lines) do
+ if line:match("^%s*[%s]*$") then
+ has_ref_section = true
+ break
+ end
+ end
+
+ local lines_to_add = {}
+ -- Add references header if it doesn't exist
+ if not has_ref_section then
+ if #lines_to_add == 0 then
+ table.insert(lines_to_add, "")
+ end
+ table.insert(lines_to_add, "")
+ end
+
+ table.insert(lines_to_add, ref_def)
+
+ -- Insert at buffer end
+ vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, lines_to_add)
+ end)
+end
+
+local function get_ref_link_labels()
+ local labels = {}
+ local seen = {} -- To avoid duplicates
+ local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
+
+ for _, line in ipairs(lines) do
+ -- Pattern explanation:
+ -- %[.-%] matches [link text] (non-greedy)
+ -- %[(.-)%] matches [label] and captures the label content
+ local start_pos = 1
+ while start_pos <= #line do
+ local match_start, match_end, label = string.find(line, "%[.-%]%[(.-)%]", start_pos)
+ if not match_start then
+ break
+ end
+
+ -- Only add unique labels
+ if label and label ~= "" and not seen[label] then
+ table.insert(labels, label)
+ seen[label] = true
+ end
+
+ start_pos = match_end + 1
+ end
+ end
+
+ return labels
+end
+
+local function count_consecutive_spaces(str)
+ -- Remove leading spaces first
+ local trimmed = str:match("^%s*(.*)")
+ local count = 0
+
+ -- Count each sequence of one or more consecutive spaces
+ for spaces in trimmed:gmatch("%s+") do
+ count = count + 1
+ end
+ return count
+end
+
+vim.api.nvim_buf_create_user_command(0, "AddRef", function(opts)
+ local args = vim.split(opts.args, " ", { trimempty = true })
+
+ if #args < 2 then
+ vim.print("Usage: :AddRef ")
+ return
+ end
+
+ local label = args[1]
+ local url = args[2]
+
+ add_reference_at_end(label, url, "")
+end, {
+ desc = "Add reference link at buffer end",
+ nargs = "+",
+ complete = function(arg_lead, cmdline, curpos)
+ -- vim.print(string.format("arg_lead: '%s', cmdline: '%s', curpos: %d", arg_lead, cmdline, curpos))
+
+ -- only complete the first argument
+ if count_consecutive_spaces(cmdline) > 1 then
+ -- we are now starting the second argument, so no completion anymore
+ return {}
+ end
+
+ local ref_link_labels = get_ref_link_labels()
+ return ref_link_labels
+ end,
+})
diff --git a/nvim/after/ftplugin/markdown.vim b/nvim/after/ftplugin/markdown.vim
new file mode 100644
index 0000000..9e4f292
--- /dev/null
+++ b/nvim/after/ftplugin/markdown.vim
@@ -0,0 +1,69 @@
+set concealcursor=c
+set synmaxcol=3000 " For long Chinese paragraphs
+
+set wrap
+
+" Fix minor issue with footnote, see https://github.com/vim-pandoc/vim-markdownfootnotes/issues/22
+if exists(':FootnoteNumber')
+ nnoremap ^^ :call markdownfootnotes#VimFootnotes('i')
+ inoremap ^^ :call markdownfootnotes#VimFootnotes('i')
+ imap @@ ReturnFromFootnote
+ nmap @@ ReturnFromFootnote
+endif
+
+" Text objects for Markdown code blocks.
+xnoremap ic :call text_obj#MdCodeBlock('i')
+xnoremap ac :call text_obj#MdCodeBlock('a')
+onoremap ic :call text_obj#MdCodeBlock('i')
+onoremap ac :call text_obj#MdCodeBlock('a')
+
+" Use + to turn several lines to an unordered list.
+" Ref: https://vi.stackexchange.com/q/5495/15292 and https://stackoverflow.com/q/42438795/6064933.
+nnoremap + :set operatorfunc=AddListSymbolg@
+xnoremap + : call AddListSymbol(visualmode(), 1)
+
+function! AddListSymbol(type, ...) abort
+ if a:0
+ let line_start = line("'<")
+ let line_end = line("'>")
+ else
+ let line_start = line("'[")
+ let line_end = line("']")
+ endif
+
+ " add list symbol to each line
+ for line in range(line_start, line_end)
+ let text = getline(line)
+
+ let l:end = matchend(text, '^\s*')
+ if l:end == 0
+ let new_text = '+ ' . text
+ else
+ let new_text = text[0 : l:end-1] . ' + ' . text[l:end :]
+ endif
+
+ call setline(line, new_text)
+ endfor
+endfunction
+
+" Add hard line breaks for Markdown
+" nnoremap \ :set operatorfunc=AddLineBreakg@
+xnoremap \ : call AddLineBreak(visualmode(), 1)
+
+function! AddLineBreak(type, ...) abort
+ if a:0
+ let line_start = line("'<")
+ let line_end = line("'>")
+ else
+ let line_start = line("'[")
+ let line_end = line("']")
+ endif
+
+ for line in range(line_start, line_end)
+ let text = getline(line)
+ " add backslash to each line
+ let new_text = text . "\\"
+
+ call setline(line, new_text)
+ endfor
+endfunction
diff --git a/nvim/after/ftplugin/python.lua b/nvim/after/ftplugin/python.lua
new file mode 100644
index 0000000..9314539
--- /dev/null
+++ b/nvim/after/ftplugin/python.lua
@@ -0,0 +1,75 @@
+local utils = require("utils")
+local opt = vim.opt
+
+opt.wrap = false
+opt.sidescroll = 5
+opt.sidescrolloff = 2
+opt.colorcolumn = "100"
+
+opt.tabstop = 4 -- Number of visual spaces per TAB
+opt.softtabstop = 4 -- Number of spaces in tab when editing
+opt.shiftwidth = 4 -- Number of spaces to use for autoindent
+opt.expandtab = true -- Expand tab to spaces so that tabs are spaces
+
+-- when we run `:compiler ruff`, then followed by `:make`,
+-- Nvim will run ruff in the current directory. By default, `--preview` option is used.
+-- The following option is used to customize the option passed to ruff.
+vim.g.ruff_makeprg_params = ""
+
+local get_proj_root = function()
+ local project_marker = { ".git", "pyproject.toml" }
+ local project_root = vim.fs.root(0, project_marker)
+
+ return project_root
+end
+
+local get_py_env = function()
+ local project_root = get_proj_root()
+ if project_root == nil then
+ return
+ end
+
+ local venv_name = utils.get_virtual_env()
+
+ if venv_name ~= "" then
+ return "plain_venv"
+ end
+
+ -- check if this is uv-managed project
+ local uv_lock_path = vim.fs.joinpath(project_root, "uv.lock")
+ if vim.fn.filereadable(uv_lock_path) == 1 then
+ return "uv"
+ end
+
+ return ""
+end
+
+local py_env = get_py_env()
+
+if vim.fn.exists(":AsyncRun") == 2 then
+ local py_cmd = "python"
+
+ if py_env == "uv" then
+ py_cmd = "uv run python"
+ end
+
+ local rhs = string.format(":AsyncRun %s -u %%", py_cmd)
+
+ vim.keymap.set("n", "", rhs, {
+ buffer = true,
+ silent = true,
+ })
+end
+
+-- format current file
+
+local py_fmt_cmd = "!black"
+if py_env == "uv" then
+ py_fmt_cmd = "!uv run black"
+end
+
+local rhs = string.format("silent %s %%", py_fmt_cmd)
+vim.keymap.set("n", "f", rhs, {
+ buffer = true,
+ silent = true,
+})
diff --git a/nvim/after/ftplugin/qf.vim b/nvim/after/ftplugin/qf.vim
new file mode 100644
index 0000000..89b4c1f
--- /dev/null
+++ b/nvim/after/ftplugin/qf.vim
@@ -0,0 +1,6 @@
+" Set quickfix window height, see also https://github.com/lervag/vimtex/issues/1127
+function! AdjustWindowHeight(minheight, maxheight)
+ execute max([a:minheight, min([line('$'), a:maxheight])]) . 'wincmd _'
+endfunction
+
+call AdjustWindowHeight(5, 15)
diff --git a/nvim/after/ftplugin/sql.vim b/nvim/after/ftplugin/sql.vim
new file mode 100644
index 0000000..8c0d563
--- /dev/null
+++ b/nvim/after/ftplugin/sql.vim
@@ -0,0 +1 @@
+set commentstring=--\ %s
diff --git a/nvim/after/ftplugin/tex.lua b/nvim/after/ftplugin/tex.lua
new file mode 100644
index 0000000..a11226a
--- /dev/null
+++ b/nvim/after/ftplugin/tex.lua
@@ -0,0 +1,2 @@
+vim.o.textwidth = 120
+vim.o.wrap = true
diff --git a/nvim/after/ftplugin/text.vim b/nvim/after/ftplugin/text.vim
new file mode 100644
index 0000000..0dfe4de
--- /dev/null
+++ b/nvim/after/ftplugin/text.vim
@@ -0,0 +1 @@
+set colorcolumn=
diff --git a/nvim/after/ftplugin/toml.vim b/nvim/after/ftplugin/toml.vim
new file mode 100644
index 0000000..e69de29
diff --git a/nvim/after/ftplugin/vim.vim b/nvim/after/ftplugin/vim.vim
new file mode 100644
index 0000000..17dcedb
--- /dev/null
+++ b/nvim/after/ftplugin/vim.vim
@@ -0,0 +1,15 @@
+" Disable inserting comment leader after hitting o or O or
+set formatoptions-=o
+set formatoptions-=r
+
+" Set the folding related options for vim script. Setting folding option in
+" modeline is annoying in that the modeline get executed each time the window
+" focus is lost (see
+" https://github.com/tmux-plugins/vim-tmux-focus-events/issues/14)
+set foldmethod=expr foldexpr=utils#VimFolds(v:lnum) foldtext=utils#MyFoldText()
+
+" Use :help command for keyword when pressing `K` in vim file,
+" see `:h K` and https://stackoverflow.com/q/15867323/6064933
+set keywordprg=:help
+
+nnoremap :source %
diff --git a/nvim/after/ftplugin/yaml.vim b/nvim/after/ftplugin/yaml.vim
new file mode 100644
index 0000000..ad695d7
--- /dev/null
+++ b/nvim/after/ftplugin/yaml.vim
@@ -0,0 +1,4 @@
+" Turn off syntax highlighting for large YAML files.
+if line('$') > 500
+ setlocal syntax=OFF
+endif
diff --git a/nvim/after/lsp/clangd.lua b/nvim/after/lsp/clangd.lua
new file mode 100644
index 0000000..3edff2b
--- /dev/null
+++ b/nvim/after/lsp/clangd.lua
@@ -0,0 +1,4 @@
+---@type vim.lsp.Config
+return {
+ filetypes = { "c", "cpp", "cc" },
+}
diff --git a/nvim/after/lsp/gopls.lua b/nvim/after/lsp/gopls.lua
new file mode 100644
index 0000000..3a2cb7d
--- /dev/null
+++ b/nvim/after/lsp/gopls.lua
@@ -0,0 +1,19 @@
+-- settings for gopls can be found in https://go.dev/gopls/settings
+---@type vim.lsp.Config
+return {
+ settings = {
+ gopls = {
+ usePlaceholders = true,
+ analyses = {
+ unusedparams = true,
+ },
+ staticcheck = true,
+ gofumpt = true,
+ -- inlayHints settings, see https://go.dev/gopls/inlayHints
+ hints = {
+ compositeLiteralFields = true,
+ parameterNames = true,
+ },
+ },
+ },
+}
diff --git a/nvim/after/lsp/lua_ls.lua b/nvim/after/lsp/lua_ls.lua
new file mode 100644
index 0000000..ad7512a
--- /dev/null
+++ b/nvim/after/lsp/lua_ls.lua
@@ -0,0 +1,21 @@
+-- settings for lua-language-server can be found on https://luals.github.io/wiki/settings/
+---@type vim.lsp.Config
+return {
+ ---@type lspconfig.settings.lua_ls
+ settings = {
+ Lua = {
+ runtime = {
+ -- Tell the language server which version of Lua you're using (most likely LuaJIT in the case of Neovim)
+ version = "LuaJIT",
+ },
+ hint = {
+ enable = true,
+ },
+ workspace = {
+ library = {
+ vim.env.VIMRUNTIME .. "/lua",
+ },
+ },
+ },
+ },
+}
diff --git a/nvim/after/lsp/pyright.lua b/nvim/after/lsp/pyright.lua
new file mode 100644
index 0000000..c6ba1d3
--- /dev/null
+++ b/nvim/after/lsp/pyright.lua
@@ -0,0 +1,53 @@
+-- For what diagnostic is enabled in which type checking mode, check doc:
+-- https://github.com/microsoft/pyright/blob/main/docs/configuration.md#diagnostic-settings-defaults
+-- Currently, the pyright also has some issues displaying hover documentation:
+-- https://www.reddit.com/r/neovim/comments/1gdv1rc/what_is_causeing_the_lsp_hover_docs_to_looks_like/
+
+local new_capability = {
+ -- this will remove some of the diagnostics that duplicates those from ruff, idea taken and adapted from
+ -- here: https://github.com/astral-sh/ruff-lsp/issues/384#issuecomment-1989619482
+ textDocument = {
+ publishDiagnostics = {
+ tagSupport = {
+ valueSet = { 2 },
+ },
+ },
+ hover = {
+ contentFormat = { "plaintext" },
+ dynamicRegistration = true,
+ },
+ },
+}
+
+---@type vim.lsp.Config
+return {
+ -- cmd = { "delance-langserver", "--stdio" },
+ ---@type lspconfig.settings.pyright
+ settings = {
+ pyright = {
+ -- disable import sorting and use Ruff for this
+ disableOrganizeImports = true,
+ disableTaggedHints = false,
+ },
+ python = {
+ analysis = {
+ autoSearchPaths = true,
+ diagnosticMode = "workspace",
+ typeCheckingMode = "standard",
+ useLibraryCodeForTypes = true,
+ -- we can this setting below to redefine some diagnostics
+ diagnosticSeverityOverrides = {
+ deprecateTypingAliases = false,
+ },
+ -- inlay hint settings are provided by pylance?
+ inlayHints = {
+ callArgumentNames = "partial",
+ functionReturnTypes = true,
+ pytestParameters = true,
+ variableTypes = true,
+ },
+ },
+ },
+ },
+ capabilities = new_capability,
+}
diff --git a/nvim/after/lsp/ruff.lua b/nvim/after/lsp/ruff.lua
new file mode 100644
index 0000000..523c736
--- /dev/null
+++ b/nvim/after/lsp/ruff.lua
@@ -0,0 +1,9 @@
+---@type vim.lsp.Config
+return {
+ init_options = {
+ -- the settings can be found here: https://docs.astral.sh/ruff/editors/settings/
+ settings = {
+ organizeImports = true,
+ },
+ },
+}
diff --git a/nvim/autoload/buf_utils.vim b/nvim/autoload/buf_utils.vim
new file mode 100644
index 0000000..4af0ba6
--- /dev/null
+++ b/nvim/autoload/buf_utils.vim
@@ -0,0 +1,30 @@
+function! buf_utils#GoToBuffer(count, direction) abort
+ if a:count == 0
+ if a:direction ==# 'forward'
+ bnext
+ elseif a:direction ==# 'backward'
+ bprevious
+ else
+ echoerr 'Bad argument ' a:direction
+ endif
+ return
+ endif
+ " Check the validity of buffer number.
+ if index(s:GetBufNums(), a:count) == -1
+ " Using `lua vim.notify('invalid bufnr: ' .. a:count)` won't work, because
+ " we are essentially mixing Lua and vim script. We need to make sure that
+ " args inside vim.notify() are valid vim values. The conversion from vim
+ " value to lua value will be done by Nvim. See also https://github.com/neovim/neovim/pull/11338.
+ call v:lua.vim.notify('Invalid bufnr: ' . a:count, 4, {'title': 'nvim-config'})
+ return
+ endif
+
+ " Do not use {count} for gB (it is less useful)
+ if a:direction ==# 'forward'
+ silent execute('buffer' . a:count)
+ endif
+endfunction
+
+function! s:GetBufNums() abort
+ return map(copy(getbufinfo({'buflisted':1})), 'v:val.bufnr')
+endfunction
diff --git a/nvim/autoload/text_obj.vim b/nvim/autoload/text_obj.vim
new file mode 100644
index 0000000..caba645
--- /dev/null
+++ b/nvim/autoload/text_obj.vim
@@ -0,0 +1,86 @@
+function! text_obj#URL() abort
+ if match(&runtimepath, 'vim-highlighturl') != -1
+ " Note that we use https://github.com/itchyny/vim-highlighturl to get the URL pattern.
+ let url_pattern = highlighturl#default_pattern()
+ else
+ let url_pattern = expand('')
+ " Since expand('') also works for normal words, we need to check if
+ " this is really URL using heuristics, e.g., URL length.
+ if len(url_pattern) <= 10
+ return
+ endif
+ endif
+
+ " We need to find all possible URL on this line and their start, end index.
+ " Then find where current cursor is, and decide if cursor is on one of the
+ " URLs.
+ let line_text = getline('.')
+ let url_infos = []
+
+ let [_url, _idx_start, _idx_end] = matchstrpos(line_text, url_pattern)
+ while _url !=# ''
+ let url_infos += [[_url, _idx_start+1, _idx_end]]
+ let [_url, _idx_start, _idx_end] = matchstrpos(line_text, url_pattern, _idx_end)
+ endwhile
+
+ " echo url_infos
+ " If no URL is found, do nothing.
+ if len(url_infos) == 0
+ return
+ endif
+
+ let [start_col, end_col] = [-1, -1]
+ " If URL is found, find if cursor is on it.
+ let [buf_num, cur_row, cur_col] = getcurpos()[0:2]
+ for url_info in url_infos
+ " echo url_info
+ let [_url, _idx_start, _idx_end] = url_info
+ if cur_col >= _idx_start && cur_col <= _idx_end
+ let start_col = _idx_start
+ let end_col = _idx_end
+ break
+ endif
+ endfor
+
+ " Cursor is not on a URL, do nothing.
+ if start_col == -1
+ return
+ endif
+
+ " Now set the '< and '> mark
+ call setpos("'<", [buf_num, cur_row, start_col, 0])
+ call setpos("'>", [buf_num, cur_row, end_col, 0])
+ normal! gv
+endfunction
+
+function! text_obj#MdCodeBlock(type) abort
+ " the parameter type specify whether it is inner text objects or around
+ " text objects.
+
+ " Move the cursor to the end of line in case that cursor is on the opening
+ " of a code block. Actually, there are still issues if the cursor is on the
+ " closing of a code block. In this case, the start row of code blocks would
+ " be wrong. Unless we can match code blocks, it not easy to fix this.
+ normal! $
+ let start_row = searchpos('\s*```', 'bnW')[0]
+ let end_row = searchpos('\s*```', 'nW')[0]
+
+ let buf_num = bufnr()
+ if a:type ==# 'i'
+ let start_row += 1
+ let end_row -= 1
+ endif
+ " echo a:type start_row end_row
+
+ call setpos("'<", [buf_num, start_row, 1, 0])
+ call setpos("'>", [buf_num, end_row, 1, 0])
+ execute 'normal! `'
+endfunction
+
+function! text_obj#Buffer() abort
+ let buf_num = bufnr()
+
+ call setpos("'<", [buf_num, 1, 1, 0])
+ call setpos("'>", [buf_num, line('$'), 1, 0])
+ execute 'normal! `'
+endfunction
diff --git a/nvim/autoload/utils.vim b/nvim/autoload/utils.vim
new file mode 100644
index 0000000..a157d25
--- /dev/null
+++ b/nvim/autoload/utils.vim
@@ -0,0 +1,141 @@
+" Create command alias safely, see https://stackoverflow.com/q/3878692/6064933
+" The following two functions are taken from answer below on SO:
+" https://stackoverflow.com/a/10708687/6064933
+function! utils#Cabbrev(key, value) abort
+ execute printf('cabbrev %s (getcmdtype() == ":" && getcmdpos() <= %d) ? %s : %s',
+ \ a:key, 1+len(a:key), Single_quote(a:value), Single_quote(a:key))
+endfunction
+
+function! s:Single_quote(str) abort
+ return "'" . substitute(copy(a:str), "'", "''", 'g') . "'"
+endfunction
+
+" Custom fold expr, adapted from https://vi.stackexchange.com/a/9094/15292
+function! utils#VimFolds(lnum) abort
+ " get content of current line and the line below
+ let l:cur_line = getline(a:lnum)
+ let l:next_line = getline(a:lnum+1)
+
+ if l:cur_line =~# '^"{'
+ return '>' . (matchend(l:cur_line, '"{*') - 1)
+ endif
+
+ if l:cur_line ==# '' && (matchend(l:next_line, '"{*') - 1) == 1
+ return 0
+ endif
+
+ return '='
+endfunction
+
+" Custom fold text, adapted from https://vi.stackexchange.com/a/3818/15292
+" and https://vi.stackexchange.com/a/6608/15292
+function! utils#MyFoldText() abort
+ let l:line = getline(v:foldstart)
+ let l:fold_line_num = v:foldend - v:foldstart
+ let l:fold_text = substitute(l:line, '^"{\+', '', 'g')
+ let l:fill_char_num = &textwidth - len(l:fold_text) - len(l:fold_line_num) - 10
+ return printf('+%s%s %s (%s L)', repeat('-', 4), l:fold_text, repeat('-', l:fill_char_num), l:fold_line_num)
+endfunction
+
+" Toggle cursor column
+function! utils#ToggleCursorCol() abort
+ if &cursorcolumn
+ set nocursorcolumn
+ echo 'cursorcolumn: OFF'
+ else
+ set cursorcolumn
+ echo 'cursorcolumn: ON'
+ endif
+endfunction
+
+function! utils#SwitchLine(src_line_idx, direction) abort
+ if a:direction ==# 'up'
+ if a:src_line_idx == 1
+ return
+ endif
+ move-2
+ elseif a:direction ==# 'down'
+ if a:src_line_idx == line('$')
+ return
+ endif
+ move+1
+ endif
+endfunction
+
+function! utils#MoveSelection(direction) abort
+ " only do this if previous mode is visual line mode. Once we press some keys in
+ " visual line mode, we will leave this mode. So the output of `mode()` will be
+ " `n` instead of `V`. We can use `visualmode()` instead to check the previous
+ " mode, see also https://stackoverflow.com/a/61486601/6064933
+ if visualmode() !=# 'V'
+ return
+ endif
+
+ let l:start_line = line("'<")
+ let l:end_line = line("'>")
+ let l:num_line = l:end_line - l:start_line + 1
+
+ if a:direction ==# 'up'
+ if l:start_line == 1
+ " we can also directly use `normal gv`, see https://stackoverflow.com/q/9724123/6064933
+ normal! gv
+ return
+ endif
+ silent execute printf('%s,%smove-2', l:start_line, l:end_line)
+ normal! gv
+ elseif a:direction ==# 'down'
+ if l:end_line == line('$')
+ normal! gv
+ return
+ endif
+ silent execute printf('%s,%smove+%s', l:start_line, l:end_line, l:num_line)
+ normal! gv
+ endif
+endfunction
+
+
+" Output current time or unix timestamp in human-readable format.
+function! utils#iso_time(timestamp) abort
+ " Get current datetime
+ if !a:timestamp
+ return strftime('%Y-%m-%d %H:%M:%S%z')
+ endif
+
+ " this timestamp in expressed in milliseconds
+ if len(a:timestamp) == 13
+ let l:timestamp = a:timestamp[:-4]
+ " this timestamp in expressed in microseconds
+ elseif len(a:timestamp) == 16
+ let l:timestamp = a:timestamp[:-7]
+ else
+ let l:timestamp = a:timestamp
+ endif
+ return strftime('%Y-%m-%d %H:%M:%S%z', l:timestamp)
+
+endfunction
+
+" Redirect command output to a register for later processing.
+" Ref: https://stackoverflow.com/q/2573021/6064933 and https://unix.stackexchange.com/q/8101/221410 .
+function! utils#CaptureCommandOutput(command) abort
+ let l:tmp = @m
+ redir @m
+ silent! execute a:command
+ redir END
+
+ "create a scratch buffer for dumping the text, ref: https://vi.stackexchange.com/a/11311/15292.
+ tabnew | setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
+
+ let l:lines = split(@m, '\n')
+ call nvim_buf_set_lines(0, 0, 0, 0, l:lines)
+
+ let @m = l:tmp
+endfunction
+
+" Edit all files matching the given patterns.
+function! utils#MultiEdit(patterns) abort
+ for p in a:patterns
+ for f in glob(p, 0, 1)
+ execute 'edit ' . f
+ endfor
+ endfor
+endfunction
diff --git a/nvim/docs/README.md b/nvim/docs/README.md
new file mode 100644
index 0000000..d041655
--- /dev/null
+++ b/nvim/docs/README.md
@@ -0,0 +1,192 @@
+This doc summarizes how to install and use this configuration in detail.
+
+# Pre-requisite
+
+## Terminal emulators
+
+Which [terminal emulator](https://en.wikipedia.org/wiki/Terminal_emulator) we choose to use greatly affects the appearance and features of Nvim.
+Since Nvim supports true colors, terminals that support true colors are preferred.
+For a list of terminals that support true colors, see [here](https://github.com/termstandard/colors).
+
+For macOS, we can use [kitty](https://sw.kovidgoyal.net/kitty/), [iterm2](https://www.iterm2.com/), [wezterm](https://wezfurlong.org/wezterm/) or [Alacritty](https://github.com/jwilm/alacritty).
+
+If you ssh to Linux server on Windows, I recommend [wsltty](https://github.com/mintty/wsltty) and [Cygwin](https://www.cygwin.com/),
+both of them use [mintty](https://github.com/mintty/mintty) as the terminal emulator.
+For the latest version of Windows 10, you can also try [Windows Terminal](https://github.com/microsoft/terminal).
+
+## Patched Fonts
+
+Since statusline or file explorer plugins often use Unicode symbols not available in normal font,
+we need to install a patched font from the [nerd-fonts](https://github.com/ryanoasis/nerd-fonts) project.
+
+# Automatic installation
+
+## Automatic Installation for Linux
+
+To set up a workable Nvim environment on Linux,
+I use [a bash script](nvim_setup_linux.sh) to automatically install necessary dependencies, Nvim itself and configs.
+
+Note that the variable `PYTHON_INSTALLED`, `SYSTEM_PYTHON` and `ADD_TO_SYSTEM_PATH` in the script
+should be set properly based on your environment.
+
+## Automatic installation for Windows
+
+Run the script [nvim_setup_windows.ps1](nvim_setup_windows.ps1) in PowerShell **with Administrator rights**:
+
+```
+.\docs\nvim_setup_windows.ps1
+```
+
+# Manual install
+
+There are a few dependencies if we want to use Nvim for efficient editing and development work.
+
+## Dependencies
+
+### Python
+
+A lot of Nvim plugins are mainly written in Python, so we must install Python 3.
+The easiest way to install is via [Anaconda](https://docs.anaconda.com/anaconda/install/index.html) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html).
+
+After installation, make sure that you can run `python --version`,
+and that the output should be Python 3.x.
+
+### Pynvim
+
+Nvim relies on [pynvim](https://github.com/neovim/pynvim) to communicate with plugins that utilize its Python binding.
+Pynvim is required by plugins such as [wilder.nvim](https://github.com/gelguy/wilder.nvim).
+
+```
+pip install -U pynvim
+```
+
+### python-lsp-server
+
+[python-lsp-server (pylsp)](https://github.com/python-lsp/python-lsp-server) is a Python [Language Server](https://microsoft.github.io/language-server-protocol/) for completion, linting, go to definition, etc.
+
+```
+pip install 'python-lsp-server[all]' pylsp-mypy python-lsp-isort python-lsp-black
+```
+
+Note the executable for pylsp is also named `pylsp`. You need to set its PATH correctly.
+If you use pip from Anaconda, the executable path may be something like `$CONDA_ROOT/bin/pylsp`.
+For native python, the path for pylsp may be like `$HOME/.local/bin/pylsp`
+
+### Node
+
+We need to install node.js from [here](https://nodejs.org/en/download/).
+For Linux, you can use the following script:
+
+```bash
+# Ref: https://johnpapa.net/node-and-npm-without-sudo/
+wget https://nodejs.org/dist/v14.15.4/node-v14.15.4-linux-x64.tar.xz
+
+mkdir -p $HOME/tools
+# extract node to a custom directory, the directory should exist.
+tar xvf node-v14.15.4-linux-x64.tar.xz --directory=$HOME/tools
+```
+
+Then add the following config to `.bash_profile` or `.zshrc`
+
+```bash
+export PATH="$HOME/tools/node-v14.15.4-linux-x64/bin:$PATH"
+```
+
+Source the file:
+
+```bash
+source ~/.bash_profile
+# source ~/.zshrc
+```
+
+### vim-language-server
+
+[vim-language-server](https://github.com/iamcco/vim-language-server) provides completion for vim script. We can install vim-language-server globally:
+
+```bash
+npm install -g vim-language-server
+```
+
+vim-language-server is installed in the same directory as the node executable.
+
+### Git
+
+Git is required by plugin manager [lazy.nvim](https://github.com/folke/lazy.nvim) and other git-related plugins.
+
+For Linux and macOS, Git is usually pre-installed.
+The version of Git on the Linux system may be too old so that plugins may break.
+Check [here](https://jdhao.github.io/2021/03/27/upgrade_git_on_linux/) on how to install and set up the latest version of Git.
+For Windows, install [Git for Windows](https://git-scm.com/download/win) and make sure you can run `git` from command line.
+
+### universal-ctags
+
+In order to use tags related plugins such as [vista.vim](https://github.com/liuchengxu/vista.vim), we need to install a ctags distribution.
+Universal-ctags is preferred.
+
+To install it on Linux, we need to build it from source. See [here](https://askubuntu.com/a/836521/768311) for the details.
+To install ctags on macOS, use [Homebrew](https://formulae.brew.sh/formula/universal-ctags):
+
+```bash
+brew install universal-ctags
+```
+
+To install it Windows, use [chocolatey](https://chocolatey.org/) or [scoop](https://scoop.sh/)
+
+```
+choco install universal-ctags
+
+# scoop bucket add extras
+# scoop install universal-ctags
+```
+
+Set its PATH properly and make sure you can run `ctags` from command line.
+
+### Ripgrep
+
+[Ripgrep](https://github.com/BurntSushi/ripgrep), aka, `rg`, is a fast grepping tool available for both Linux, Windows and macOS.
+It is used by several searching plugins.
+
+For Windows and macOS, we can install it via chocolatey and homebrew respectively.
+For Linux, we can download the [binary release](https://github.com/BurntSushi/ripgrep/releases) and install it.
+
+Set its PATH properly and make sure you can run `rg` from command line.
+
+### Linters
+
+A linter is a tool to check the source code for possible style and syntax issues.
+Based on the programming languages we use, we may need to install various linters.
+
++ Python: [pylint](https://github.com/PyCQA/pylint) and [flake8](https://github.com/PyCQA/flake8).
++ Vim script: [vint](https://github.com/Kuniwak/vint).
+
+Set their PATH properly and make sure you can run `pylint`, `flake8` and `vint` from command line.
+
+## Install Nvim
+
+There are various ways to install Nvim depending on your system.
+This config is only maintained for [the latest nvim stable release](https://github.com/neovim/neovim/releases/tag/stable).
+
+Please check the official doc on how to install Neovim on different systems: https://github.com/neovim/neovim/blob/master/INSTALL.md
+
+After installing Nvim, we need to set the PATH to nvim correctly.
+**Make sure that you can run `nvim` from the command line after all these setups**.
+
+## Setting up Nvim
+
+After installing nvim and all the dependencies, we will install plugin managers and set up this config.
+
+### How to install this configuration
+
+On Linux and macOS, the directory is `~/.config/nvim`.
+On Windows, the config directory is `$HOME/AppData/Local/nvim`[^1].
+First, we need to remove all the files under the config directory (including dot files),
+then go to this directory, and run the following command:
+
+```
+git clone --depth=1 https://github.com/jdhao/nvim-config.git .
+```
+
+After that, when we first open nvim, all the plugins will be installed automatically.
+Since I use quite a lot of plugins (more than 60), it may take some time to install all of them.
+
+[^1]: Use `echo %userprofile%` to see where your `$HOME` is.
diff --git a/nvim/docs/nvim_install_mac.sh b/nvim/docs/nvim_install_mac.sh
new file mode 100644
index 0000000..ef698c8
--- /dev/null
+++ b/nvim/docs/nvim_install_mac.sh
@@ -0,0 +1,20 @@
+# This script is used to update Nvim on macOS
+#!/bin/bash
+set -eux
+
+wget https://github.com/neovim/neovim/releases/download/stable/nvim-macos.tar.gz
+
+if [[ ! -d "$HOME/tools/" ]]; then
+ mkdir -p "$HOME/tools"
+fi
+
+# Delete existing nvim installation.
+# For newer release, the directory name is nvim-macos
+if [[ -d "$HOME/tools/nvim-macos" ]]; then
+ rm -rf "$HOME/tools/nvim-macos"
+fi
+
+# Extract the tar ball
+tar zxvf nvim-macos.tar.gz -C "$HOME/tools"
+
+rm nvim-macos.tar.gz
diff --git a/nvim/docs/nvim_resources.md b/nvim/docs/nvim_resources.md
new file mode 100644
index 0000000..47c9156
--- /dev/null
+++ b/nvim/docs/nvim_resources.md
@@ -0,0 +1,18 @@
+Below are a list of resources that inspire me. This list is non-exhaustive as I
+can not remember the source of many settings.
+
+- [From .vimrc to .vim](https://vimways.org/2018/from-vimrc-to-vim/)
+- http://stevelosh.com/blog/2010/09/coming-home-to-vim/
+- https://github.com/tamlok/tvim/blob/master/.vimrc
+- https://nvie.com/posts/how-i-boosted-my-vim/
+- https://blog.carbonfive.com/2011/10/17/vim-text-objects-the-definitive-guide/
+- https://sanctum.geek.nz/arabesque/vim-anti-patterns/
+- https://github.com/gkapfham/dotfiles/blob/master/.vimrc
+- https://google.github.io/styleguide/vimscriptguide.xml
+- https://github.com/romainl/idiomatic-vimrc
+- https://github.com/mhinz/vim-galore
+- https://learnvimscriptthehardway.stevelosh.com/
+- https://vimways.org/2018/from-vimrc-to-vim/
+- [vim script for Python developers](https://github.com/yegappan/VimScriptForPythonDevelopers)
+- [Awesome neovim](https://github.com/rockerBOO/awesome-neovim): extensive list of neovim resources (plugins, mostly).
+- [What is your favorite plugins for Nvim 0.5](https://www.reddit.com/r/neovim/comments/o20w3z/what_are_your_favorite_neovim_plugins_exclusive/)
diff --git a/nvim/docs/nvim_setup_linux.sh b/nvim/docs/nvim_setup_linux.sh
new file mode 100755
index 0000000..9d0ec3b
--- /dev/null
+++ b/nvim/docs/nvim_setup_linux.sh
@@ -0,0 +1,258 @@
+#!/bin/bash
+set -exu
+set -o pipefail
+
+# Whether python3 has been installed on the system
+PYTHON_INSTALLED=true
+
+# If Python has been installed, then we need to know whether Python is provided
+# by the system, or you have already installed Python under your HOME.
+SYSTEM_PYTHON=false
+
+# If SYSTEM_PYTHON is false, we need to decide whether to install
+# Anaconda (INSTALL_ANACONDA=true) or Miniconda (INSTALL_ANACONDA=false)
+INSTALL_ANACONDA=false
+
+# Whether to add the path of the installed executables to system PATH
+ADD_TO_SYSTEM_PATH=true
+
+# select which shell we are using
+USE_ZSH_SHELL=true
+USE_BASH_SHELL=false
+
+if [[ ! -d "$HOME/packages/" ]]; then
+ mkdir -p "$HOME/packages/"
+fi
+
+if [[ ! -d "$HOME/tools/" ]]; then
+ mkdir -p "$HOME/tools/"
+fi
+
+#######################################################################
+# Anaconda or miniconda install #
+#######################################################################
+if [[ "$INSTALL_ANACONDA" = true ]]; then
+ CONDA_DIR=$HOME/tools/anaconda
+ CONDA_NAME=Anaconda.sh
+ CONDA_LINK="https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/Anaconda3-2021.11-Linux-x86_64.sh"
+else
+ CONDA_DIR=$HOME/tools/miniconda
+ CONDA_NAME=Miniconda.sh
+ CONDA_LINK="https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py39_4.10.3-Linux-x86_64.sh"
+fi
+
+if [[ ! "$PYTHON_INSTALLED" = true ]]; then
+ echo "Installing Python in user HOME"
+
+ SYSTEM_PYTHON=false
+
+ echo "Downloading and installing conda"
+
+ if [[ ! -f "$HOME/packages/$CONDA_NAME" ]]; then
+ curl -Lo "$HOME/packages/$CONDA_NAME" $CONDA_LINK
+ fi
+
+ # Install conda silently
+ if [[ -d $CONDA_DIR ]]; then
+ rm -rf "$CONDA_DIR"
+ fi
+ bash "$HOME/packages/$CONDA_NAME" -b -p "$CONDA_DIR"
+
+ # Setting up environment variables
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$CONDA_DIR/bin:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+else
+ echo "Python is already installed. Skip installing it."
+fi
+
+# Install some Python packages used by Nvim plugins.
+echo "Installing Python packages"
+declare -a PY_PACKAGES=("pynvim" 'python-lsp-server[all]' "vim-vint" "python-lsp-isort" "pylsp-mypy" "python-lsp-black")
+
+if [[ "$SYSTEM_PYTHON" = true ]]; then
+ echo "Using system Python to install $(PY_PACKAGES)"
+
+ # If we use system Python, we need to install these Python packages under
+ # user HOME, since we do not have permissions to install them under system
+ # directories.
+ for p in "${PY_PACKAGES[@]}"; do
+ pip install --user "$p"
+ done
+else
+ echo "Using custom Python to install $(PY_PACKAGES)"
+ for p in "${PY_PACKAGES[@]}"; do
+ "$CONDA_DIR/bin/pip" install "$p"
+ done
+fi
+
+#######################################################################
+# Install node and js-based language server #
+#######################################################################
+NODE_DIR=$HOME/tools/nodejs
+NODE_SRC_NAME=$HOME/packages/nodejs.tar.gz
+# when download speed is slow, we can also use its mirror site: https://mirrors.ustc.edu.cn/node/v15.0.0/
+NODE_LINK="https://mirrors.ustc.edu.cn/node/v15.0.0/node-v15.0.0-linux-x64.tar.xz"
+if [[ -z "$(command -v node)" ]]; then
+ echo "Install Node.js"
+ if [[ ! -f $NODE_SRC_NAME ]]; then
+ echo "Downloading Node.js and renaming"
+ wget $NODE_LINK -O "$NODE_SRC_NAME"
+ fi
+
+ if [[ ! -d "$NODE_DIR" ]]; then
+ echo "Creating Node.js directory under tools directory"
+ mkdir -p "$NODE_DIR"
+ echo "Extracting to $HOME/tools/nodejs directory"
+ tar xvf "$NODE_SRC_NAME" -C "$NODE_DIR" --strip-components 1
+ fi
+
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$NODE_DIR/bin:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+else
+ echo "Node.js is already installed. Skip installing it."
+ NODE_DIR="$(realpath $(dirname $(which node))/..)"
+fi
+
+# Install vim-language-server
+"$NODE_DIR/bin/npm" install -g vim-language-server
+
+# Install bash-language-server
+"$NODE_DIR/bin/npm" install -g bash-language-server
+
+#######################################################################
+# lua-language-server #
+#######################################################################
+LUA_LS_DIR=$HOME/tools/lua-language-server
+LUA_LS_SRC=$HOME/packages/lua-language-server.tar.gz
+LUA_LS_LINK="https://github.com/LuaLS/lua-language-server/releases/download/3.6.11/lua-language-server-3.6.11-linux-x64.tar.gz"
+
+if [[ -z "$(command -v lua-language-server)" ]] && [[ ! -f "$LUA_LS_DIR/bin/lua-language-server" ]]; then
+ echo 'Install lua-language-server'
+ if [[ ! -f $LUA_LS_SRC ]]; then
+ echo "Downloading lua-language-server and renaming"
+ wget $LUA_LS_LINK -O "$LUA_LS_SRC"
+ fi
+
+ if [[ ! -d "$LUA_LS_DIR" ]]; then
+ echo "Creating lua-language-server directory under tools directory"
+ mkdir -p "$LUA_LS_DIR"
+ echo "Extracting to directory $LUA_LS_DIR"
+
+ tar zxvf "$LUA_LS_SRC" -C "$LUA_LS_DIR"
+ fi
+
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$LUA_LS_DIR/bin:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+else
+ echo "lua-language-server is already installed. Skip installing it."
+fi
+
+#######################################################################
+# Ripgrep part #
+#######################################################################
+RIPGREP_DIR=$HOME/tools/ripgrep
+RIPGREP_SRC_NAME=$HOME/packages/ripgrep.tar.gz
+RIPGREP_LINK="https://github.com/BurntSushi/ripgrep/releases/download/12.0.0/ripgrep-12.0.0-x86_64-unknown-linux-musl.tar.gz"
+if [[ -z "$(command -v rg)" ]] && [[ ! -f "$RIPGREP_DIR/rg" ]]; then
+ echo "Install ripgrep"
+ if [[ ! -f $RIPGREP_SRC_NAME ]]; then
+ echo "Downloading ripgrep and renaming"
+ wget $RIPGREP_LINK -O "$RIPGREP_SRC_NAME"
+ fi
+
+ if [[ ! -d "$RIPGREP_DIR" ]]; then
+ echo "Creating ripgrep directory under tools directory"
+ mkdir -p "$RIPGREP_DIR"
+ echo "Extracting to $HOME/tools/ripgrep directory"
+ tar zxvf "$RIPGREP_SRC_NAME" -C "$RIPGREP_DIR" --strip-components 1
+ fi
+
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$RIPGREP_DIR:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+
+ # set up manpath and zsh completion for ripgrep
+ mkdir -p "$HOME/tools/ripgrep/doc/man/man1"
+ mv "$HOME/tools/ripgrep/doc/rg.1" "$HOME/tools/ripgrep/doc/man/man1"
+
+ if [[ "$USE_BASH_SHELL" = true ]]; then
+ echo 'export MANPATH=$HOME/tools/ripgrep/doc/man:$MANPATH' >> "$HOME/.bash_profile"
+ else
+ echo 'export MANPATH=$HOME/tools/ripgrep/doc/man:$MANPATH' >> "$HOME/.zshrc"
+ echo 'export FPATH=$HOME/tools/ripgrep/complete:$FPATH' >> "$HOME/.zshrc"
+ fi
+else
+ echo "ripgrep is already installed. Skip installing it."
+fi
+
+#######################################################################
+# Ctags install #
+#######################################################################
+CTAGS_SRC_DIR=$HOME/packages/ctags
+CTAGS_DIR=$HOME/tools/ctags
+CTAGS_LINK="https://github.com/universal-ctags/ctags.git"
+if [[ ! -f "$CTAGS_DIR/bin/ctags" ]]; then
+ echo "Install ctags"
+
+ if [[ ! -d $CTAGS_SRC_DIR ]]; then
+ mkdir -p "$CTAGS_SRC_DIR"
+ else
+ # Prevent an incomplete download.
+ rm -rf "$CTAGS_SRC_DIR"
+ fi
+
+ git clone --depth=1 "$CTAGS_LINK" "$CTAGS_SRC_DIR" && cd "$CTAGS_SRC_DIR"
+ ./autogen.sh && ./configure --prefix="$CTAGS_DIR"
+ make -j && make install
+
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$CTAGS_DIR/bin:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+else
+ echo "ctags is already installed. Skip installing it."
+fi
+
+#######################################################################
+# Nvim install #
+#######################################################################
+NVIM_DIR=$HOME/tools/nvim
+NVIM_SRC_NAME=$HOME/packages/nvim-linux64.tar.gz
+NVIM_CONFIG_DIR=$HOME/.config/nvim
+NVIM_LINK="https://github.com/neovim/neovim/releases/download/stable/nvim-linux-x86_64.tar.gz"
+if [[ ! -f "$NVIM_DIR/bin/nvim" ]]; then
+ echo "Installing Nvim"
+ echo "Creating nvim directory under tools directory"
+
+ if [[ ! -d "$NVIM_DIR" ]]; then
+ mkdir -p "$NVIM_DIR"
+ fi
+
+ if [[ ! -f $NVIM_SRC_NAME ]]; then
+ echo "Downloading Nvim"
+ wget "$NVIM_LINK" -O "$NVIM_SRC_NAME"
+ fi
+ echo "Extracting neovim"
+ tar zxvf "$NVIM_SRC_NAME" --strip-components 1 -C "$NVIM_DIR"
+
+ if [[ "$ADD_TO_SYSTEM_PATH" = true ]] && [[ "$USE_BASH_SHELL" = true ]]; then
+ echo "export PATH=\"$NVIM_DIR/bin:\$PATH\"" >> "$HOME/.bash_profile"
+ fi
+else
+ echo "Nvim is already installed. Skip installing it."
+fi
+
+echo "Setting up config and installing plugins"
+if [[ -d "$NVIM_CONFIG_DIR" ]]; then
+ rm -rf "$NVIM_CONFIG_DIR.backup"
+ mv "$NVIM_CONFIG_DIR" "$NVIM_CONFIG_DIR.backup"
+fi
+
+git clone --depth=1 https://github.com/jdhao/nvim-config.git "$NVIM_CONFIG_DIR"
+
+echo "Installing nvim plugins, please wait"
+"$NVIM_DIR/bin/nvim" -c "autocmd User LazyInstall quitall" -c "lua require('lazy').install()"
+
+echo "Finished installing Nvim and its dependencies!"
diff --git a/nvim/docs/nvim_setup_windows.ps1 b/nvim/docs/nvim_setup_windows.ps1
new file mode 100644
index 0000000..9148a22
--- /dev/null
+++ b/nvim/docs/nvim_setup_windows.ps1
@@ -0,0 +1,55 @@
+# Set policy to avoid errors
+Set-ExecutionPolicy RemoteSigned -scope CurrentUser
+
+# Install scoop
+Invoke-WebRequest -UseBasicParsing get.scoop.sh | Invoke-Expression
+
+# Install node
+scoop install nodejs
+
+# Install ripgrep
+scoop install ripgrep
+
+# Install universal-ctags
+scoop bucket add extras
+scoop install universal-ctags
+
+# Install vim-language-server
+npm install -g vim-language-server
+
+# Install bash-language-server
+npm install -g bash-language-server
+
+# Install miniconda3 (for Python)
+scoop install miniconda3
+
+# Install pynvim
+pip install -U pynvim
+
+# Install python-language-server
+pip install 'python-lsp-server[all]' pylsp-mypy python-lsp-isort
+
+# Install visual c++ redistribution
+scoop install vcredist2022
+
+# Install 7zip
+scoop install 7zip
+
+# Install lua-language-server
+$lua_ls_link = "https://github.com/LuaLS/lua-language-server/releases/download/3.6.11/lua-language-server-3.6.11-win32-x64.zip"
+$lua_ls_install_dir = "D:\portable_tools"
+$lua_ls_src_path = "$lua_ls_install_dir\lua-language-server.zip"
+$lua_ls_dir = "$lua_ls_install_dir\lua-language-server"
+
+# Download file, ref: https://stackoverflow.com/a/51225744/6064933
+Invoke-WebRequest $lua_ls_link -OutFile "$lua_ls_src_path"
+
+# Extract the zip file using 7zip, ref: https://stackoverflow.com/a/41933215/6064933
+7z x "$lua_ls_src_path" -o"$lua_ls_dir"
+
+# Setup PATH env variable, ref: https://stackoverflow.com/q/714877/6064933
+[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$lua_ls_dir\bin", "Machine")
+
+# Install neovim nightly
+scoop bucket add versions
+scoop install neovim
diff --git a/nvim/ftdetect/pdc.vim b/nvim/ftdetect/pdc.vim
new file mode 100644
index 0000000..20f96cf
--- /dev/null
+++ b/nvim/ftdetect/pdc.vim
@@ -0,0 +1,4 @@
+augroup det_md
+ autocmd!
+ autocmd BufRead,BufNewFile *.pdc set filetype=markdown
+augroup END
diff --git a/nvim/ftdetect/snippets.vim b/nvim/ftdetect/snippets.vim
new file mode 100644
index 0000000..3b81a36
--- /dev/null
+++ b/nvim/ftdetect/snippets.vim
@@ -0,0 +1,4 @@
+augroup det_snippet
+ autocmd!
+ autocmd BufRead,BufNewFile *.snippets set filetype=snippets
+augroup END
diff --git a/nvim/ginit.vim b/nvim/ginit.vim
new file mode 100644
index 0000000..484510d
--- /dev/null
+++ b/nvim/ginit.vim
@@ -0,0 +1,74 @@
+" Fix key mapping issues for GUI
+inoremap +
+cnoremap +
+nnoremap
+
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+" config for nvim-qt "
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+" To check if neovim-qt is running, use `exists('g:GuiLoaded')`,
+" see https://github.com/equalsraf/neovim-qt/issues/219
+if exists('g:GuiLoaded')
+ " call GuiWindowMaximized(1)
+ GuiTabline 0
+ GuiPopupmenu 0
+ GuiLinespace 2
+ GuiFont! Hack\ NF:h10:l
+endif
+
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+" config for fvim "
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+if exists('g:fvim_loaded')
+ set termguicolors
+ colorscheme gruvbox8_hard
+ set guifont=Hack\ NF:h13
+ " Cursor tweaks
+ FVimCursorSmoothMove v:true
+ FVimCursorSmoothBlink v:true
+
+ " Background composition, can be 'none', 'blur' or 'acrylic'
+ FVimBackgroundComposition 'none'
+ FVimBackgroundOpacity 1.0
+ FVimBackgroundAltOpacity 1.0
+
+ " Title bar tweaks (themed with colorscheme)
+ FVimCustomTitleBar v:true
+
+ " Debug UI overlay
+ FVimDrawFPS v:false
+ " Font debugging -- draw bounds around each glyph
+ FVimFontDrawBounds v:false
+
+ " Font tweaks
+ FVimFontAntialias v:true
+ FVimFontAutohint v:true
+ FVimFontHintLevel 'full'
+ FVimFontSubpixel v:true
+ FVimFontLigature v:true
+ " can be 'default', '14.0', '-1.0' etc.
+ FVimFontLineHeight '+1'
+
+ " Try to snap the fonts to the pixels, reduces blur
+ " in some situations (e.g. 100% DPI).
+ FVimFontAutoSnap v:true
+
+ " Font weight tuning, possible values are 100..900
+ FVimFontNormalWeight 100
+ FVimFontBoldWeight 700
+
+ FVimUIPopupMenu v:false
+endif
+
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+" config for neovide "
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+if exists("g:neovide")
+ set background=dark
+ let g:neovide_transparency = 1.0
+ let g:neovide_cursor_animation_length = 0.1
+ let g:neovide_cursor_trail_size=0.3
+ let g:neovide_cursor_vfx_mode = ""
+ let g:neovide_cursor_vfx_particle_density=10.0
+ let g:neovide_cursor_vfx_opacity=150.0
+endif
diff --git a/nvim/init.lua b/nvim/init.lua
new file mode 100644
index 0000000..2a99d77
--- /dev/null
+++ b/nvim/init.lua
@@ -0,0 +1,41 @@
+-- This is my personal Nvim configuration supporting Mac, Linux and Windows, with various plugins configured.
+-- This configuration evolves as I learn more about Nvim and become more proficient in using Nvim.
+-- Since it is very long (more than 1000 lines!), you should read it carefully and take only the settings that suit you.
+-- I would not recommend cloning this repo and replace your own config. Good configurations are personal,
+-- built over time with a lot of polish.
+--
+-- Author: Jiedong Hao
+-- Email: jdhao@hotmail.com
+-- Blog: https://jdhao.github.io/
+-- GitHub: https://github.com/jdhao
+-- StackOverflow: https://stackoverflow.com/users/6064933/jdhao
+local utils = require("utils")
+
+vim.loader.enable()
+
+local expected_version = "0.12.2"
+utils.is_compatible_version(expected_version)
+
+-- some global settings
+require("globals")
+
+-- setting options in nvim
+require("options")
+
+-- various autocommands
+require("custom-autocmd")
+
+-- all the user-defined mappings
+require("mappings")
+
+-- all the plugins installed and their configurations
+require("plugin_specs")
+
+-- This is done after plugin_specs, since lsp-config is loaded in that step
+require("lsp_conf")
+
+-- diagnostic related config
+require("diagnostic-conf")
+
+-- colorscheme settings
+require("ui")
diff --git a/nvim/lua/config/blink-cmp.lua b/nvim/lua/config/blink-cmp.lua
new file mode 100644
index 0000000..ac83c63
--- /dev/null
+++ b/nvim/lua/config/blink-cmp.lua
@@ -0,0 +1,100 @@
+require("blink.cmp").setup {
+ -- 'default' (recommended) for mappings similar to built-in completions (C-y to accept)
+ -- 'super-tab' for mappings similar to vscode (tab to accept)
+ -- 'enter' for enter to accept
+ -- 'none' for no mappings
+ --
+ keymap = {
+ preset = "default",
+ [""] = { "select_next", "fallback" },
+ [""] = { "select_prev", "fallback" },
+ [""] = { "select_and_accept", "fallback" },
+ [""] = { "scroll_documentation_up", "fallback" },
+ [""] = { "scroll_documentation_down", "fallback" },
+ },
+
+ appearance = {
+ -- 'mono' (default) for 'Nerd Font Mono' or 'normal' for 'Nerd Font'
+ -- Adjusts spacing to ensure icons are aligned
+ nerd_font_variant = "mono",
+ },
+
+ -- (Default) Only show the documentation popup when manually triggered
+ completion = {
+ documentation = {
+ auto_show = true,
+ },
+ },
+
+ -- Default list of enabled providers defined so that you can extend it
+ -- elsewhere in your config, without redefining it, due to `opts_extend`
+ sources = {
+ default = { "lsp", "path", "buffer", "omni" },
+ providers = {
+ -- Use the thesaurus source
+ thesaurus = {
+ name = "blink-cmp-words",
+ module = "blink-cmp-words.thesaurus",
+ -- All available options
+ opts = {
+ -- A score offset applied to returned items.
+ -- By default the highest score is 0 (item 1 has a score of -1, item 2 of -2 etc..).
+ score_offset = 0,
+
+ -- Default pointers define the lexical relations listed under each definition,
+ -- see Pointer Symbols below.
+ -- Default is as below ("antonyms", "similar to" and "also see").
+ definition_pointers = { "!", "&", "^" },
+
+ -- The pointers that are considered similar words when using the thesaurus,
+ -- see Pointer Symbols below.
+ -- Default is as below ("similar to", "also see" }
+ similarity_pointers = { "&", "^" },
+
+ -- The depth of similar words to recurse when collecting synonyms. 1 is similar words,
+ -- 2 is similar words of similar words, etc. Increasing this may slow results.
+ similarity_depth = 2,
+ },
+ },
+
+ -- Use the dictionary source
+ dictionary = {
+ name = "blink-cmp-words",
+ module = "blink-cmp-words.dictionary",
+ -- All available options
+ opts = {
+ -- The number of characters required to trigger completion.
+ -- Set this higher if completion is slow, 3 is default.
+ dictionary_search_threshold = 3,
+
+ -- See above
+ score_offset = 0,
+
+ -- See above
+ definition_pointers = { "!", "&", "^" },
+ },
+ },
+ },
+ per_filetype = {
+ text = { "dictionary" },
+ markdown = { "dictionary" },
+ },
+ },
+
+ -- (Default) Rust fuzzy matcher for typo resistance and significantly better performance
+ -- You may use a lua implementation instead by using `implementation = "lua"` or fallback to the lua implementation,
+ -- when the Rust fuzzy matcher is not available, by using `implementation = "prefer_rust"`
+ --
+ -- See the fuzzy documentation for more information
+ fuzzy = { implementation = "prefer_rust_with_warning" },
+ cmdline = {
+ completion = {
+ menu = {
+ auto_show = true,
+ },
+ },
+ keymap = {
+ [""] = { "select_and_accept", "fallback" },
+ },
+ },
+}
diff --git a/nvim/lua/config/bqf.lua b/nvim/lua/config/bqf.lua
new file mode 100644
index 0000000..c62a513
--- /dev/null
+++ b/nvim/lua/config/bqf.lua
@@ -0,0 +1,6 @@
+require("bqf").setup {
+ auto_resize_height = false,
+ preview = {
+ auto_preview = false,
+ },
+}
diff --git a/nvim/lua/config/bufferline.lua b/nvim/lua/config/bufferline.lua
new file mode 100644
index 0000000..747fe18
--- /dev/null
+++ b/nvim/lua/config/bufferline.lua
@@ -0,0 +1,50 @@
+require("bufferline").setup {
+ options = {
+ numbers = "none",
+ close_command = "bdelete! %d",
+ right_mouse_command = nil,
+ left_mouse_command = "buffer %d",
+ middle_mouse_command = nil,
+ indicator = {
+ icon = "▎", -- this should be omitted if indicator style is not 'icon'
+ style = "icon",
+ },
+ buffer_close_icon = "",
+ modified_icon = "●",
+ close_icon = "",
+ left_trunc_marker = "",
+ right_trunc_marker = "",
+ max_name_length = 18,
+ max_prefix_length = 15,
+ tab_size = 10,
+ diagnostics = false,
+ custom_filter = function(bufnr)
+ -- if the result is false, this buffer will be shown, otherwise, this
+ -- buffer will be hidden.
+
+ -- filter out filetypes you don't want to see
+ local exclude_ft = { "qf", "fugitive", "git" }
+ local cur_ft = vim.bo[bufnr].filetype
+ local should_filter = vim.tbl_contains(exclude_ft, cur_ft)
+
+ if should_filter then
+ return false
+ end
+
+ return true
+ end,
+ show_buffer_icons = false,
+ show_buffer_close_icons = true,
+ show_close_icon = true,
+ show_tab_indicators = true,
+ persist_buffer_sort = true, -- whether or not custom sorted buffers should persist
+ separator_style = "bar",
+ enforce_regular_tabs = false,
+ always_show_bufferline = true,
+ sort_by = "id",
+ },
+}
+
+vim.keymap.set("n", "bp", "BufferLinePick", {
+ desc = "pick a buffer",
+})
diff --git a/nvim/lua/config/diffview.lua b/nvim/lua/config/diffview.lua
new file mode 100644
index 0000000..97d2c57
--- /dev/null
+++ b/nvim/lua/config/diffview.lua
@@ -0,0 +1,51 @@
+local ok, diffview = pcall(require, "diffview")
+if not ok then
+ return
+end
+
+local actions = require("diffview.actions")
+local prefix_conflicts = "gC"
+
+diffview.setup {
+ enhanced_diff_hl = true,
+ view = {
+ default = {
+ disable_diagnostics = true,
+ },
+ merge_tool = {
+ layout = "diff3_mixed",
+ },
+ },
+ file_history_panel = {
+ win_config = {
+ type = "split",
+ position = "bottom",
+ height = 10,
+ },
+ },
+ keymaps = {
+ view = {
+ {
+ "n",
+ prefix_conflicts .. "t",
+ actions.conflict_choose("theirs"),
+ { desc = "Conflict choose theirs" },
+ },
+ {
+ "n",
+ prefix_conflicts .. "o",
+ actions.conflict_choose("ours"),
+ { desc = "Conflict choose ours" },
+ },
+ {
+ "n",
+ prefix_conflicts .. "a",
+ actions.conflict_choose("all"),
+ { desc = "Conflict choose both" },
+ },
+
+ { "n", "]C", actions.next_conflict, { desc = "Next conflict" } },
+ { "n", "[C", actions.prev_conflict, { desc = "Previous conflict" } },
+ },
+ },
+}
diff --git a/nvim/lua/config/fidget-nvim.lua b/nvim/lua/config/fidget-nvim.lua
new file mode 100644
index 0000000..945f564
--- /dev/null
+++ b/nvim/lua/config/fidget-nvim.lua
@@ -0,0 +1 @@
+require("fidget").setup {}
diff --git a/nvim/lua/config/fugitive.lua b/nvim/lua/config/fugitive.lua
new file mode 100644
index 0000000..306d71b
--- /dev/null
+++ b/nvim/lua/config/fugitive.lua
@@ -0,0 +1,27 @@
+local keymap = vim.keymap
+
+keymap.set("n", "gs", "Git", { desc = "Git: show status" })
+keymap.set("n", "gw", "Gwrite", { desc = "Git: add file" })
+keymap.set("n", "gc", "Git commit", { desc = "Git: commit changes" })
+keymap.set("n", "gpl", "Git pull", { desc = "Git: pull changes" })
+keymap.set("n", "gpu", "15 split|term git push", { desc = "Git: push changes" })
+keymap.set("v", "gb", ":Git blame", { desc = "Git: blame selected line" })
+
+-- convert git to Git in command line mode
+vim.fn["utils#Cabbrev"]("git", "Git")
+
+keymap.set("n", "gbn", function()
+ vim.ui.input({ prompt = "Enter a new branch name" }, function(user_input)
+ if user_input == nil or user_input == "" then
+ return
+ end
+
+ local cmd_str = string.format("G checkout -b %s", user_input)
+ vim.cmd(cmd_str)
+ end)
+end, {
+ desc = "Git: create new branch",
+})
+
+keymap.set("n", "gf", ":Git fetch ", { desc = "Git: prune branches" })
+keymap.set("n", "gbd", ":Git branch -D ", { desc = "Git: delete branch" })
diff --git a/nvim/lua/config/fzf-lua.lua b/nvim/lua/config/fzf-lua.lua
new file mode 100644
index 0000000..50cdcb7
--- /dev/null
+++ b/nvim/lua/config/fzf-lua.lua
@@ -0,0 +1,43 @@
+require("fzf-lua").setup {
+ defaults = {
+ file_icons = "mini",
+ },
+ winopts = {
+ row = 0.5,
+ height = 0.7,
+ },
+ files = {
+ previewer = false,
+ git_icons = true,
+ -- using .gitignore is usually good, but still we may want to include some files,
+ -- you can create a file `.rgignore` to "unignore" those files, e.g., `.env` files.
+ -- see also https://github.com/BurntSushi/ripgrep/discussions/2512
+ -- and https://www.reddit.com/r/linuxquestions/comments/zycvud/ripgrep_respect_gitignore_but_show_env_files/
+ no_ignore = false,
+ },
+ grep = {
+ RIPGREP_CONFIG_PATH = vim.env.RIPGREP_CONFIG_PATH,
+ },
+}
+
+vim.keymap.set("n", "ff", "FzfLua files", { desc = "Fuzzy find files" })
+vim.keymap.set("n", "fg", "FzfLua live_grep_native", { desc = "Fuzzy grep files" })
+vim.keymap.set(
+ "n",
+ "fh",
+ "FzfLua helptags",
+ { desc = "Fuzzy grep tags in help files" }
+)
+vim.keymap.set("n", "ft", "FzfLua btags", { desc = "Fuzzy search buffer tags" })
+vim.keymap.set(
+ "n",
+ "fb",
+ "FzfLua buffers",
+ { desc = "Fuzzy search opened buffers" }
+)
+vim.keymap.set(
+ "n",
+ "fr",
+ "FzfLua oldfiles",
+ { desc = "Fuzzy search opened files history" }
+)
diff --git a/nvim/lua/config/git-linker.lua b/nvim/lua/config/git-linker.lua
new file mode 100644
index 0000000..cbb4242
--- /dev/null
+++ b/nvim/lua/config/git-linker.lua
@@ -0,0 +1,47 @@
+local keymap = vim.keymap
+local gitlinker = require("gitlinker")
+
+gitlinker.setup {
+ callbacks = {
+ ["dev.azure.com"] = function(url_data)
+ vim.print(url_data)
+ local url = require("gitlinker.hosts").get_base_https_url(url_data)
+
+ if url_data.lstart then
+ if url_data.lend == nil then
+ url_data.lend = url_data.lstart
+ end
+ url = url
+ .. "?path=/"
+ .. url_data.file
+ .. "&version=GC"
+ .. url_data.rev
+ .. "&line="
+ .. url_data.lstart
+ .. "&lineEnd="
+ .. url_data.lend
+ .. "&lineStartColumn=1"
+ .. "&lineEndColumn=120"
+ end
+ return url
+ end,
+ },
+ mappings = nil,
+}
+
+keymap.set({ "n", "v" }, "gl", function()
+ local mode = string.lower(vim.fn.mode())
+ gitlinker.get_buf_range_url(mode)
+end, {
+ silent = true,
+ desc = "Git: get permlink",
+})
+
+keymap.set("n", "gbr", function()
+ gitlinker.get_repo_url {
+ action_callback = gitlinker.actions.open_in_browser,
+ }
+end, {
+ silent = true,
+ desc = "Git: browse repo in browser",
+})
diff --git a/nvim/lua/config/gitsigns.lua b/nvim/lua/config/gitsigns.lua
new file mode 100644
index 0000000..c7216b7
--- /dev/null
+++ b/nvim/lua/config/gitsigns.lua
@@ -0,0 +1,57 @@
+local gs = require("gitsigns")
+
+gs.setup {
+ signs = {
+ add = { text = "+" },
+ change = { text = "~" },
+ delete = { text = "_" },
+ topdelete = { text = "‾" },
+ changedelete = { text = "│" },
+ },
+ word_diff = false,
+ on_attach = function(bufnr)
+ local function map(mode, l, r, opts)
+ opts = opts or {}
+ opts.buffer = bufnr
+ vim.keymap.set(mode, l, r, opts)
+ end
+
+ -- Navigation
+ map("n", "]c", function()
+ if vim.wo.diff then
+ return "]c"
+ end
+ vim.schedule(function()
+ gs.next_hunk()
+ end)
+ return ""
+ end, { expr = true, desc = "next hunk" })
+
+ map("n", "[c", function()
+ if vim.wo.diff then
+ return "[c"
+ end
+ vim.schedule(function()
+ gs.prev_hunk()
+ end)
+ return ""
+ end, { expr = true, desc = "previous hunk" })
+
+ -- Actions
+ map("n", "hp", gs.preview_hunk, { desc = "preview hunk" })
+ map("n", "hb", function()
+ gs.blame_line { full = true }
+ end, { desc = "blame hunk" })
+ end,
+}
+
+vim.api.nvim_create_autocmd("ColorScheme", {
+ pattern = "*",
+ callback = function()
+ vim.cmd([[
+ hi GitSignsChangeInline gui=reverse
+ hi GitSignsAddInline gui=reverse
+ hi GitSignsDeleteInline gui=reverse
+ ]])
+ end,
+})
diff --git a/nvim/lua/config/glance.lua b/nvim/lua/config/glance.lua
new file mode 100644
index 0000000..daaef82
--- /dev/null
+++ b/nvim/lua/config/glance.lua
@@ -0,0 +1,12 @@
+local glance = require("glance")
+
+glance.setup {
+ height = 25,
+ border = {
+ enable = true,
+ },
+}
+
+vim.keymap.set("n", "gd", "Glance definitions")
+vim.keymap.set("n", "gr", "Glance references")
+vim.keymap.set("n", "gi", "Glance implementations")
diff --git a/nvim/lua/config/gx.lua b/nvim/lua/config/gx.lua
new file mode 100644
index 0000000..e8109ad
--- /dev/null
+++ b/nvim/lua/config/gx.lua
@@ -0,0 +1,21 @@
+---@diagnostic disable-next-line: missing-fields
+require("gx").setup {
+ handlers = {
+ jira = {
+ name = "jira", -- set name of handler
+ handle = function(mode, line, _)
+ local ticket = require("gx.helper").find(line, mode, "(%a%a%a+%-%d+)")
+
+ local dotenv = require("dotenv")
+ local env_path = vim.fs.joinpath(vim.fn.stdpath("config"), ".env")
+ dotenv.load_dotenv(env_path)
+ local company_name = dotenv.get("COMPANY_NAME")
+
+ if ticket and #ticket < 20 then
+ local ticket_link = string.format("http://jira.%s/browse/%s", company_name, ticket)
+ return ticket_link
+ end
+ end,
+ },
+ },
+}
diff --git a/nvim/lua/config/hlslens.lua b/nvim/lua/config/hlslens.lua
new file mode 100644
index 0000000..e216a64
--- /dev/null
+++ b/nvim/lua/config/hlslens.lua
@@ -0,0 +1,82 @@
+local api = vim.api
+local keymap = vim.keymap
+
+local hlslens = require("hlslens")
+
+hlslens.setup {
+ calm_down = true,
+ nearest_only = true,
+}
+
+local activate_hlslens = function(direction)
+ local cmd = string.format("normal! %s%szzzv", vim.v.count1, direction)
+ local status, msg = pcall(vim.cmd, cmd)
+
+ -- Deal with the case that there is no such pattern in current buffer.
+ if not status then
+ local start_idx, _ = string.find(msg, "E486", 1, true)
+ local msg_part = string.sub(msg, start_idx)
+ api.nvim_echo({ { msg_part } }, true, { err = true })
+ return
+ end
+
+ hlslens.start()
+end
+
+keymap.set("n", "n", "", {
+ callback = function()
+ activate_hlslens("n")
+ end,
+})
+
+keymap.set("n", "N", "", {
+ callback = function()
+ activate_hlslens("N")
+ end,
+})
+
+local check_cursor_word = function()
+ local cursor_word = vim.fn.expand("")
+ local result = cursor_word == ""
+ if result then
+ local msg = "E348: No string under cursor"
+ api.nvim_echo({ { msg } }, true, { err = true })
+ end
+
+ return result, cursor_word
+end
+
+keymap.set("n", "*", "", {
+ callback = function()
+ local cursor_word_empty, cursor_word = check_cursor_word()
+ if cursor_word_empty then
+ return
+ end
+
+ local cmd = string.format([[normal! /\v<%s>]], cursor_word)
+
+ -- In order to say that we are pressing Enter key, instead of typing literally the character,
+ -- we need to replace special notation with their internal representation.
+ local escaped_enter = vim.api.nvim_replace_termcodes("", true, false, true)
+
+ -- character `N` is used to keep the cursor when pressing `*`
+ local full_cmd = cmd .. escaped_enter .. "N"
+ vim.fn.execute(full_cmd)
+ hlslens.start()
+ end,
+})
+keymap.set("n", "#", "", {
+ callback = function()
+ local cursor_word_empty, cursor_word = check_cursor_word()
+ if cursor_word_empty then
+ return
+ end
+
+ local cmd = string.format([[normal! ?\v<%s>]], cursor_word)
+ local escaped_enter = vim.api.nvim_replace_termcodes("", true, false, true)
+
+ local full_cmd = cmd .. escaped_enter .. "N"
+ vim.fn.execute(full_cmd)
+ hlslens.start()
+ end,
+})
diff --git a/nvim/lua/config/iron.lua b/nvim/lua/config/iron.lua
new file mode 100644
index 0000000..67f5d81
--- /dev/null
+++ b/nvim/lua/config/iron.lua
@@ -0,0 +1,7 @@
+local iron = require("iron")
+iron.core.set_config {
+ preferred = {
+ python = "ipython",
+ },
+ repl_open_cmd = "vertical 120 split",
+}
diff --git a/nvim/lua/config/iswap.lua b/nvim/lua/config/iswap.lua
new file mode 100644
index 0000000..cf29e1d
--- /dev/null
+++ b/nvim/lua/config/iswap.lua
@@ -0,0 +1,5 @@
+require("iswap").setup {
+ move_cursor = true,
+}
+vim.keymap.set("n", "gs<", "ISwapWithLeft")
+vim.keymap.set("n", "gs>", "ISwapWithRight")
diff --git a/nvim/lua/config/lightbulb.lua b/nvim/lua/config/lightbulb.lua
new file mode 100644
index 0000000..8f49746
--- /dev/null
+++ b/nvim/lua/config/lightbulb.lua
@@ -0,0 +1,20 @@
+---@diagnostic disable: missing-fields
+require("nvim-lightbulb").setup {
+ autocmd = {
+ enabled = true,
+ updatetime = -1,
+ },
+ ---@diagnostic disable-next-line: unused-local
+ filter = function(client_name, result)
+ -- Ruff always sends these two actions even if there are no action to take,
+ -- so it is better to just ignore this to avoid noise. See also discussion below:
+ -- https://github.com/astral-sh/ruff-lsp/issues/91
+ local ignored_kinds = { "source.fixAll.ruff", "source.organizeImports.ruff" }
+
+ if vim.tbl_contains(ignored_kinds, result.kind) then
+ return false
+ end
+
+ return true
+ end,
+}
diff --git a/nvim/lua/config/live-command.lua b/nvim/lua/config/live-command.lua
new file mode 100644
index 0000000..4680c8e
--- /dev/null
+++ b/nvim/lua/config/live-command.lua
@@ -0,0 +1,9 @@
+require("live-command").setup {
+ enable_highlighting = true,
+ inline_highlighting = true,
+ commands = {
+ Norm = { cmd = "norm" },
+ },
+}
+
+vim.cmd("cnoreabbrev norm Norm")
diff --git a/nvim/lua/config/lualine.lua b/nvim/lua/config/lualine.lua
new file mode 100644
index 0000000..8aed3c7
--- /dev/null
+++ b/nvim/lua/config/lualine.lua
@@ -0,0 +1,311 @@
+local utils = require("utils")
+local fn = vim.fn
+
+-- cache for git states
+local git_status_cache = {
+ fetch_success = false,
+ behind_count = 0,
+ ahead_count = 0,
+}
+
+local on_exit_fetch = function(result)
+ if result.code == 0 then
+ git_status_cache.fetch_success = true
+ end
+end
+
+local function handle_numeric_result(cache_key)
+ return function(result)
+ if result.code == 0 then
+ git_status_cache[cache_key] = tonumber(result.stdout:match("(%d+)")) or 0
+ else
+ -- when the git command fails, it usually means there are some changes in your branch. For example, you
+ -- on branchA, for this one, you have upstream branch. Then you changed to branchB, and there is no upstream
+ -- branch, the git rev-list command will error out. In this case, we should clear the cache
+ -- vim.print("Error running git command", result)
+ git_status_cache[cache_key] = 0
+ end
+ end
+end
+
+local async_cmd = function(cmd_str, on_exit)
+ local cmd = vim.tbl_filter(function(element)
+ return element ~= ""
+ end, vim.split(cmd_str, " "))
+
+ vim.system(cmd, { text = true }, on_exit)
+end
+
+local async_git_status_update = function()
+ -- Fetch the latest changes from the remote repository (replace 'origin' if needed)
+ async_cmd("git fetch origin", on_exit_fetch)
+ if not git_status_cache.fetch_success then
+ return
+ end
+
+ -- Get the number of commits behind
+ -- the @{upstream} notation is inspired by post: https://www.reddit.com/r/neovim/comments/t48x5i/git_branch_aheadbehind_info_status_line_component/
+ -- note that here we should use double dots instead of triple dots
+ local behind_cmd_str = "git rev-list --count HEAD..@{upstream}"
+ async_cmd(behind_cmd_str, handle_numeric_result("behind_count"))
+
+ -- Get the number of commits ahead
+ local ahead_cmd_str = "git rev-list --count @{upstream}..HEAD"
+ async_cmd(ahead_cmd_str, handle_numeric_result("ahead_count"))
+end
+
+local function get_git_ahead_behind_info()
+ async_git_status_update()
+
+ local status = git_status_cache
+ if not status then
+ return ""
+ end
+
+ local msg = ""
+
+ if type(status.ahead_count) == "number" and status.ahead_count > 0 then
+ local ahead_str = string.format("↑[%d] ", status.ahead_count)
+ msg = msg .. ahead_str
+ end
+
+ if type(status.behind_count) == "number" and status.behind_count > 0 then
+ local behind_str = string.format("↓[%d] ", status.behind_count)
+ msg = msg .. behind_str
+ end
+
+ return msg
+end
+
+local function spell()
+ if vim.o.spell then
+ return string.format("[SPELL]")
+ end
+
+ return ""
+end
+
+--- show indicator for Chinese IME
+local function ime_state()
+ if vim.g.is_mac then
+ -- ref: https://github.com/vim-airline/vim-airline/blob/master/autoload/airline/extensions/xkblayout.vim#L11
+ local layout = fn.libcall(vim.g.XkbSwitchLib, "Xkb_Switch_getXkbLayout", "")
+
+ -- We can use `xkbswitch -g` on the command line to get current mode.
+ -- mode for macOS builtin pinyin IME: com.apple.inputmethod.SCIM.ITABC
+ -- mode for Rime: im.rime.inputmethod.Squirrel.Rime
+ local res = fn.match(layout, [[\v(Squirrel\.Rime|SCIM.ITABC)]])
+ if res ~= -1 then
+ return "[CN]"
+ end
+ end
+
+ return ""
+end
+
+local function trailing_space()
+ if not vim.o.modifiable then
+ return ""
+ end
+
+ local line_num = nil
+
+ for i = 1, fn.line("$") do
+ local linetext = fn.getline(i)
+ -- To prevent invalid escape error, we wrap the regex string with `[[]]`.
+ local idx = fn.match(linetext, [[\v\s+$]])
+
+ if idx ~= -1 then
+ line_num = i
+ break
+ end
+ end
+
+ local msg = ""
+ if line_num ~= nil then
+ msg = string.format("[%d]trailing", line_num)
+ end
+
+ return msg
+end
+
+local function mixed_indent()
+ if not vim.o.modifiable then
+ return ""
+ end
+
+ local space_pat = [[\v^ +]]
+ local tab_pat = [[\v^\t+]]
+ local space_indent = fn.search(space_pat, "nwc")
+ local tab_indent = fn.search(tab_pat, "nwc")
+ local mixed = (space_indent > 0 and tab_indent > 0)
+ local mixed_same_line
+ if not mixed then
+ mixed_same_line = fn.search([[\v^(\t+ | +\t)]], "nwc")
+ mixed = mixed_same_line > 0
+ end
+ if not mixed then
+ return ""
+ end
+ if mixed_same_line ~= nil and mixed_same_line > 0 then
+ return "MI:" .. mixed_same_line
+ end
+ local space_indent_cnt = fn.searchcount({ pattern = space_pat, max_count = 1e3 }).total
+ local tab_indent_cnt = fn.searchcount({ pattern = tab_pat, max_count = 1e3 }).total
+ if space_indent_cnt > tab_indent_cnt then
+ return "MI:" .. tab_indent
+ else
+ return "MI:" .. space_indent
+ end
+end
+
+local diff = function()
+ local git_status = vim.b.gitsigns_status_dict
+ if git_status == nil then
+ return
+ end
+
+ local modify_num = git_status.changed
+ local remove_num = git_status.removed
+ local add_num = git_status.added
+
+ local info = { added = add_num, modified = modify_num, removed = remove_num }
+ -- vim.print(info)
+ return info
+end
+
+local virtual_env = function()
+ local venv_name = utils.get_virtual_env()
+
+ if venv_name ~= "" then
+ return string.format(" (%s)", venv_name)
+ else
+ return ""
+ end
+end
+
+local get_active_lsp = function()
+ local msg = "🚫"
+ local buf_ft = vim.api.nvim_get_option_value("filetype", {})
+ local clients = vim.lsp.get_clients { bufnr = 0 }
+ if next(clients) == nil then
+ return msg
+ end
+
+ for _, client in ipairs(clients) do
+ ---@diagnostic disable-next-line: undefined-field
+ local filetypes = client.config.filetypes
+ if filetypes and vim.fn.index(filetypes, buf_ft) ~= -1 then
+ return client.name
+ end
+ end
+ return msg
+end
+
+require("lualine").setup {
+ options = {
+ icons_enabled = true,
+ theme = "auto",
+ component_separators = { left = "|", right = "|" },
+ section_separators = "",
+ disabled_filetypes = {},
+ always_divide_middle = true,
+ refresh = {
+ statusline = 1000,
+ },
+ },
+ sections = {
+ lualine_a = {
+ {
+ "filename",
+ symbols = {
+ readonly = "[🔒]",
+ },
+ },
+ },
+ lualine_b = {
+ {
+ "branch",
+ fmt = function(name, _)
+ -- truncate branch name in case the name is too long
+ return string.sub(name, 1, 20)
+ end,
+ color = { gui = "italic,bold" },
+ },
+ {
+ get_git_ahead_behind_info,
+ color = { fg = "#E0C479" },
+ },
+ {
+ "diff",
+ source = diff,
+ },
+ {
+ virtual_env,
+ color = { fg = "black", bg = "#F1CA81" },
+ },
+ },
+ lualine_c = {
+ {
+ "%S",
+ color = { gui = "bold", fg = "cyan" },
+ },
+ {
+ spell,
+ color = { fg = "black", bg = "#a7c080" },
+ },
+ },
+ lualine_x = {
+ {
+ get_active_lsp,
+ icon = "📡",
+ },
+ {
+ "diagnostics",
+ sources = { "nvim_diagnostic" },
+ symbols = { error = "🆇 ", warn = "⚠️ ", info = "ℹ️ ", hint = " " },
+ },
+ {
+ trailing_space,
+ color = "WarningMsg",
+ },
+ {
+ mixed_indent,
+ color = "WarningMsg",
+ },
+ },
+ lualine_y = {
+ {
+ "encoding",
+ fmt = string.upper,
+ },
+ {
+ "fileformat",
+ symbols = {
+ unix = "unix",
+ dos = "win",
+ mac = "mac",
+ },
+ },
+ "filetype",
+ {
+ ime_state,
+ color = { fg = "black", bg = "#f46868" },
+ },
+ },
+ lualine_z = {
+ "location",
+ "progress",
+ },
+ },
+ inactive_sections = {
+ lualine_a = {},
+ lualine_b = {},
+ lualine_c = { "filename" },
+ lualine_x = { "location" },
+ lualine_y = {},
+ lualine_z = {},
+ },
+ tabline = {},
+ extensions = { "quickfix", "fugitive", "nvim-tree" },
+}
diff --git a/nvim/lua/config/nvim-cmp.lua b/nvim/lua/config/nvim-cmp.lua
new file mode 100644
index 0000000..b5fc78e
--- /dev/null
+++ b/nvim/lua/config/nvim-cmp.lua
@@ -0,0 +1,111 @@
+-- Setup nvim-cmp.
+local cmp = require("cmp")
+
+-- The extentions needed by nvim-cmp should be loaded beforehand
+require("cmp_nvim_lsp")
+require("cmp_path")
+require("cmp_buffer")
+require("cmp_omni")
+require("cmp_nvim_ultisnips")
+require("cmp_cmdline")
+
+local MiniIcons = require("mini.icons")
+
+cmp.setup {
+ snippet = {
+ expand = function(args)
+ -- For `ultisnips` user.
+ vim.fn["UltiSnips#Anon"](args.body)
+ end,
+ },
+ mapping = cmp.mapping.preset.insert {
+ [""] = function(fallback)
+ if cmp.visible() then
+ cmp.select_next_item()
+ else
+ fallback()
+ end
+ end,
+ [""] = function(fallback)
+ if cmp.visible() then
+ cmp.select_prev_item()
+ else
+ fallback()
+ end
+ end,
+ [""] = cmp.mapping.confirm { select = true },
+ [""] = cmp.mapping.abort(),
+ [""] = cmp.mapping.close(),
+ [""] = cmp.mapping.scroll_docs(-4),
+ [""] = cmp.mapping.scroll_docs(4),
+ },
+ sources = {
+ { name = "nvim_lsp" }, -- For nvim-lsp
+ { name = "ultisnips" }, -- For ultisnips user.
+ { name = "path" }, -- for path completion
+ { name = "buffer", keyword_length = 2 }, -- for buffer word completion
+ },
+ completion = {
+ keyword_length = 1,
+ completeopt = "menu,noselect",
+ },
+ view = {
+ entries = "custom",
+ },
+ -- solution taken from https://github.com/echasnovski/mini.nvim/issues/1007#issuecomment-2258929830
+ formatting = {
+ format = function(_, vim_item)
+ local icon, hl = MiniIcons.get("lsp", vim_item.kind)
+ vim_item.kind = icon .. " " .. vim_item.kind
+ vim_item.kind_hl_group = hl
+ return vim_item
+ end,
+ },
+}
+
+cmp.setup.filetype("tex", {
+ sources = {
+ { name = "omni" },
+ { name = "ultisnips" }, -- For ultisnips user.
+ { name = "buffer", keyword_length = 2 }, -- for buffer word completion
+ { name = "path" }, -- for path completion
+ },
+})
+
+cmp.setup.cmdline("/", {
+ mapping = cmp.mapping.preset.cmdline(),
+ sources = {
+ { name = "buffer" },
+ },
+})
+
+cmp.setup.cmdline(":", {
+ mapping = cmp.mapping.preset.cmdline(),
+ sources = cmp.config.sources({
+ { name = "path" },
+ }, {
+ { name = "cmdline" },
+ }),
+ matching = { disallow_symbol_nonprefix_matching = false },
+})
+
+-- see https://github.com/hrsh7th/nvim-cmp/wiki/Menu-Appearance#how-to-add-visual-studio-code-dark-theme-colors-to-the-menu
+vim.cmd([[
+ highlight! link CmpItemMenu Comment
+ " gray
+ highlight! CmpItemAbbrDeprecated guibg=NONE gui=strikethrough guifg=#808080
+ " blue
+ highlight! CmpItemAbbrMatch guibg=NONE guifg=#569CD6
+ highlight! CmpItemAbbrMatchFuzzy guibg=NONE guifg=#569CD6
+ " light blue
+ highlight! CmpItemKindVariable guibg=NONE guifg=#9CDCFE
+ highlight! CmpItemKindInterface guibg=NONE guifg=#9CDCFE
+ highlight! CmpItemKindText guibg=NONE guifg=#9CDCFE
+ " pink
+ highlight! CmpItemKindFunction guibg=NONE guifg=#C586C0
+ highlight! CmpItemKindMethod guibg=NONE guifg=#C586C0
+ " front
+ highlight! CmpItemKindKeyword guibg=NONE guifg=#D4D4D4
+ highlight! CmpItemKindProperty guibg=NONE guifg=#D4D4D4
+ highlight! CmpItemKindUnit guibg=NONE guifg=#D4D4D4
+]])
diff --git a/nvim/lua/config/nvim-lint.lua b/nvim/lua/config/nvim-lint.lua
new file mode 100644
index 0000000..6b416dd
--- /dev/null
+++ b/nvim/lua/config/nvim-lint.lua
@@ -0,0 +1,16 @@
+local nvim_lint = require("lint")
+
+nvim_lint.linters_by_ft = {
+ -- to use vale, install it: `brew install vale`
+ -- configure it via: https://vale.sh/generator, put config in `.vale.ini` in project root.
+ -- to install cspell and typos: `brew install cspell typos-cli`
+ -- markdown = { "vale", "cspell", "typos" },
+}
+
+vim.api.nvim_create_autocmd({ "InsertLeave" }, {
+ callback = function()
+ -- try_lint without arguments runs the linters defined in `linters_by_ft`
+ -- for the current filetype
+ nvim_lint.try_lint()
+ end,
+})
diff --git a/nvim/lua/config/nvim-notify.lua b/nvim/lua/config/nvim-notify.lua
new file mode 100644
index 0000000..4fcd02e
--- /dev/null
+++ b/nvim/lua/config/nvim-notify.lua
@@ -0,0 +1,10 @@
+local nvim_notify = require("notify")
+nvim_notify.setup {
+ -- Animation style
+ stages = "fade_in_slide_out",
+ -- Default timeout for notifications
+ timeout = 1500,
+ background_colour = "#2E3440",
+}
+
+vim.notify = nvim_notify
diff --git a/nvim/lua/config/nvim-statuscol.lua b/nvim/lua/config/nvim-statuscol.lua
new file mode 100644
index 0000000..3b86047
--- /dev/null
+++ b/nvim/lua/config/nvim-statuscol.lua
@@ -0,0 +1,23 @@
+local builtin = require("statuscol.builtin")
+local ffi = require("statuscol.ffidef")
+local C = ffi.C
+
+-- only show fold level up to this level
+local fold_level_limit = 3
+local function foldfunc(args)
+ local foldinfo = C.fold_info(args.wp, args.lnum)
+ if foldinfo.level > fold_level_limit then
+ return " "
+ end
+
+ return builtin.foldfunc(args)
+end
+
+require("statuscol").setup {
+ relculright = false,
+ segments = {
+ { text = { "%s" }, click = "v:lua.ScSa" },
+ { text = { builtin.lnumfunc, " " }, click = "v:lua.ScLa" },
+ { text = { foldfunc, " " }, condition = { true, builtin.not_empty }, click = "v:lua.ScFa" },
+ },
+}
diff --git a/nvim/lua/config/nvim-tree.lua b/nvim/lua/config/nvim-tree.lua
new file mode 100644
index 0000000..c9196b0
--- /dev/null
+++ b/nvim/lua/config/nvim-tree.lua
@@ -0,0 +1,104 @@
+local keymap = vim.keymap
+local nvim_tree = require("nvim-tree")
+
+nvim_tree.setup {
+ auto_reload_on_write = true,
+ disable_netrw = false,
+ hijack_netrw = true,
+ hijack_cursor = false,
+ hijack_unnamed_buffer_when_opening = false,
+ open_on_tab = false,
+ sort_by = "name",
+ update_cwd = false,
+ view = {
+ width = 30,
+ side = "left",
+ preserve_window_proportions = false,
+ number = false,
+ relativenumber = false,
+ signcolumn = "yes",
+ },
+ renderer = {
+ indent_markers = {
+ enable = false,
+ icons = {
+ corner = "└ ",
+ edge = "│ ",
+ none = " ",
+ },
+ },
+ icons = {
+ webdev_colors = true,
+ },
+ },
+ hijack_directories = {
+ enable = true,
+ auto_open = true,
+ },
+ update_focused_file = {
+ enable = false,
+ update_cwd = false,
+ ignore_list = {},
+ },
+ diagnostics = {
+ enable = false,
+ show_on_dirs = false,
+ icons = {
+ hint = "",
+ info = "",
+ warning = "",
+ error = "",
+ },
+ },
+ filters = {
+ dotfiles = false,
+ custom = {},
+ exclude = {},
+ },
+ git = {
+ enable = true,
+ ignore = true,
+ timeout = 400,
+ },
+ actions = {
+ use_system_clipboard = true,
+ change_dir = {
+ enable = true,
+ global = false,
+ restrict_above_cwd = false,
+ },
+ open_file = {
+ quit_on_open = false,
+ resize_window = false,
+ window_picker = {
+ enable = true,
+ chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
+ exclude = {
+ filetype = { "notify", "qf", "diff", "fugitive", "fugitiveblame" },
+ buftype = { "nofile", "terminal", "help" },
+ },
+ },
+ },
+ },
+ trash = {
+ cmd = "trash",
+ require_confirm = true,
+ },
+ log = {
+ enable = false,
+ truncate = false,
+ types = {
+ all = false,
+ config = false,
+ copy_paste = false,
+ diagnostics = false,
+ git = false,
+ profile = false,
+ },
+ },
+}
+
+keymap.set("n", "s", require("nvim-tree.api").tree.toggle, {
+ silent = true,
+ desc = "toggle nvim-tree",
+})
diff --git a/nvim/lua/config/nvim_hop.lua b/nvim/lua/config/nvim_hop.lua
new file mode 100644
index 0000000..205543d
--- /dev/null
+++ b/nvim/lua/config/nvim_hop.lua
@@ -0,0 +1,28 @@
+local keymap = vim.keymap
+local hop = require("hop")
+hop.setup {
+ case_insensitive = true,
+ char2_fallback_key = "",
+ quit_key = "",
+ match_mappings = { "zh_sc" },
+}
+
+keymap.set({ "n", "v", "o" }, "f", "", {
+ silent = true,
+ noremap = true,
+ callback = function()
+ hop.hint_char2()
+ end,
+ desc = "nvim-hop char2",
+})
+
+vim.api.nvim_create_autocmd("ColorScheme", {
+ pattern = "*",
+ callback = function()
+ vim.cmd([[
+ hi HopNextKey cterm=bold ctermfg=176 gui=bold guibg=#ff00ff guifg=#ffffff
+ hi HopNextKey1 cterm=bold ctermfg=176 gui=bold guibg=#ff00ff guifg=#ffffff
+ hi HopNextKey2 cterm=bold ctermfg=176 gui=bold guibg=#ff00ff guifg=#ffffff
+ ]])
+ end,
+})
diff --git a/nvim/lua/config/nvim_ufo.lua b/nvim/lua/config/nvim_ufo.lua
new file mode 100644
index 0000000..bc1ae60
--- /dev/null
+++ b/nvim/lua/config/nvim_ufo.lua
@@ -0,0 +1,45 @@
+local handler = function(virtText, lnum, endLnum, width, truncate)
+ local newVirtText = {}
+ local foldedLines = endLnum - lnum
+ local suffix = (" %d"):format(foldedLines)
+ local sufWidth = vim.fn.strdisplaywidth(suffix)
+ local targetWidth = width - sufWidth
+ local curWidth = 0
+
+ for _, chunk in ipairs(virtText) do
+ local chunkText = chunk[1]
+ local chunkWidth = vim.fn.strdisplaywidth(chunkText)
+ if targetWidth > curWidth + chunkWidth then
+ table.insert(newVirtText, chunk)
+ else
+ chunkText = truncate(chunkText, targetWidth - curWidth)
+ local hlGroup = chunk[2]
+ table.insert(newVirtText, { chunkText, hlGroup })
+ chunkWidth = vim.fn.strdisplaywidth(chunkText)
+ -- str width returned from truncate() may less than 2nd argument, need padding
+ if curWidth + chunkWidth < targetWidth then
+ suffix = suffix .. (" "):rep(targetWidth - curWidth - chunkWidth)
+ end
+ break
+ end
+ curWidth = curWidth + chunkWidth
+ end
+ local rAlignAppndx =
+ math.max(math.min(vim.opt.textwidth["_value"], width - 1) - curWidth - sufWidth, 0)
+ suffix = (" "):rep(rAlignAppndx) .. suffix
+ table.insert(newVirtText, { suffix, "MoreMsg" })
+ return newVirtText
+end
+
+require("ufo").setup {
+ fold_virt_text_handler = handler,
+}
+
+vim.keymap.set("n", "zR", require("ufo").openAllFolds)
+vim.keymap.set("n", "zM", require("ufo").closeAllFolds)
+vim.keymap.set("n", "zr", require("ufo").openFoldsExceptKinds)
+vim.keymap.set("n", "K", function()
+ local _ = require("ufo").peekFoldedLinesUnderCursor()
+end, {
+ desc = "Preview folded maps",
+})
diff --git a/nvim/lua/config/snacks.lua b/nvim/lua/config/snacks.lua
new file mode 100644
index 0000000..81c7dff
--- /dev/null
+++ b/nvim/lua/config/snacks.lua
@@ -0,0 +1,12 @@
+require("snacks").setup {
+ -- more beautiful vim.ui.input
+ input = {
+ enabled = true,
+ win = {
+ relative = "cursor",
+ backdrop = true,
+ },
+ },
+ -- more beautiful vim.ui.select
+ picker = { enabled = true },
+}
diff --git a/nvim/lua/config/treesitter-textobjects.lua b/nvim/lua/config/treesitter-textobjects.lua
new file mode 100644
index 0000000..051e828
--- /dev/null
+++ b/nvim/lua/config/treesitter-textobjects.lua
@@ -0,0 +1,50 @@
+-- configuration
+require("nvim-treesitter-textobjects").setup {
+ select = {
+ -- Automatically jump forward to textobj, similar to targets.vim
+ lookahead = true,
+ -- You can choose the select mode (default is charwise 'v')
+ --
+ -- Can also be a function which gets passed a table with the keys
+ -- * query_string: eg '@function.inner'
+ -- * method: eg 'v' or 'o'
+ -- and should return the mode ('v', 'V', or '') or a table
+ -- mapping query_strings to modes.
+ selection_modes = {
+ ["@function.inner"] = "V", -- linewise
+ ["@function.outer"] = "V", -- linewise
+ ["@class.outer"] = "V", -- linewise
+ ["@class.inner"] = "V", -- linewise
+ ["@parameter.outer"] = "v", -- charwise
+ },
+ -- If you set this to `true` (default is `false`) then any textobject is
+ -- extended to include preceding or succeeding whitespace. Succeeding
+ -- whitespace has priority in order to act similarly to eg the built-in
+ -- `ap`.
+ --
+ -- Can also be a function which gets passed a table with the keys
+ -- * query_string: eg '@function.inner'
+ -- * selection_mode: eg 'v'
+ -- and should return true of false
+ include_surrounding_whitespace = false,
+ },
+}
+
+-- keymaps
+-- You can use the capture groups defined in `textobjects.scm`
+vim.keymap.set({ "x", "o" }, "af", function()
+ require("nvim-treesitter-textobjects.select").select_textobject("@function.outer", "textobjects")
+end)
+vim.keymap.set({ "x", "o" }, "if", function()
+ require("nvim-treesitter-textobjects.select").select_textobject("@function.inner", "textobjects")
+end)
+vim.keymap.set({ "x", "o" }, "ac", function()
+ require("nvim-treesitter-textobjects.select").select_textobject("@class.outer", "textobjects")
+end)
+vim.keymap.set({ "x", "o" }, "ic", function()
+ require("nvim-treesitter-textobjects.select").select_textobject("@class.inner", "textobjects")
+end)
+-- You can also use captures from other query groups like `locals.scm`
+vim.keymap.set({ "x", "o" }, "as", function()
+ require("nvim-treesitter-textobjects.select").select_textobject("@local.scope", "locals")
+end)
diff --git a/nvim/lua/config/treesitter.lua b/nvim/lua/config/treesitter.lua
new file mode 100644
index 0000000..52f2f2e
--- /dev/null
+++ b/nvim/lua/config/treesitter.lua
@@ -0,0 +1,61 @@
+-- a list of filetypes to install treesitter parsers and queries
+local nvim_treesitter = require("nvim-treesitter")
+
+local ensure_installed = {
+ "cpp",
+ "diff",
+ "go",
+ "gomod",
+ "gosum",
+ "javascript",
+ "json",
+ "lua",
+ "markdown",
+ "python",
+ "sh",
+ "toml",
+ "typescript",
+ "vim",
+ "yaml",
+ "zsh",
+}
+
+vim.api.nvim_create_autocmd("FileType", {
+ pattern = ensure_installed,
+
+ callback = function(args)
+ local ft = vim.bo[args.buf].filetype
+ local lang = vim.treesitter.language.get_lang(ft)
+ if lang == nil then
+ return
+ end
+
+ -- check if parser is available
+ local is_parser_available = vim.treesitter.language.add(lang)
+ if not is_parser_available then
+ local available_langs = vim.g.ts_available or nvim_treesitter.get_available()
+ if not vim.g.ts_available then
+ vim.g.ts_available = available_langs
+ end
+
+ if vim.tbl_contains(available_langs, lang) then
+ -- install treesitter parsers and queries
+ local install_msg = string.format("Installing parsers and queries for %s", lang)
+ vim.print(install_msg)
+ require("nvim-treesitter").install(lang)
+ end
+ end
+
+ if vim.treesitter.language.add(lang) then
+ -- start treesitter highlighting
+ vim.treesitter.start(args.buf, lang)
+
+ -- the following two statements will enable treesitter folding
+ -- vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()"
+ -- vim.wo[0][0].foldmethod = "expr"
+
+ -- enable treesitter-based indentation
+ -- vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
+ end
+ end,
+})
diff --git a/nvim/lua/config/which-key.lua b/nvim/lua/config/which-key.lua
new file mode 100644
index 0000000..136895a
--- /dev/null
+++ b/nvim/lua/config/which-key.lua
@@ -0,0 +1,6 @@
+require("which-key").setup {
+ preset = "modern",
+ icons = {
+ mappings = false,
+ },
+}
diff --git a/nvim/lua/config/yanky.lua b/nvim/lua/config/yanky.lua
new file mode 100644
index 0000000..2c7b134
--- /dev/null
+++ b/nvim/lua/config/yanky.lua
@@ -0,0 +1,17 @@
+require("yanky").setup {
+ preserve_cursor_position = {
+ enabled = false,
+ },
+ highlight = {
+ on_put = true,
+ on_yank = false,
+ timer = 300,
+ },
+}
+
+vim.keymap.set({ "n", "x" }, "p", "(YankyPutAfter)")
+vim.keymap.set({ "n", "x" }, "P", "(YankyPutBefore)")
+
+-- cycle through the yank history, only work after paste
+vim.keymap.set("n", "[y", "(YankyPreviousEntry)")
+vim.keymap.set("n", "]y", "(YankyNextEntry)")
diff --git a/nvim/lua/custom-autocmd.lua b/nvim/lua/custom-autocmd.lua
new file mode 100644
index 0000000..57d31a1
--- /dev/null
+++ b/nvim/lua/custom-autocmd.lua
@@ -0,0 +1,308 @@
+local fn = vim.fn
+local api = vim.api
+
+local utils = require("utils")
+
+-- Display a message when the current file is not in utf-8 format.
+-- Note that we need to use `unsilent` command here because of this issue:
+-- https://github.com/vim/vim/issues/4379
+api.nvim_create_autocmd({ "BufRead" }, {
+ pattern = "*",
+ group = api.nvim_create_augroup("non_utf8_file", { clear = true }),
+ callback = function()
+ if vim.bo.fileencoding ~= "utf-8" then
+ vim.notify("File not in UTF-8 format!", vim.log.levels.WARN, { title = "nvim-config" })
+ end
+ end,
+})
+
+-- highlight yanked region, see `:h lua-highlight`
+local yank_group = api.nvim_create_augroup("highlight_yank", { clear = true })
+api.nvim_create_autocmd({ "TextYankPost" }, {
+ pattern = "*",
+ group = yank_group,
+ callback = function()
+ vim.hl.on_yank { higroup = "YankColor", timeout = 300 }
+ end,
+})
+
+api.nvim_create_autocmd({ "CursorMoved" }, {
+ pattern = "*",
+ group = yank_group,
+ callback = function()
+ vim.g.current_cursor_pos = vim.fn.getcurpos()
+ end,
+})
+
+api.nvim_create_autocmd("TextYankPost", {
+ pattern = "*",
+ group = yank_group,
+ ---@diagnostic disable-next-line: unused-local
+ callback = function(context)
+ if vim.v.event.operator == "y" then
+ vim.fn.setpos(".", vim.g.current_cursor_pos)
+ end
+ end,
+})
+
+-- Auto-create dir when saving a file, in case some intermediate directory does not exist
+api.nvim_create_autocmd({ "BufWritePre" }, {
+ pattern = "*",
+ group = api.nvim_create_augroup("auto_create_dir", { clear = true }),
+ callback = function(ctx)
+ local dir = fn.fnamemodify(ctx.file, ":p:h")
+ utils.may_create_dir(dir)
+ end,
+})
+
+-- Automatically reload the file if it is changed outside of Nvim, see https://unix.stackexchange.com/a/383044/221410.
+-- It seems that `checktime` does not work in command line. We need to check if we are in command
+-- line before executing this command, see also https://vi.stackexchange.com/a/20397/15292 .
+api.nvim_create_augroup("auto_read", { clear = true })
+
+api.nvim_create_autocmd({ "FileChangedShellPost" }, {
+ pattern = "*",
+ group = "auto_read",
+ callback = function()
+ vim.notify(
+ "File changed on disk. Buffer reloaded!",
+ vim.log.levels.WARN,
+ { title = "nvim-config" }
+ )
+ end,
+})
+
+api.nvim_create_autocmd({ "FocusGained", "CursorHold" }, {
+ pattern = "*",
+ group = "auto_read",
+ callback = function()
+ if fn.getcmdwintype() == "" then
+ vim.cmd("checktime")
+ end
+ end,
+})
+
+-- Resize all windows when we resize the terminal
+api.nvim_create_autocmd("VimResized", {
+ group = api.nvim_create_augroup("win_autoresize", { clear = true }),
+ desc = "autoresize windows on resizing operation",
+ command = "wincmd =",
+})
+
+local function open_nvim_tree(data)
+ -- check if buffer is a directory
+ local directory = vim.fn.isdirectory(data.file) == 1
+
+ if not directory then
+ return
+ end
+
+ -- create a new, empty buffer
+ vim.cmd.enew()
+
+ -- wipe the directory buffer
+ vim.cmd.bw(data.buf)
+
+ -- open the tree
+ require("nvim-tree.api").tree.open()
+end
+
+api.nvim_create_autocmd({ "VimEnter" }, { callback = open_nvim_tree })
+
+-- Do not use smart case in command line mode, extracted from https://vi.stackexchange.com/a/16511/15292.
+api.nvim_create_augroup("dynamic_smartcase", { clear = true })
+api.nvim_create_autocmd("CmdLineEnter", {
+ group = "dynamic_smartcase",
+ pattern = ":",
+ callback = function()
+ vim.o.smartcase = false
+ end,
+})
+
+api.nvim_create_autocmd("CmdLineLeave", {
+ group = "dynamic_smartcase",
+ pattern = ":",
+ callback = function()
+ vim.o.smartcase = true
+ end,
+})
+
+api.nvim_create_autocmd("TermOpen", {
+ group = api.nvim_create_augroup("term_start", { clear = true }),
+ pattern = "*",
+ callback = function()
+ -- Do not use number and relative number for terminal inside nvim
+ vim.wo.relativenumber = false
+ vim.wo.number = false
+
+ -- Go to insert mode by default to start typing command
+ vim.cmd("startinsert")
+ end,
+})
+
+local number_toggle_group = api.nvim_create_augroup("numbertoggle", { clear = true })
+api.nvim_create_autocmd({ "BufEnter", "FocusGained", "InsertLeave", "WinEnter" }, {
+ pattern = "*",
+ group = number_toggle_group,
+ desc = "togger line number",
+ callback = function()
+ if vim.wo.number then
+ vim.wo.relativenumber = true
+ end
+ end,
+})
+
+api.nvim_create_autocmd({ "BufLeave", "FocusLost", "InsertEnter", "WinLeave" }, {
+ group = number_toggle_group,
+ desc = "togger line number",
+ callback = function()
+ if vim.wo.number then
+ vim.wo.relativenumber = false
+ end
+ end,
+})
+
+api.nvim_create_autocmd("ColorScheme", {
+ group = api.nvim_create_augroup("custom_highlight", { clear = true }),
+ pattern = "*",
+ desc = "Define or overrride some highlight groups",
+ callback = function()
+ -- For yank highlight
+ vim.api.nvim_set_hl(
+ 0,
+ "YankColor",
+ { fg = "#34495E", bg = "#2ECC71", ctermfg = 59, ctermbg = 41 }
+ )
+
+ -- For cursor colors, see option guicursor for more info
+ vim.api.nvim_set_hl(0, "Cursor", { fg = "black", bg = "#00c918", update = true })
+ vim.api.nvim_set_hl(0, "Cursor2", { fg = "None", bg = "yellow", update = true })
+
+ -- For floating windows border highlight
+ vim.api.nvim_set_hl(0, "FloatBorder", { bg = "None", fg = "LightGreen", update = true })
+
+ -- change the background color of floating window to None, so it blenders better
+ vim.api.nvim_set_hl(0, "NormalFloat", { bg = "None", update = true })
+
+ -- this is the highlight used by nvim-cmp for cmdline completion window border
+ vim.api.nvim_set_hl(0, "Pmenu", { bg = "None", update = true })
+
+ -- highlight for matching parentheses
+ vim.api.nvim_set_hl(0, "MatchParen", { bold = true, underline = true, update = true })
+
+ vim.api.nvim_set_hl(0, "IlluminatedWordWrite", { reverse = true, update = true })
+ vim.api.nvim_set_hl(0, "IlluminatedWordRead", { reverse = true, update = true })
+ vim.api.nvim_set_hl(0, "IlluminatedWordText", { reverse = true, update = true })
+ end,
+})
+
+api.nvim_create_autocmd("BufEnter", {
+ pattern = "*",
+ group = api.nvim_create_augroup("auto_close_win", { clear = true }),
+ desc = "Quit Nvim if we have only one window, and its filetype match our pattern",
+ ---@diagnostic disable-next-line: unused-local
+ callback = function(context)
+ local quit_filetypes = { "qf", "vista", "NvimTree" }
+
+ local should_quit = true
+ local tabwins = api.nvim_tabpage_list_wins(0)
+
+ for _, win in pairs(tabwins) do
+ local buf = api.nvim_win_get_buf(win)
+ local buf_type = vim.api.nvim_get_option_value("filetype", { buf = buf })
+
+ if not vim.tbl_contains(quit_filetypes, buf_type) then
+ should_quit = false
+ end
+ end
+
+ if should_quit then
+ vim.cmd("qall")
+ end
+ end,
+})
+
+api.nvim_create_autocmd({ "VimEnter", "DirChanged" }, {
+ group = api.nvim_create_augroup("git_repo_check", { clear = true }),
+ pattern = "*",
+ desc = "check if we are inside Git repo",
+ callback = function()
+ utils.inside_git_repo()
+ end,
+})
+
+-- ref: https://vi.stackexchange.com/a/169/15292
+api.nvim_create_autocmd("BufReadPre", {
+ group = api.nvim_create_augroup("large_file", { clear = true }),
+ pattern = "*",
+ desc = "optimize for large file",
+ callback = function(ev)
+ local file_size_limit = 524288 -- 0.5MB
+ local f = ev.file
+
+ if fn.getfsize(f) > file_size_limit or fn.getfsize(f) == -2 then
+ vim.o.eventignore = "all"
+
+ -- show ruler
+ vim.o.ruler = true
+
+ -- turning off relative number helps a lot
+ vim.wo.relativenumber = false
+ vim.wo.number = false
+
+ vim.bo.swapfile = false
+ vim.bo.bufhidden = "unload"
+ vim.bo.undolevels = -1
+ end
+ end,
+})
+
+-- check if current file is formatted
+local ft_to_command = {
+ python = { "black", "--check" },
+ lua = { "stylua", "--check" },
+}
+
+api.nvim_create_autocmd("BufWritePost", {
+ group = api.nvim_create_augroup("format check", { clear = true }),
+ pattern = { "*" },
+ desc = "Check if file needs reformat",
+ callback = function(ev)
+ local fpath = ev.file
+ local ft = vim.api.nvim_get_option_value("filetype", { buf = ev.buf })
+
+ local cmd_partial = ft_to_command[ft]
+ local cmd = nil
+ if cmd_partial ~= nil then
+ cmd = vim.deepcopy(cmd_partial)
+ table.insert(cmd, fpath)
+ else
+ return
+ end
+
+ local exe_path = cmd[1]
+ if not utils.executable(exe_path) then
+ vim.print(string.format("%s not found!", exe_path))
+ return
+ end
+
+ local result = vim.system(cmd, { text = true }):wait()
+ if result.code ~= 0 then
+ vim.notify("This file is not formatted!")
+ end
+ end,
+})
+
+api.nvim_create_autocmd({ "InsertLeave", "TextChanged" }, {
+ group = api.nvim_create_augroup("auto_save", { clear = true }),
+ pattern = { "*" },
+ desc = "Auto save current file",
+ callback = function(ev)
+ local is_readonly = vim.api.nvim_get_option_value("readonly", { buf = ev.buf })
+ local is_modifiable = vim.api.nvim_get_option_value("modifiable", { buf = ev.buf })
+
+ if not is_readonly and is_modifiable then
+ vim.cmd([[silent! update]])
+ end
+ end,
+})
diff --git a/nvim/lua/diagnostic-conf.lua b/nvim/lua/diagnostic-conf.lua
new file mode 100644
index 0000000..7763a61
--- /dev/null
+++ b/nvim/lua/diagnostic-conf.lua
@@ -0,0 +1,69 @@
+local diagnostic = vim.diagnostic
+local api = vim.api
+
+-- global config for diagnostic
+diagnostic.config {
+ underline = false,
+ virtual_text = false,
+ virtual_lines = false,
+ signs = {
+ text = {
+ [diagnostic.severity.ERROR] = "🆇",
+ [diagnostic.severity.WARN] = "⚠️",
+ [diagnostic.severity.INFO] = "ℹ️",
+ [diagnostic.severity.HINT] = "",
+ },
+ },
+ severity_sort = true,
+ float = {
+ source = true,
+ header = "Diagnostics:",
+ prefix = " ",
+ border = "single",
+ max_height = 10,
+ max_width = 130,
+ close_events = { "CursorMoved", "BufLeave", "WinLeave", "InsertEnter" },
+ },
+}
+
+-- set quickfix list from diagnostics in a certain buffer, not the whole workspace
+local set_qflist = function(buf_num, severity)
+ local diagnostics = nil
+ diagnostics = diagnostic.get(buf_num, { severity = severity })
+
+ local qf_items = diagnostic.toqflist(diagnostics)
+ vim.fn.setqflist({}, " ", { title = "Diagnostics", items = qf_items })
+
+ -- open quickfix by default
+ vim.cmd([[copen]])
+end
+
+-- this puts diagnostics from opened files to quickfix
+vim.keymap.set("n", "qw", diagnostic.setqflist, { desc = "put window diagnostics to qf" })
+
+-- this puts diagnostics from current buffer to quickfix
+vim.keymap.set("n", "qb", function()
+ set_qflist(0)
+end, { desc = "put buffer diagnostics to qf" })
+
+-- automatically show diagnostic in float win for current line
+api.nvim_create_autocmd("CursorHold", {
+ pattern = "*",
+ callback = function()
+ if #vim.diagnostic.get(0) == 0 then
+ return
+ end
+
+ if not vim.b.diagnostics_pos then
+ vim.b.diagnostics_pos = { nil, nil }
+ end
+
+ local cursor_pos = api.nvim_win_get_cursor(0)
+
+ if not vim.deep_equal(cursor_pos, vim.b.diagnostics_pos) then
+ diagnostic.open_float {}
+ end
+
+ vim.b.diagnostics_pos = cursor_pos
+ end,
+})
diff --git a/nvim/lua/dotenv.lua b/nvim/lua/dotenv.lua
new file mode 100644
index 0000000..a065dda
--- /dev/null
+++ b/nvim/lua/dotenv.lua
@@ -0,0 +1,29 @@
+-- code from https://github.com/mcjkula/lua-dotenv/blob/main/env.lua
+local M = {}
+local env_vars = {}
+
+function M.get(key, default)
+ local value = env_vars[key] or os.getenv(key)
+ return value or default
+end
+
+function M.load_dotenv(file_path)
+ file_path = file_path or os.getenv("HOME") .. "/.config/.env"
+ local file, _ = io.open(file_path, "r")
+ if not file then
+ return
+ end
+
+ local content = file:read("*all")
+ file:close()
+
+ for line in content:gmatch("[^\r\n]+") do
+ local key, value = line:match("^([%w_]+)%s*=%s*(.+)$")
+ if key and value then
+ value = value:gsub("^[\"'](.-)[\"']$", "%1")
+ env_vars[key] = value
+ end
+ end
+end
+
+return M
diff --git a/nvim/lua/globals.lua b/nvim/lua/globals.lua
new file mode 100644
index 0000000..83e64a9
--- /dev/null
+++ b/nvim/lua/globals.lua
@@ -0,0 +1,59 @@
+local utils = require("utils")
+
+------------------------------------------------------------------------
+-- custom variables --
+------------------------------------------------------------------------
+vim.g.is_win = (utils.has("win32") or utils.has("win64")) and true or false
+vim.g.is_linux = (utils.has("unix") and (not utils.has("macunix"))) and true or false
+vim.g.is_mac = utils.has("macunix") and true or false
+
+vim.g.logging_level = vim.log.levels.INFO
+
+------------------------------------------------------------------------
+-- builtin variables --
+------------------------------------------------------------------------
+vim.g.loaded_perl_provider = 0 -- Disable perl provider
+vim.g.loaded_ruby_provider = 0 -- Disable ruby provider
+vim.g.loaded_node_provider = 0 -- Disable node provider
+vim.g.did_install_default_menus = 1 -- do not load menu
+
+-- Custom mapping (see `:h mapleader` for more info)
+vim.g.mapleader = ","
+
+-- Enable highlighting for lua HERE doc inside vim script
+vim.g.vimsyn_embed = "l"
+
+-- Use English as main language
+vim.cmd([[language fr_FR.UTF-8]])
+
+-- Disable loading certain plugins
+
+-- Whether to load netrw by default, see https://github.com/bling/dotvim/issues/4
+vim.g.loaded_netrw = 1
+vim.g.loaded_netrwPlugin = 1
+vim.g.netrw_liststyle = 3
+if vim.g.is_win then
+ vim.g.netrw_http_cmd = "curl --ssl-no-revoke -Lo"
+end
+
+-- Do not load tohtml.vim
+vim.g.loaded_2html_plugin = 1
+
+-- Do not load zipPlugin.vim, gzip.vim and tarPlugin.vim (all these plugins are
+-- related to checking files inside compressed files)
+vim.g.loaded_zipPlugin = 1
+vim.g.loaded_gzip = 1
+vim.g.loaded_tarPlugin = 1
+
+-- Do not load the tutor plugin
+vim.g.loaded_tutor_mode_plugin = 1
+
+-- Do not use builtin matchit.vim and matchparen.vim since we use vim-matchup
+vim.g.loaded_matchit = 1
+vim.g.loaded_matchparen = 1
+
+-- Disable sql omni completion, it is broken.
+vim.g.loaded_sql_completion = 1
+
+-- control how to show health check window
+vim.g.health = { style = nil }
diff --git a/nvim/lua/lsp_conf.lua b/nvim/lua/lsp_conf.lua
new file mode 100644
index 0000000..27817f7
--- /dev/null
+++ b/nvim/lua/lsp_conf.lua
@@ -0,0 +1,210 @@
+local utils = require("utils")
+
+vim.api.nvim_create_autocmd("LspAttach", {
+ group = vim.api.nvim_create_augroup("lsp_buf_conf", { clear = true }),
+ callback = function(event_context)
+ local client = vim.lsp.get_client_by_id(event_context.data.client_id)
+ -- vim.print(client.name, client.server_capabilities)
+
+ if not client then
+ return
+ end
+
+ local bufnr = event_context.buf
+
+ -- Mappings.
+ local map = function(mode, l, r, opts)
+ opts = opts or {}
+ opts.silent = true
+ opts.buffer = bufnr
+ vim.keymap.set(mode, l, r, opts)
+ end
+
+ map("n", "gd", function()
+ vim.lsp.buf.definition {
+ on_list = function(options)
+ -- custom logic to avoid showing multiple definition when you use this style of code:
+ -- `local M.my_fn_name = function() ... end`.
+ -- See also post here: https://www.reddit.com/r/neovim/comments/19cvgtp/any_way_to_remove_redundant_definition_in_lua_file/
+
+ -- vim.print(options.items)
+ local unique_defs = {}
+ local def_loc_hash = {}
+
+ -- each item in options.items contain the location info for a definition provided by LSP server
+ for _, def_location in pairs(options.items) do
+ -- use filename and line number to uniquelly indentify a definition,
+ -- we do not expect/want multiple definition in single line!
+ local hash_key = def_location.filename .. def_location.lnum
+
+ if not def_loc_hash[hash_key] then
+ def_loc_hash[hash_key] = true
+ table.insert(unique_defs, def_location)
+ end
+ end
+
+ options.items = unique_defs
+
+ -- set the location list
+ ---@diagnostic disable-next-line: param-type-mismatch
+ vim.fn.setloclist(0, {}, " ", options)
+
+ -- open the location list when we have more than 1 definitions found,
+ -- otherwise, jump directly to the definition
+ if #options.items > 1 then
+ vim.cmd.lopen()
+ else
+ vim.cmd([[silent! lfirst]])
+ end
+ end,
+ }
+ end, { desc = "go to definition" })
+ map("n", "", vim.lsp.buf.definition)
+ map("n", "K", function()
+ vim.lsp.buf.hover {
+ border = "single",
+ max_height = 20,
+ max_width = 130,
+ close_events = { "CursorMoved", "BufLeave", "WinLeave", "LSPDetach" },
+ }
+ end)
+ map("n", "", vim.lsp.buf.signature_help)
+ map("n", "rn", vim.lsp.buf.rename, { desc = "varialbe rename" })
+ map("n", "ca", vim.lsp.buf.code_action, { desc = "LSP code action" })
+ map("n", "wa", vim.lsp.buf.add_workspace_folder, { desc = "add workspace folder" })
+ map("n", "wr", vim.lsp.buf.remove_workspace_folder, { desc = "remove workspace folder" })
+ map("n", "wl", function()
+ vim.print(vim.lsp.buf.list_workspace_folders())
+ end, { desc = "list workspace folder" })
+
+ -- Set some key bindings conditional on server capabilities
+ -- Disable ruff hover feature in favor of Pyright
+ if client.name == "ruff" then
+ client.server_capabilities.hoverProvider = false
+ end
+ end,
+ nested = true,
+ desc = "Configure buffer keymap and behavior based on LSP",
+})
+
+-- Enable lsp servers when they are available
+local capabilities = require("lsp_utils").get_default_capabilities()
+
+-- `*` will set default config for all lsp
+vim.lsp.config("*", {
+ capabilities = capabilities,
+ flags = {
+ debounce_text_changes = 500,
+ },
+})
+
+vim.lsp.config("basedpyright", {
+ capabilities = capabilities,
+ offset_encoding = "utf-16",
+ settings = {
+ python = {
+ analysis = {
+ typeCheckingMode = "strict",
+ },
+ },
+ },
+})
+
+vim.lsp.config("ruff", {
+ capabilities = capabilities,
+ offset_encoding = "utf-8",
+})
+
+-- A mapping from lsp server name to the executable name
+local enabled_lsp_servers = {
+ basedpyright = "basedpyright-langserver",
+ ruff = "ruff",
+ lua_ls = "lua-language-server",
+ -- clangd = "clangd",
+ vimls = "vim-language-server",
+ bashls = "bash-language-server",
+ yamlls = "yaml-language-server",
+ gopls = "gopls",
+ -- the server can be install via homebrew: brew install golangci-lint-langserver
+ -- golangci-lint also needs to be installed: https://github.com/golangci/golangci-lint
+ golangci_lint_ls = "golangci-lint-langserver",
+
+ -- to install codebook, run `brew install codebook-lsp`
+ -- codebook = "codebook-lsp"
+}
+
+for server_name, lsp_executable in pairs(enabled_lsp_servers) do
+ if utils.executable(lsp_executable) then
+ vim.lsp.enable(server_name)
+ else
+ local msg = string.format(
+ "Executable '%s' for server '%s' not found! Server will not be enabled",
+ lsp_executable,
+ server_name
+ )
+ vim.notify(msg, vim.log.levels.WARN, { title = "Nvim-config" })
+ end
+end
+
+-- LSP related command
+
+vim.api.nvim_create_user_command("LspInfo", "checkhealth vim.lsp", {
+ desc = "Show LSP Info",
+})
+
+vim.api.nvim_create_user_command("LspLog", function(_)
+ local log_path = vim.lsp.log.get_filename()
+
+ vim.cmd(string.format("edit %s", log_path))
+end, {
+ desc = "Show LSP log",
+})
+
+vim.api.nvim_create_user_command("LspRestart", "lsp restart", {
+ desc = "Restart LSP",
+})
+
+--- show LSP progress (works on Ghostty)
+vim.api.nvim_create_autocmd("LspProgress", {
+ callback = function(ev)
+ local value = ev.data.params.value
+ vim.api.nvim_echo({ { value.message or "done" } }, false, {
+ id = "lsp." .. ev.data.client_id,
+ kind = "progress",
+ source = "vim.lsp",
+ title = value.title,
+ status = value.kind ~= "end" and "running" or "success",
+ percent = value.percentage,
+ })
+ end,
+})
+
+-- this controls the LSP inlayHints behavior
+vim.g.lsp_inlay_hint_enabled = false
+
+local update_inlayhint = function(enable)
+ -- Some LSP server supports inlay hint, but disable this feature by default, so you may need to
+ -- enable inlay hint in the LSP server config.
+ vim.lsp.inlay_hint.enable(enable)
+end
+
+vim.api.nvim_create_user_command("LspInlayHints", function(context)
+ -- vim.print("context", context)
+ if context["args"] == "enable" then
+ vim.g.lsp_inlay_hint_enabled = true
+ end
+
+ if context["args"] == "disable" then
+ vim.g.lsp_inlay_hint_enabled = false
+ end
+
+ update_inlayhint(vim.g.lsp_inlay_hint_enabled)
+end, {
+ bang = false,
+ nargs = 1,
+ force = true,
+ desc = "Toggle LSP inlayHints",
+ complete = function()
+ return { "enable", "disable" }
+ end,
+})
diff --git a/nvim/lua/lsp_utils.lua b/nvim/lua/lsp_utils.lua
new file mode 100644
index 0000000..df3b4db
--- /dev/null
+++ b/nvim/lua/lsp_utils.lua
@@ -0,0 +1,39 @@
+local M = {}
+
+M.get_default_capabilities = function()
+ local capabilities = vim.lsp.protocol.make_client_capabilities()
+
+ -- nvim-cmp integration (IMPORTANT)
+ local ok, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp")
+ if ok then
+ capabilities = cmp_nvim_lsp.default_capabilities(capabilities)
+ end
+
+ -- required by nvim-ufo (tu l'as déjà, on garde)
+ capabilities.textDocument.foldingRange = {
+ dynamicRegistration = false,
+ lineFoldingOnly = true,
+ }
+
+ -- améliore Pyright / Python completion
+ capabilities.textDocument.completion.completionItem = {
+ snippetSupport = true,
+ preselectSupport = true,
+ insertReplaceSupport = true,
+ labelDetailsSupport = true,
+ deprecatedSupport = true,
+ commitCharactersSupport = true,
+ tagSupport = { valueSet = { 1 } },
+ resolveSupport = {
+ properties = {
+ "documentation",
+ "detail",
+ "additionalTextEdits",
+ },
+ },
+ }
+
+ return capabilities
+end
+
+return M
diff --git a/nvim/lua/mappings.lua b/nvim/lua/mappings.lua
new file mode 100644
index 0000000..bd03501
--- /dev/null
+++ b/nvim/lua/mappings.lua
@@ -0,0 +1,276 @@
+local keymap = vim.keymap
+local uv = vim.uv
+
+-- Save key strokes (now we do not need to press shift to enter command mode).
+keymap.set({ "n", "x" }, ";", ":")
+
+-- Turn the word under cursor to upper case
+keymap.set("i", "", "viwUea")
+
+-- Turn the current word into title case
+keymap.set("i", "", "b~lea")
+
+-- Paste non-linewise text above or below current line, see https://stackoverflow.com/a/1346777/6064933
+keymap.set("n", "p", "m`op``", { desc = "paste below current line" })
+keymap.set("n", "P", "m`Op``", { desc = "paste above current line" })
+
+-- Shortcut for faster save and quit
+keymap.set("n", "w", "update", { silent = true, desc = "save buffer" })
+
+-- Saves the file if modified and quit
+keymap.set("n", "q", "x", { silent = true, desc = "quit current window" })
+
+-- Quit all opened buffers
+keymap.set("n", "Q", "qa!", { silent = true, desc = "quit nvim" })
+
+-- Close location list or quickfix list if they are present, see https://superuser.com/q/355325/736190
+keymap.set("n", [[\x]], "windo lclose cclose ", {
+ silent = true,
+ desc = "close qf and location list",
+})
+
+-- Delete a buffer, without closing the window, see https://stackoverflow.com/q/4465095/6064933
+keymap.set("n", [[\db]], "bprevious bdelete #", {
+ silent = true,
+ desc = "Delete current buffer",
+})
+
+keymap.set("n", [[\dB]], function()
+ local buf_ids = vim.api.nvim_list_bufs()
+ local cur_buf = vim.api.nvim_win_get_buf(0)
+
+ for _, buf_id in pairs(buf_ids) do
+ -- do not Delete unlisted buffers, which may lead to unexpected errors
+ if vim.api.nvim_get_option_value("buflisted", { buf = buf_id }) and buf_id ~= cur_buf then
+ vim.api.nvim_buf_delete(buf_id, { force = true })
+ end
+ end
+end, {
+ desc = "Delete other buffers",
+})
+
+keymap.set("n", [[\dt]], "tabclose", {
+ silent = true,
+ desc = "Delete current tab",
+})
+
+keymap.set("n", [[\dT]], "tabonly", {
+ silent = true,
+ desc = "Delete other tabs",
+})
+
+-- Move the cursor based on physical lines, not the actual lines.
+keymap.set("n", "j", "v:count == 0 ? 'gj' : 'j'", { expr = true })
+keymap.set("n", "k", "v:count == 0 ? 'gk' : 'k'", { expr = true })
+keymap.set("n", "^", "g^")
+keymap.set("n", "0", "g0")
+
+-- Do not include white space characters when using $ in visual mode,
+-- see https://vi.stackexchange.com/q/12607/15292
+keymap.set("x", "$", "g_")
+
+-- Go to start or end of line easier
+keymap.set({ "n", "x" }, "H", "^")
+keymap.set({ "n", "x" }, "L", "g_")
+
+-- Continuous visual shifting (does not exit Visual mode), `gv` means
+-- to reselect previous visual area, see https://superuser.com/q/310417/736190
+keymap.set("x", "<", "", ">gv")
+
+-- Restart nvim
+keymap.set("n", "sv", function()
+ vim.print("Use ZR to restart nvim instead!")
+end)
+
+keymap.set("n", "ZR", function()
+ local current_buf_path = vim.fn.expand("%")
+ local restart_cmd = string.format("restart edit %s", current_buf_path)
+ vim.cmd(restart_cmd)
+end, {
+ silent = true,
+ desc = "Restart nvim",
+})
+
+-- Reselect the text that has just been pasted, see also https://stackoverflow.com/a/4317090/6064933.
+keymap.set("n", "v", "printf('`[%s`]', getregtype()[0])", {
+ expr = true,
+ desc = "reselect last pasted area",
+})
+
+-- Always use very magic mode for searching
+-- keymap.set("n", "/", [[/\v]])
+
+-- Search in selected region
+-- xnoremap / :call feedkeys('/\%>'.(line("'<")-1).'l\%<'.(line("'>")+1)."l")
+
+-- Change current working directory locally and print cwd after that,
+-- see https://vim.fandom.com/wiki/Set_working_directory_to_the_current_file
+keymap.set("n", "cd", "