Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
16 changes: 16 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Vim Tabs - use vim tabs correctly and productively.
Copyright (C) 2020 Flavius Aspra <flavius.as@gmail.com>
Get the latest source from https://github.com/flavius/vim-tabs

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
163 changes: 161 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,161 @@
# vim-tabs
Vim plugin which helps you to use tabs correctly and productively
# Why it matters?
Productivity in vim matters.

# What it does

It helps you use tabs correctly and productively.

This plugin is a work in progress, but it introduces the notion of "context",
or "logical view" which, when paired with vim tabs, can be very powerful.

In short:

- a context is a logical view of your code, a cut through a smaller subset of
your buffers
- contexts can be nested
- you can work with multiple contexts in parallel
- you can work on the same file from different contexts
- in vim, one context is represented by exactly one tab
- "context thinking" can help resolve complex merge conflicts

A lot of productivity features are planned. See issues for a list.

# Screenshot

You don't see it, you use it:

![screenshot](doc/screenshot.png)

# Why this plugin?

Whenever beginners or somewhat routined vimmers find out about tabs, they
attempt to use tabs as windows. When they ask for help about managing their
tabs, they are told by seasoned users that they're doing it wrong.

The correct way to use tabs is to use one tab per logical context.

But what is a logical context?

Let's have a look at a hypothetical directory structure

```

── project-root
├── main.txt
├── A
│   └── B
│   ├── cd.txt
│   ├── C
│   │   ├── b.txt
│   │   └── a.txt
│   └── D
│   └── c.txt
└── X
└── Y
├── ab.txt
└── Z
   ├── b.txt
   └── a.txt

```


In one logical view you might be editing just the files `a.txt` and `b.txt`.

Note: The directory `C` has just two files for brevity, but it could have more
files and directories inside.

The working directory of this logical view might sense to be the directory
`C`.


In another logical view you could be editing just file `c.txt`, but
semantically, let's suppose it would make sense to also edit `cd.txt` in this
view. For this reason, the working directory of this view would be the entire
directory `B`.

You might then argue that the first logical view is a subset of the second
view - and you might be right, but maybe also not! It depends on the criteria
you use to group files. Maybe from the architectural standpoint it makes sense
to group them differently - for the task at hand.

A logical view is a (semantic) lens through which you look at your project and
what lenses you define depends heavily on the project. Fact is, in reasonably
sized projects, you will need this lensing because of the rigidity of the
filesystem.

If a feature request comes in, and the changes you need to make have ripple
effects throughout the architecture, it might make sense to have three logical
views:

1. `a.txt`, `b.txt` CWD: `C` - for implementation details of subsystem `C`
2. `c.txt`, `cd.txt`, CWD: `B` - for implementation details of subsystem `B`
3. `main.txt`, `cd.txt`, `ab.txt`, CWD: `project-root` - for the topmost
integration level

You might have to switch constantly between the three logical views, and
having each of them available in its own tab can help you move around faster
with less cognitive load.

Note: the file `cd.txt` is loaded in two logical views simultaneously.
Note: CWD stands for current working directory (of the view/tab).

Please notice the MIGHT in the above example. It might be a good approach! But
it can also be that you realize that in fact you're moving a lot between the
files `a.txt`, `ab.txt` and `main.txt`. In this case, maybe it's best to
define a new logical view with these files, so that you move between them
quickly.

# Installation

Install using your favorite package manager, or use Vim's built-in package support:

```
mkdir -p ~/.vim/pack/flavius/start
cd ~/.vim/pack/flavius/start
git clone https://github.com/flavius/vim-tabs.git
vim -u NONE -c "helptags vim-tabs/doc" -c q
```

# Provided commands

Keep in mind that this is a working in progress and things might change - but
I'm trying to keep the "public API" as stable as possible.

That being said, the commands are currently:

- `TabHistoryGotoNext`
- `TabHistoryGotoPrev`
- `TabHistoryClear`
- `TabHistoryList`

With much more being planned!

My mappings are for instance

```
nnoremap <silent> <Leader>tn :TabHistoryGotoNext<CR>
nnoremap <silent> <Leader>tp :TabHistoryGotoPrev<CR>
nnoremap <silent> <Leader>tc :TabHistoryClear<CR>
nnoremap <silent> <Leader>tl :TabHistoryList<CR>
```

# Relevant vim settings, commands and other plugins, and requirements

- `autochdir` - should not be used, because it constraints the `CWD` too much, and
thus the files which you can load with ease
- `set hidden` - must be set, so that you can hide buffers, while loading
different ones in the current window
- `vim-rooter` - must have its chdir feature disabled
- `tcd` - is vital, use it. If you start a tab with a file at the root of the new
context, you can change the directory with `tcd %:p:h`
- use marks and other motions! See `:help motions.txt`

This plugin is best used with other productivity plugins:

