diff --git a/docs/adrs/005.tabs-vim.ecosystem-support.md b/docs/adrs/005.tabs-vim.ecosystem-support.md new file mode 100644 index 0000000..2cf5d17 --- /dev/null +++ b/docs/adrs/005.tabs-vim.ecosystem-support.md @@ -0,0 +1,54 @@ +# 005. Ecosystem Support — FlogInTab and Ecosystem Buffer Close + +**SPEC:** ecosystem-support +**Status:** Accepted +**Last Updated:** 2026-04-05 + +--- + +## Decision + +Add two ecosystem integration features to `plugin/tabs.vim`: + +1. **`TabsVim_FlogInTab()`** — opens vim-flog's full-repo git log in a new tab via `Flogsplit -open-cmd=tabedit -all`. Guards with `exists(':Flogsplit')` and emits a warning if vim-flog is not loaded. Exposed as a `TabsVim_*` function (rather than a vimrc one-liner) for the same discoverability reason as `TabsVim_FzfOpenInTab()` — users exploring the API should not need to inspect vim-flog's command syntax. + +2. **`g:tabs_vim_tabclose_types` autocmd** — reads a user-supplied list of tokens at plugin load time and installs buffer-local `q` → `:tabclose` mappings for matching filetypes or conditions. Default is empty (no OOTB behavior). Supported tokens: `'floggraph'`, `'git'`, `'diff'`; any other string is treated as a `FileType` name. + +--- + +## Context + +Users who open ecosystem tool outputs (flog graph, fugitive commit view, vimdiff) in tabs consistently need two things: + +1. A single command to open the tool in a new tab (the same pattern `TabsVim_FzfOpenInTab()` already established for fzf). +2. A `q` binding to close the whole tab when done — the "modal overlay" mental model. + +Without plugin support, users must write three separate augroups in their vimrc, each guarding a different filetype or buffer condition. This is boilerplate that belongs in the plugin because: +- It is tab-scoped behavior (`:tabclose`, not `:q` or `:close`) +- The same pattern repeats identically for every ecosystem tool +- It is discoverable — users looking for tab integrations will find it here + +The `diff` condition (`WinEnter` + `&diff`) requires special handling (not a FileType) but is common enough to be a first-class supported token. + +--- + +## Considered Options + +| Option | Pros | Cons | +|--------|------|------| +| **`g:tabs_vim_tabclose_types` list, default empty** *(chosen)* | Opt-in; consistent with ADR-004 (no OOTB bindings); name reflects buffer types not just filetypes | Requires one vimrc line to activate | +| Always-on autocmd for hardcoded filetypes | Zero config | Imposes behavior on users who manage their own `q` bindings for these tools | +| No autocmd; document pattern in key-binding.md | Zero plugin complexity | Users copy the same boilerplate repeatedly; no discoverability | +| Per-tool boolean flags (`g:tabs_vim_flog_close`, etc.) | Granular opt-in | Proliferates config variables for a single repeated pattern | + +The list-based approach keeps one config axis while remaining extensible. Any filetype string works, so users with tools not listed here (e.g. `'fugitiveblame'`, `'GV'`) can add them without plugin changes. + +--- + +## Consequences + +- `TabsVim_FlogInTab()` is added to the Public Function API in `key-binding.md` under a new "Git Integration" section. +- `g:tabs_vim_tabclose_types` is documented in both `ecosystem-support.md` and the recommended vimrc block in `key-binding.md`. +- The plugin continues to install zero OOTB keybindings (ADR-004 is not affected); the autocmd only fires if the user sets `g:tabs_vim_tabclose_types`. +- Users with existing manual augroups for `q` → `:tabclose` can migrate to the config or leave their vimrc unchanged — both work. +- `diff` handling uses `WinEnter` + `&diff` guard (matching the existing vimrc pattern) rather than a filetype, since vimdiff buffers don't have a dedicated filetype. diff --git a/docs/specs/ecosystem-support.md b/docs/specs/ecosystem-support.md new file mode 100644 index 0000000..c361a61 --- /dev/null +++ b/docs/specs/ecosystem-support.md @@ -0,0 +1,78 @@ +# SPEC: Ecosystem Support + +**Last Updated:** 2026-04-05 + +--- + +## Description + +Defines tab-aware integration functions for common Vim ecosystem plugins. The plugin already exposes `TabsVim_FzfOpenInTab()` for fzf. This spec extends that pattern to cover vim-flog (git log viewer) and establishes a configurable autocmd for binding `q` → `:tabclose` in ecosystem tool buffers (flog graph, fugitive, vimdiff). + +The common workflow: open a tool's output in a dedicated tab, then press `q` to close the whole tab when done — the same mental model as a modal overlay. Without plugin support, users must replicate three separate augroups across their vimrc. + +**Persona:** Vim/Neovim users with fzf, vim-flog, and/or vim-fugitive in their setup who open those tools' outputs in tabs. + +--- + +## Features + +| Feature | Description | ADR | Done? | +|---------|-------------|-----|-------| +| **`TabsVim_FlogInTab()`** | Open vim-flog git log in a new tab (`Flogsplit -open-cmd=tabedit -all`) | ADR-005 | ⬜ | +| **Ecosystem buffer close** | `g:tabs_vim_tabclose_types` list: buffer types/conditions that get `q` → `:tabclose` auto-wired | ADR-005 | ⬜ | + +--- + +## Public Function API + +### Git Integration + +| Function | Description | +|----------|-------------| +| `TabsVim_FlogInTab()` | Open vim-flog full-repo git log in a new tab (requires vim-flog) | + +### Ecosystem Buffer Close + +No function — controlled by config only (see below). + +--- + +## Configuration + +### `g:tabs_vim_tabclose_types` + +A list of buffer type tokens. For each entry the plugin installs a buffer-local `q` → `:tabclose` mapping. Default is empty (no OOTB behavior). + +```vim +" opt-in example — must be set before the plugin loads (i.e. before plug#end()) +let g:tabs_vim_tabclose_types = ['floggraph', 'git', 'diff'] +``` + +Supported entry values: + +| Value | Trigger condition | Typical source | +|-------|-------------------|----------------| +| `'floggraph'` | `FileType floggraph` | vim-flog graph buffer | +| `'git'` | `FileType git` | vim-fugitive commit/status buffer | +| `'diff'` | `WinEnter` with `&diff` set | vimdiff / `Gdiffsplit` | + +Users may pass any valid `FileType` name to cover other tools (e.g. `'fugitiveblame'`). + +--- + +## Recommended vimrc Wiring + +```vim +" ── Git log ────────────────────────────────────────────────────────────────── +nnoremap gg :call TabsVim_FlogInTab() + +" ── Ecosystem buffer close (q → :tabclose) ─────────────────────────────────── +let g:tabs_vim_tabclose_types = ['floggraph', 'git', 'diff'] +``` + +--- + +## Related + +- [key-binding.md](key-binding.md) — full public function API and vimrc wiring reference +- ADR-005 — decision record for this spec diff --git a/docs/specs/key-binding.md b/docs/specs/key-binding.md index 1ff5c1b..46d8c73 100644 --- a/docs/specs/key-binding.md +++ b/docs/specs/key-binding.md @@ -47,6 +47,12 @@ Only operations with real logic are exposed as `TabsVim_*` functions. Simple one |----------|-------------| | `TabsVim_FzfOpenInTab()` | Open fzf file picker with `tabedit` as the sink (requires fzf.vim) | +### Git Integration + +| Function | Description | +|----------|-------------| +| `TabsVim_FlogInTab()` | Open vim-flog full-repo git log in a new tab (requires vim-flog) | + --- ## Recommended vimrc Wiring @@ -80,6 +86,13 @@ nnoremap tt :call TabsVim_NewTabTerm() nnoremap gF :tabedit nnoremap fy :let @+ = expand("%:p") nnoremap ft :call TabsVim_FzfOpenInTab() + +" ── Git log ─────────────────────────────────────────────────────────────────── +nnoremap gg :call TabsVim_FlogInTab() + +" ── Ecosystem buffer close (q → :tabclose) ─────────────────────────────────── +" Must be set before plug#end() — read once at plugin load time. +let g:tabs_vim_tabclose_types = ['floggraph', 'git', 'diff'] ``` --- @@ -127,3 +140,4 @@ In normal mode, `` is the native "jump to tag under cursor" (ctags / cscope | **No OOTB keybindings** | Plugin installs zero keymaps; mouse DnD infrastructure only | ADR-004 | ✅ | | **Public function API** | All operations promoted to `TabsVim_*` public functions | ADR-004 | ✅ | | **Example vimrc block** | Ready-made mapping block for users to copy into vimrc | — | ✅ | +| **Ecosystem integrations** | `TabsVim_FlogInTab()` and `g:tabs_vim_tabclose_types` config | ADR-005 | ⬜ | diff --git a/docs/specs/tabs.vim.md b/docs/specs/tabs.vim.md index a72fbba..faa3f84 100644 --- a/docs/specs/tabs.vim.md +++ b/docs/specs/tabs.vim.md @@ -258,6 +258,7 @@ let g:tabs_vim_colors = { ## Related Specs - [key-binding.md](key-binding.md) — public `TabsVim_*` function API, OOTB behavior contract, and recommended vimrc wiring +- [ecosystem-support.md](ecosystem-support.md) — tab-aware integrations for fzf, vim-flog, vim-fugitive, and vimdiff --- diff --git a/plugin/tabs.vim b/plugin/tabs.vim index e98c249..5495539 100644 --- a/plugin/tabs.vim +++ b/plugin/tabs.vim @@ -9,15 +9,12 @@ let g:tabs_vim_loaded = 1 " Principles: Locality, Discoverability, Speed, Integration """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" Terminal State (for split terminal toggle) +" TERMINAL """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + let s:term_bufnr = -1 let s:vterm_bufnr = -1 -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" TERMINAL: Split terminals & new tab terminal -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" - function! s:ToggleTerm(bufvar, open_cmd, resize_cmd) abort let bufnr = eval(a:bufvar) for w in range(1, winnr('$')) @@ -48,17 +45,6 @@ function! TabsVim_NewTabTerm() abort setlocal nobuflisted endfunction -" Terminal settings -augroup TermSettings - autocmd! - " No line numbers in terminal windows - autocmd TerminalOpen,BufEnter,WinEnter * if &buftype ==# 'terminal' | setlocal nonumber norelativenumber | endif - " Resize terminal buffers when vim window size changes - autocmd VimResized * call s:ResizeTerminals() - " Keep newly opened terminals in the current working directory - autocmd TerminalOpen * call term_sendkeys(bufnr(''), 'cd ' . shellescape(getcwd()) . "\n") -augroup END - function! s:ResizeTerminals() abort for buf in term_list() let w = bufwinnr(buf) @@ -66,11 +52,18 @@ function! s:ResizeTerminals() abort endfor endfunction +augroup TabsVimTerminal + autocmd! + autocmd TerminalOpen,BufEnter,WinEnter * if &buftype ==# 'terminal' | setlocal nonumber norelativenumber | endif + autocmd VimResized * call s:ResizeTerminals() + autocmd TerminalOpen * call term_sendkeys(bufnr(''), 'cd ' . shellescape(getcwd()) . "\n") +augroup END + """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" WINDOWS & BUFFERS +" WINDOW / BUFFER """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" Close window or terminal, with prompt to quit if it's the last window in the last tab +" Close window or terminal; prompt to quit if it's the last window in the last tab function! TabsVim_CloseOrHide() abort if tabpagenr('$') == 1 && winnr('$') == 1 if confirm('Quit Vim?', "&Yes\n&No", 2) == 1 @@ -92,7 +85,6 @@ function! TabsVim_CloseOrHide() abort endif endfunction -" Rename current buffer function! TabsVim_RenameBuffer() abort let l:new_name = input('Rename buffer: ', expand('%:t')) if empty(l:new_name) @@ -102,8 +94,10 @@ function! TabsVim_RenameBuffer() abort endfunction """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" FZF Integration: Open files in tabs +" ECOSYSTEM """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +" fzf: open file picker with tabedit as the sink function! TabsVim_FzfOpenInTab() abort if !exists('*fzf#vim#files') || !exists('*fzf#vim#with_preview') echohl WarningMsg @@ -114,27 +108,69 @@ function! TabsVim_FzfOpenInTab() abort call fzf#vim#files('', fzf#vim#with_preview({'sink': 'tabedit'}), 0) endfunction +" vim-flog: open full-repo git log in a new tab +function! TabsVim_FlogInTab() abort + if !exists(':Flogsplit') + echohl WarningMsg + echo 'tabs.vim: TabsVim_FlogInTab requires vim-flog (:Flogsplit not available)' + echohl None + return + endif + Flogsplit -open-cmd=tabedit -all +endfunction + +" g:tabs_vim_tabclose_types: wire q → :tabclose for specified buffer types. +" Must be set before the plugin loads. Supported tokens: 'floggraph', 'git', +" 'diff', or any FileType name matching ^\h\w*$. +if exists('g:tabs_vim_tabclose_types') + if type(g:tabs_vim_tabclose_types) != type([]) + echohl WarningMsg + echo 'tabs.vim: g:tabs_vim_tabclose_types must be a List of strings; skipping' + echohl None + else + let s:tabclose_types = filter(copy(g:tabs_vim_tabclose_types), + \ 'type(v:val) == type("") && !empty(v:val)') + if !empty(s:tabclose_types) + augroup TabsVimTabClose + autocmd! + for s:tabclose_type in s:tabclose_types + if s:tabclose_type ==# 'diff' + " Use so the mapping re-checks &diff at keypress time; + " prevents stale q→tabclose after a buffer leaves diff mode. + autocmd WinEnter * if &diff | nnoremap q (&diff ? "\tabclose\" : 'q') | endif + elseif s:tabclose_type =~# '^\h\w*\%(,\h\w*\)*$' + execute 'autocmd FileType ' . s:tabclose_type . ' nnoremap q :tabclose' + else + echohl WarningMsg + echo 'tabs.vim: ignoring invalid tabclose type: ' . string(s:tabclose_type) + echohl None + endif + endfor + unlet s:tabclose_type + augroup END + endif + unlet s:tabclose_types + endif +endif + """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" DRAG-AND-DROP: Drop a file path onto the terminal → open in new tab +" DRAG AND DROP """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" Requires bracketed-paste support in the terminal (iTerm2, xterm, etc.) -" Drag a file from Finder/Explorer → terminal sends ESC[200~path ESC[201~ + +" Drop a file path onto the terminal → open in new tab. +" Requires bracketed-paste support (iTerm2, xterm, etc.). +" Activates only in terminal Vim (not GVim) on Vim 8.0.0210+. if has('patch-8.0.0210') && !has('gui_running') let &t_BE = "\e[?2004h" " enable bracketed paste on Vim entry let &t_BD = "\e[?2004l" " disable on Vim exit exec "set =\e[200~" exec "set =\e[201~" - " Normal mode: intercept bracket-paste start, collect path, open in new tab - nnoremap call HandleFileDrop() - - " Insert mode: swallow paste markers - inoremap - inoremap - - " Command mode: silently swallow the markers so pasted text is clean - cnoremap - cnoremap + nnoremap call HandleFileDrop() + inoremap + inoremap + cnoremap + cnoremap function! s:HandleFileDrop() abort let text = '' @@ -153,12 +189,12 @@ if has('patch-8.0.0210') && !has('gui_running') endif """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" Tab bar — replaces status line: mode (left) | tabs (center) | position (right) +" TAB BAR """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -" ══════════════════════════════════════════════════════════════════════════════ -" MODE COLORS — override any mode via g:tabs_vim_colors (see docs/specs/tabs.vim.md) -" Each entry: [guifg, guibg, ctermfg, ctermbg] +" ── Colors ──────────────────────────────────────────────────────────────────── +" Override any key via g:tabs_vim_colors: [guifg, guibg, ctermfg, ctermbg] + let s:tabs_vim_defaults = { \ 'normal': ['#282a36', '#bd93f9', 235, 141], \ 'insert': ['#282a36', '#50fa7b', 235, 84 ], @@ -198,18 +234,17 @@ function! s:ApplyColors() abort endfunction call s:ApplyColors() -" ══════════════════════════════════════════════════════════════════════════════ -set showtabline=2 +" ── Rendering ───────────────────────────────────────────────────────────────── function! TabsVim_ModeName() abort let l:map = { \ 'n': 'N', 'no': 'N·OP', \ 'i': 'I', 'ic': 'INSERT', 'ix': 'INSERT', - \ 'R': 'R', 'Rc': 'REPLACE', + \ 'R': 'R', 'Rc': 'REPLACE', \ 'v': 'V', 'V': 'V·LINE', "\": 'V·BLOCK', \ 's': 'S', 'S': 'S·LINE', "\": 'S·BLOCK', - \ 'c': 'C', 't': 'T' + \ 'c': 'C', 't': 'T' \ } return get(l:map, mode(), mode()) endfunction @@ -234,10 +269,10 @@ function! TabsVim_ModeStyle() abort endfunction function! TabsVim_Line() abort - let l:hls = TabsVim_ModeHl() " [pill_hl, sel_tab_hl] + let l:hls = TabsVim_ModeHl() let l:style = TabsVim_ModeStyle() let l:sel_hl = l:style ==# 'mode' ? '%#TabLineSel#' : l:hls[1] - " ── Left: tabs (%NT = native Vim click-to-switch + drag) + let s = '' for t in range(1, tabpagenr('$')) let buflist = tabpagebuflist(t) @@ -251,7 +286,6 @@ function! TabsVim_Line() abort if t < tabpagenr('$') | let s .= '%#TabLineFill#│' | endif endfor - " ── Right: mode block ─────────────────────────────────────────────────────── if l:style !=# 'tabs' let s .= '%T%=' . l:hls[0] . ' ' . TabsVim_ModeName() . ' ' endif @@ -259,9 +293,11 @@ function! TabsVim_Line() abort return s endfunction +set showtabline=2 set tabline=%!TabsVim_Line() -" Refresh tabline on mode change and cursor movement +" ── Refresh ─────────────────────────────────────────────────────────────────── + augroup TabsVimRefresh autocmd! autocmd InsertEnter,InsertLeave,CursorMoved,CursorMovedI,WinEnter * redrawtabline