" One vimrc for Mac and Linux. Source of truth: /Users/xs/dev_env/core/vimrc " Mac: cp core/vimrc ~/.vimrc Linux: sudo bash core/vim.sh (copies it to /etc/vimrc or /etc/vim/vimrc) set nocompatible " /etc/vimrc is read before vim leaves vi mode; without this \ continuations and break " gt: go to next tab (right, wraps) " gT: go to previous tab (left, wraps) " 2gt: go to tab number 2 (counted from 1 at the left) " :q: close the current window, and the tab too if it was the last window in it " ZZ: save if changed, then close the window (and the tab if it was the last window in it) " ZQ: close the window WITHOUT saving (like :q!), and the tab too if it was the last window in it " g: jump to the last-accessed tab (press g, release, then Tab); toggles between two tabs " Ctrl-w w: cycle windows in this tab, e.g. into and out of the K popup (Ctrl-w p toggles back) " 7G12|: go to line 7, column 12 (normal-mode keys, no colon; 3w moves 3 words) " :set foldcolumn=0 : hide the 12-wide fold gutter (:set foldcolumn=12 brings it back) " Reset an edited cargo dependency: delete ~/.cargo/registry/src//-/ then run cargo fetch " " Setup (LSP servers below are optional — each is guarded by executable()): " plugins: curl -fLo ~/.vim/autoload/plug.vim --create-dirs \ " https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim " then launch vim and run :PlugInstall " rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y " rustup component add rust-analyzer " python: curl -LsSf https://astral.sh/uv/install.sh | sh " uv tool install ruff && uv tool install pyright " " Stop vim from auto-loading defaults.vim, which loads AFTER this file and " would otherwise silently override settings here. Everything is in this file. let g:skip_defaults_vim = 1 " ===== Shortcuts: one line each, = \ (comment follows the |) ===== inoremap | " insert Ctrl-a: jump to line start inoremap | " insert Ctrl-e: jump to line end inoremap pumvisible() ? "\" : asyncomplete#force_refresh()| " insert Ctrl-l: toggle completion menu inoremap pumvisible() ? "\" : "\"| " in menu Tab: next item inoremap pumvisible() ? "\" : "\"| " in menu Shift-Tab: previous item inoremap pumvisible() ? "\" : "\"| " in menu Enter: accept item nnoremap 0| " Ctrl-a: jump to line start nnoremap $| " Ctrl-e: jump to line end nnoremap :nohlsearch| " Ctrl-l: clear search highlight nnoremap :Files| " Ctrl-p: fuzzy-find a file nnoremap rg :Rg| " \rg: search text in project nnoremap b :Buffers| " \b: list open buffers nnoremap n :NERDTreeToggle| " \n: toggle file tree nnoremap gd :call GotoDef()| " gd: go to definition (Ctrl-o back) nnoremap gi :LspImplementation| " gi: go to implementation nnoremap gy :tab LspTypeDefinition| " gy: go to type definition in a new tab (e -> struct DirEntry) nnoremap k :call ToggleHover()| " \k: toggle hover docs nnoremap K :call ToggleHover()| " K: toggle hover docs (same as \k) nnoremap D :LspDocumentation| " \D: show documentation nnoremap e :call ToggleDiagnostic()| " \e: show error on this line nnoremap l :lopen| " \l: list all errors nnoremap a :LspCodeAction| " \a: code actions popup nnoremap rn :LspRename| " \rn: rename symbol nnoremap f :LspDocumentFormat| " \f: format file nnoremap em :call ExpandMacro()| " \em: expand macro under cursor " ===== Shared clipboard: RAM file /dev/shm/clip/buf on g:clip_host (same as tmux.conf) ===== let g:clip_host = 'root@p.noml.ch' " One persistent ssh link (opens on first use, one key touch). let g:clip = ['ssh', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=yes', '-o', 'ControlPath=~/.ssh/cm-%C', '-o', 'ServerAliveInterval=30', '-o', 'ConnectTimeout=3', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel=ERROR', g:clip_host] " push (yank): every yank or delete sends @" in a background job augroup ssh_clipboard au! au TextYankPost * let g:clip_job = job_start(g:clip + ['umask 077; mkdir -p /dev/shm/clip && cat >/dev/shm/clip/new && mv /dev/shm/clip/new /dev/shm/clip/buf'], {'in_io': 'pipe', 'out_io': 'null', 'err_io': 'null'}) | call ch_sendraw(g:clip_job, @") | call ch_close_in(g:clip_job) augroup END " pull (paste): every p P gp gP ]p [p (normal) and p P (visual) first fetches the buffer into @". " Waits up to 3 s for a pending push, then up to 3 s for the fetch (perl alarm kills ssh); " no buffer on g:clip_host yet (exit 3, e.g. after its reboot) = normal local paste; " any other failure prints a message and skips the paste. Counts and "x register prefixes still work. function! ClipPull() abort let l:i = 0 while exists('g:clip_job') && job_status(g:clip_job) ==# 'run' && l:i < 30 sleep 100m let l:i += 1 endwhile let l:out = system("perl -e 'alarm 3; exec @ARGV' " . join(g:clip) . " '[ -f /dev/shm/clip/buf ] || exit 3; cat /dev/shm/clip/buf'") if v:shell_error == 3 return 1 elseif v:shell_error echohl ErrorMsg | echo 'clip: pull from ' . g:clip_host . ' failed or took >3s, paste skipped' | echohl None return 0 endif call setreg('"', l:out) return 1 endfunction for s:k in ['p', 'P', 'gp', 'gP', ']p', '[p'] execute 'nnoremap ' . s:k . " ClipPull() ? '" . s:k . "' : ''" endfor for s:k in ['p', 'P'] execute 'xnoremap ' . s:k . " ClipPull() ? '" . s:k . "' : ''" endfor " ===== end shared clipboard ===== filetype plugin indent on set number set wrap set breakindent set breakindentopt=shift:1 set expandtab set shiftwidth=4 set softtabstop=4 set tabstop=4 set noautoindent set nosmartindent set hlsearch set incsearch set ignorecase set smartcase set scrolloff=8 set signcolumn=no set foldcolumn=12 set laststatus=2 set statusline=%F set wildmenu set splitright set splitbelow set hidden " gd/LSP jumps reuse the window even with unsaved changes set noswapfile set regexpengine=2 set completeopt=menu,menuone,noinsert,noselect set mouse=a if has('mouse_sgr') set ttymouse=sgr endif set undofile set undodir=~/.vim/undo if !isdirectory($HOME . '/.vim/undo') call mkdir($HOME . '/.vim/undo', 'p') endif if has('termguicolors') if !empty($TMUX) let &t_8f = "\[38;2;%lu;%lu;%lum" let &t_8b = "\[48;2;%lu;%lu;%lum" endif set termguicolors endif colorscheme zellner autocmd InsertLeave * silent! update " Files rustup and cargo manage (toolchains, downloaded crates) open locked; :setl ma noro to override function! s:LockIfManaged() abort if !exists('s:managed') let l:cargo = empty($CARGO_HOME) ? expand('~/.cargo') : $CARGO_HOME let s:managed = [l:cargo . '/registry', l:cargo . '/git'] if executable('rustup') call add(s:managed, trim(system('rustup show home'))) endif endif for l:dir in s:managed if stridx(expand('%:p'), l:dir . '/') == 0 setlocal readonly nomodifiable return endif endfor endfunction augroup managed_readonly autocmd! autocmd BufReadPost * call s:LockIfManaged() augroup END " vim-plug is optional: skip the plugin block until plug.vim is installed (see Setup above) if filereadable(expand('~/.vim/autoload/plug.vim')) call plug#begin('~/.vim/plugged') Plug 'prabirshrestha/async.vim' Plug 'prabirshrestha/vim-lsp' Plug 'prabirshrestha/asyncomplete.vim' Plug 'prabirshrestha/asyncomplete-lsp.vim' Plug 'rust-lang/rust.vim' Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } Plug 'junegunn/fzf.vim' Plug 'tpope/vim-commentary' Plug 'tpope/vim-surround' Plug 'tpope/vim-fugitive' Plug 'airblade/vim-gitgutter' Plug 'preservim/nerdtree' Plug 'andymass/vim-matchup' call plug#end() endif let g:gitgutter_sign_column_always = 0 " rust-analyzer: a file that is a target of its nearest Cargo.toml (src/ examples/ tests/ " benches/ build.rs, or its path is in the manifest) uses that crate. Any other .rs file is " loaded standalone via detachedFiles, with checkOnSave off (cargo check needs a Cargo.toml). function! RustCrate() abort let l:p = expand('%:p') let l:d = lsp#utils#find_nearest_parent_file_directory(l:p, 'Cargo.toml') if empty(l:d) | return '' | endif let l:d = substitute(fnamemodify(l:d, ':p'), '/$', '', '') let l:rel = stridx(l:p, l:d . '/') == 0 ? l:p[len(l:d) + 1:] : '' if l:rel =~# '^\(src\|examples\|tests\|benches\)/\|^build\.rs$' \ || (!empty(l:rel) && stridx(join(readfile(l:d . '/Cargo.toml')), '"' . l:rel . '"') >= 0) return l:d endif return '' endfunction function! s:RustAddDetached() abort let l:p = expand('%:p') if empty(l:p) || !isdirectory(fnamemodify(l:p, ':h')) || index(g:ra_init.detachedFiles, l:p) >= 0 | return 0 | endif call add(g:ra_init.detachedFiles, l:p) return 1 endfunction " Called once at server start (root_uri is read just before the init options). function! RustRoot() abort let l:c = RustCrate() let g:ra_init.checkOnSave = empty(l:c) ? v:false : v:true if !empty(l:c) | return l:c | endif call s:RustAddDetached() return expand('%:p:h') endfunction let g:ra_init = { \ 'completion': {'autoimport': {'enable': v:true}, 'postfix': {'enable': v:true}}, \ 'checkOnSave': v:true, 'detachedFiles': []} augroup rust_detached au! " standalone file opened while the server already runs: add it live (check setting untouched) au BufEnter *.rs if exists('g:lsp_loaded') && lsp#get_server_status('rust-analyzer') ==# 'running' \ && empty(RustCrate()) && s:RustAddDetached() \ | call lsp#update_workspace_config('rust-analyzer', {'rust-analyzer': g:ra_init}) | endif augroup END " rust-analyzer via the rustup proxy shim (portable across toolchains/machines) if executable(expand('~/.cargo/bin/rust-analyzer')) au User lsp_setup call lsp#register_server({ \ 'name': 'rust-analyzer', 'allowlist': ['rust'], \ 'cmd': {server_info -> [expand('~/.cargo/bin/rust-analyzer')]}, \ 'root_uri': {server_info -> lsp#utils#path_to_uri(RustRoot())}, \ 'initialization_options': g:ra_init, \ 'workspace_config': {'rust-analyzer': g:ra_init}}) endif if executable(expand('~/.local/bin/ruff')) au User lsp_setup call lsp#register_server({ \ 'name': 'ruff', \ 'cmd': {server_info -> [expand('~/.local/bin/ruff'), 'server']}, \ 'allowlist': ['python'], \ }) endif let s:uv_bin = has('macunix') && executable('/opt/homebrew/bin/uv') \ ? '/opt/homebrew/bin/uv' \ : expand('~/.local/bin/uv') if executable(expand('~/.local/bin/ty')) && executable(s:uv_bin) " Launched through `uv run` (not the raw path) so it picks up whichever " project's .venv is active via cwd/VIRTUAL_ENV. au User lsp_setup call lsp#register_server({ \ 'name': 'ty', \ 'cmd': {server_info -> [s:uv_bin, 'run', 'ty', 'server']}, \ 'allowlist': ['python'], \ }) endif " tofu-ls (OpenTofu/Terraform .tf) for go-to-definition if executable(expand('~/.local/bin/tofu-ls')) au User lsp_setup call lsp#register_server({ \ 'name': 'tofu-ls', \ 'cmd': {server_info -> [expand('~/.local/bin/tofu-ls'), 'serve']}, \ 'allowlist': ['terraform', 'terraform-vars'], \ }) endif " gopls (Go) for go-to-definition if executable(expand('~/go/bin/gopls')) au User lsp_setup call lsp#register_server({ \ 'name': 'gopls', \ 'cmd': {server_info -> [expand('~/go/bin/gopls')]}, \ 'allowlist': ['go'], \ 'root_uri': {server_info -> lsp#utils#path_to_uri(lsp#utils#find_nearest_parent_file_directory(lsp#utils#get_buffer_path(), 'go.mod'))}, \ }) endif " Force per-keystroke LSP sync for Python (vim-lsp only syncs on " InsertLeave/TextChanged by default, not while actively typing). autocmd FileType python autocmd TextChangedI doautocmd TextChanged let g:lsp_diagnostics_echo_cursor = 1 let g:lsp_diagnostics_float_cursor = 0 let g:lsp_diagnostics_virtual_text_enabled = 0 let g:lsp_diagnostics_signs_enabled = 0 let g:lsp_document_highlight_enabled = 0 let g:lsp_preview_float = 0 let g:lsp_hover_ui = 'preview' let g:lsp_code_action_ui = 'float' " \a = one popup menu, not 3 windows let g:lsp_signature_help_enabled = 0 function! s:set_preview_height() abort let &previewheight = max([10, &lines * 40 / 100]) endfunction call s:set_preview_height() autocmd VimResized * call s:set_preview_height() " Toggle hover docs in the preview window. function! ToggleHover() abort for l:nr in range(1, winnr('$')) if getwinvar(l:nr, '&previewwindow') pclose return endif endfor LspHover endfunction " Show the current line's diagnostic in a preview window. function! ToggleDiagnostic() abort for l:nr in range(1, winnr('$')) if getwinvar(l:nr, '&previewwindow') pclose return endif endfor let l:diag = lsp#internal#diagnostics#under_cursor#get_diagnostic() if empty(l:diag) echo 'No diagnostic on this line' return endif let l:cur = win_getid() botright new execute 'resize ' . &previewheight setlocal buftype=nofile bufhidden=wipe noswapfile previewwindow call setline(1, split(l:diag['message'], "\n")) setlocal nomodifiable call win_gotoid(l:cur) endfunction " rust-analyzer macro expansion (LSP extension, no built-in command). function! ExpandMacro() abort let l:params = { \ 'textDocument': { 'uri': lsp#utils#get_buffer_uri() }, \ 'position': lsp#get_position(), \ } call lsp#send_request('rust-analyzer', { \ 'method': 'rust-analyzer/expandMacro', \ 'params': l:params, \ 'on_notification': function('s:expand_macro_handler'), \ }) endfunction function! s:expand_macro_handler(data) abort if !has_key(a:data, 'response') || empty(a:data['response']) echo 'ExpandMacro: no response / not a macro' return endif let l:result = get(a:data['response'], 'result', v:null) if l:result is v:null echo 'ExpandMacro: nothing to expand at cursor' return endif botright new setlocal buftype=nofile bufhidden=wipe noswapfile filetype=rust call setline(1, split('// expansion of: ' . l:result['name'], "\n")) call append(line('$'), split(l:result['expansion'], "\n")) endfunction " gd: say when rust-analyzer is still starting/indexing instead of a false "No definition found" let g:lsp_work_done_progress_enabled = 1 function! s:GotoDef() abort if &filetype ==# 'rust' let l:st = lsp#get_server_status('rust-analyzer') if l:st !=# 'running' echo 'rust-analyzer ' . l:st . ', try again in a few seconds' return endif let l:busy = filter(copy(lsp#get_progress()), \ {_, p -> p['server'] ==# 'rust-analyzer' && get(p, 'title', '') !~? 'check'}) if !empty(l:busy) echo 'rust-analyzer still indexing: ' . get(l:busy[0], 'title', '') . ' ' . get(l:busy[0], 'message', '') return endif endif tab LspDefinition endfunction " Numbered tab labels (1:main.rs 2:fs.rs): the number is what Ngt jumps to. function! TabLine() abort let l:s = '' for l:i in range(1, tabpagenr('$')) let l:buf = tabpagebuflist(l:i)[tabpagewinnr(l:i) - 1] let l:name = fnamemodify(bufname(l:buf), ':t') if l:name ==# '' let l:name = '[No Name]' endif let l:hl = l:i == tabpagenr() ? '%#TabLineSel#' : '%#TabLine#' let l:s .= l:hl . ' ' . l:i . ':' . l:name . ' ' endfor return l:s . '%#TabLineFill#' endfunction set tabline=%!TabLine()