- `CtrlP` - find files only in the current context
- `taboo.vim` - give the tab a meaningful name
- `startify` - easily resume your work on your contexts by using sessions
- `floaterm` - start a shell in the current context
- `NERDTree` - browse files in the current context, when you don't know what to
search for with `CtrlP`
141 changes: 141 additions & 0 deletions autoload/tabs.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
" =============================================================================
" Public API
" =============================================================================
" Go to the next-higher buffer number
function! tabs#GoToNext()
let visited_bufs = s:GetSortedBuffersOfTab(tabpagenr())
let current_buf = str2nr(bufnr())
let current_pos = index(visited_bufs, current_buf)
if current_pos == -1
echom "Current buffer not found. Please report a bug."
return
endif
if current_pos == len(visited_bufs)-1
let new_buffer_pos = 0
else
let new_buffer_pos = current_pos + 1
endif
execute "buffer" visited_bufs[new_buffer_pos]
endfunction

" Go to the previous buffer by number
function! tabs#GoToPrev()
let visited_bufs = s:GetSortedBuffersOfTab(tabpagenr())
let current_buf = str2nr(bufnr())
let current_pos = index(visited_bufs, string(current_buf))
if current_pos == -1
return
endif
if current_pos == 0
let new_buffer_pos = len(visited_bufs)-1
else
let new_buffer_pos = current_pos - 1
endif
execute "buffer" visited_bufs[new_buffer_pos]
endfunction

function! tabs#List()
let tabnr = tabpagenr()
let visited_bufs = gettabvar(tabnr, 'visited_bufs', {})
let cwd = getcwd(-1, tabnr)
let cwdlen = len(cwd)
echom printf("%7s %7s %s", "Buffer", "Visits", " File relative to " . cwd)
for bufnr in keys(visited_bufs)
" if !s:BufferIsListed(bufnr)
" continue
" endif
let info = getbufinfo(bufnr+0)[0]
if has_key(info, 'variables')
unlet info['variables']
endif
if has_key(info, 'signs')
unlet info['signs']
endif
let vis_count = visited_bufs[bufnr]
let name = info['name'][cwdlen+1:]
echom printf("%7d %7d %5s", bufnr, vis_count, name)
"echom bufnr . " visited " . vis_count . " " . info['name'] . " info " . string(info)
endfor
endfunction

function! tabs#ClearHistory()
let tabnr = tabpagenr()
let visited_bufs = s:GetSortedBuffersOfTab(tabnr)
for bufnr in visited_bufs
call s:DeleteFromTabHistory(tabnr, bufnr)
endfor
endfunction

" =============================================================================
" Helper functions
" =============================================================================
function! s:GetSortedBuffersOfTab(tabnr)
let visited_bufs = gettabvar(a:tabnr, 'visited_bufs', {})
let visited_bufs = keys(visited_bufs)
let t = map(visited_bufs, {k, v -> str2nr(v) })
return sort(t)
endfunction

function! s:BufferIsListed(bufnr)
let info = getbufinfo(a:bufnr + 0)
if !len(info)
return v:false
endif
if info[0].listed == 1
return v:true
endif
return v:false
endfunction

function! s:DeleteFromTabHistory(tabnr, bufnr)
let visited_bufs = gettabvar(a:tabnr, 'visited_bufs', {})
if has_key(visited_bufs, a:bufnr)
unlet visited_bufs[a:bufnr]
call settabvar(a:tabnr, 'visited_bufs', visited_bufs)
endif
endfunction

function! s:DeleteFromAllTabHistory(bufnr)
for tabnr in range(1, tabpagenr('$'))
call s:DeleteFromTabHistory(tabnr, a:bufnr)
endfor
endfunction!

" =============================================================================
" Event handlers
" =============================================================================
function! tabs#OnBufVisible(bufnr)
if !s:BufferIsListed(a:bufnr + 0)
return
endif
let tabnr = tabpagenr()
let visited_bufs = gettabvar(tabnr, 'visited_bufs', {})
if !has_key(visited_bufs, a:bufnr+0)
let visited_bufs[a:bufnr+0] = 0
endif
let visited_bufs[a:bufnr+0] = visited_bufs[a:bufnr+0] + 1
call settabvar(tabnr, 'visited_bufs', visited_bufs)
" let info = getbufinfo(a:bufnr+0)[0]
" unlet info['variables']
" let name = fnamemodify(info['name'], ':t')
endfunction

function! tabs#OnBufDeleted(bufnr)
if !s:BufferIsListed(a:bufnr+0)
return
endif
call s:DeleteFromAllTabHistory(a:bufnr)
let tabnr = tabpagenr()
" echom "buffer deleted " . a:bufnr . ' tab: ' . tabnr . ' visited: ' . string(t:visited_bufs)
endfunction

function! tabs#FilterFileTypes(bufnr)
let ft = getbufvar(a:bufnr, '&filetype')
if (index([], ft) >= 0)
call s:DeleteFromAllTabHistory(a:bufnr)
endif
if !s:BufferIsListed(a:bufnr + 0)
call s:DeleteFromAllTabHistory(a:bufnr)
endif
endfunction

Binary file added doc/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading