diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6b0e952 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,16 @@ +Vim Tabs - use vim tabs correctly and productively. +Copyright (C) 2020 Flavius Aspra +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 . diff --git a/README.md b/README.md index 9010e4c..23a21bd 100644 --- a/README.md +++ b/README.md @@ -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 tn :TabHistoryGotoNext +nnoremap tp :TabHistoryGotoPrev +nnoremap tc :TabHistoryClear +nnoremap tl :TabHistoryList +``` + +# 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` diff --git a/autoload/tabs.vim b/autoload/tabs.vim new file mode 100644 index 0000000..4704e14 --- /dev/null +++ b/autoload/tabs.vim @@ -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 + diff --git a/doc/screenshot.png b/doc/screenshot.png new file mode 100644 index 0000000..3ababb1 Binary files /dev/null and b/doc/screenshot.png differ diff --git a/doc/vim-tabs.txt b/doc/vim-tabs.txt new file mode 100644 index 0000000..04d161a --- /dev/null +++ b/doc/vim-tabs.txt @@ -0,0 +1,304 @@ +*vim-tabs.txt* Use tabs correctly and productively. + +Author: Flavius Aspra +License: GPL v3 + + +============================================================================== +# _____ _ # +# |_ _| | | # +# | | __ _ | |__ ___ # +# | | / _` || '_ \ / __| # +# | || (_| || |_) |\__ \ # +# \_/ \__,_||_.__/ |___/ # +# # +============================================================================== + +CONTENTS *vim-tabs-contents* + + 1. Terminology and Workflow ................................. |vim-tabs-intro| + 2. Required Settings ................................. |vim-tabs-requirements| + 3. Usage .................................................... |vim-tabs-usage| + 4. Commands .............................................. |vim-tabs-commands| + 5. Mappings .............................................. |vim-tabs-mappings| + 6. Example Config .......................................... |vim-tabs-config| + 7. Recommendations ................................ |vim-tabs-recommendations| + + +============================================================================== +TERMINOLOGY AND WORKFLOW *vim-tabs-intro* +------------------------------------------------------------------------------ + +Before talking about (vim) tabs and how they fit into the picture, we should +be talking about the notion of "context". Another term for "context" could be +"logical view". + +In a project with many files, you try to group files together in directories +of related files, by a certain criteria, which makes sense for the project at +hand. + +Unfortunately, a flat hierarchy of directories with a single level of depth is +usually not enough, so you will naturally nest the directories in other +directories, creating a tree of directories and files, arbitrarily deep. + +While the criteria you used initially to group your files was good, in +a functioning project of reasonable complexity, you will also have +dependencies between files in different subtrees of the project. + +This is not a problem with your strategy for grouping files, it's rather +a problem with filesystems, which force you to have a rigid tree of +directories. Add to that the fact that at some point, different parts of the +project, different subsystems, different classes, etc, have to be wired +together - they have to communicate, even if higher up in the abstraction tree +of the architecture. + +Components from different subtrees need to be integrated. + +This is normal and can be tackled with architectural artifacts. + +The problem is the following: when you make a change in one of the subtrees, +you have to make compensating changes in another subtree, in order to not +introduce bugs. + +And here is where tabs come into play. Or rather, contexts. Let's talk about +contexts first, and in the next section we'll focus on the practical aspect. + +Each of the subtrees can normally be edited in isolation, if you're changing +implementation details of the respective subsystem. For each of them, you +would have their own "logical view". In vim, this "logical view" is +materialized as "a tab page". + +If you then need to implement a feature which involves integrating them, you +create a third "logical view". + +In this third logical view, you will probably not open so many files, but it +will be those which, when modified, trigger corresponding changes in the files +from another subsystem. But you will be making this changes all over the +place, poking at the code. + +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 the 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. + + +------------------------------------------------------------------------------ +VIM PARLANCE +------------------------------------------------------------------------------ + +As you might have guessed, a logical view is a tab in vim. This is the purpose +of a tab, and this is why seasoned vim users will tell beginners that they use +tabs wrongly. + +The building block of vim is "the buffer". This is the in-memory +representation of a file. When you load a file in vim, it gets a buffer. + +When you look at a file, you see just a part of it (if the file is very big or +the window is too small). The mechanism you use to see this slice of a buffer +is called a window. As an analogy, in web browser, people call this "the +viewport" - just a slice of the whole thing, or a "view into". + +So you might have different windows looking at different slices of the same +buffer. + +On top of this come the tabs. They are collections of windows. + +The issue with tabs is that they don't keep track of which buffers you're +viewing or editing in them, and you must manage this more manually, adding +friction to the workflow. + +Vim itself has commands like :ls to list the buffers, :bnext & co to move +across buffers, but they involve all buffers, irrespective of the tab! + +Which means that if you use them, you end up cobbling up your logical view +with buffers from completely different logical views. + +Vim tabs provides the necessary commands to navigate and manage your logical +views with care. + + +A logical view consists of + +- a purpose +- a tab +- the name of that tab +- the working directory of the tab +- all the files that can be loaded with vim, starting at the said directory +- the buffers containing those files + +To elaborate: + +- the purpose is what you set yourself to do, the types of edits that you want + to make to the files; the context is not something strictly vim-related, + it's rather a framework; I'm talking about it explicitly to put you in + a specific mindset (the vim mindset, when we're talking about tabs) +- the tab is the mechanics offered by vim as a representation of the purpose +- the name of the tab: by default, this is the name of the current file being + viewed in the tab; but there are plugins like `vim-taboo` which allow you to + rename it +- the working directory is thus relevant, because it can constrain what we load + into the current tab; we should strive to at least load buffers of files + from the current directory or deeper in the tree structure; the tab-local + `CWD` can be set with the `tcd` command +- the files themselves: as mentioned, keep in a tab only files under the same + directory (recursively); `CtrlP` helps here: once you set the `tcd` and open + `CtrlP`, you will get only files starting inside the `CWD` +- buffers are obviously shared among windows, and thus among tabs; however, + again, strive to edit the files only via the tab of the context in which + they belong + + +------------------------------------------------------------------------------ +EXAMPLE WORKFLOWS +------------------------------------------------------------------------------ + +While resolving a merge conflict, you might want to switch between contexts, +because 3-way diff can get very complicated very fast: + +- context "diff local to base" - this context reminds you of the changes done + in the current branch (`LOCAL`), relative to where you started +- context "diff remote to base" - this context shows the changes done by the + other branch (`REMOTE`) +- context "diff local to remote" - here you can resolve the conflicts more + easily, after understanding the changes from the previous two contexts +- context "working copy" - here you can navigate the current code + +This is not yet a feature of this plugin, but it's planned and it's meant to +outline again the power of "context thinking" when combined with vim tabs. + +------------------------------------------------------------------------------ +WORKFLOW ALTERNATIVES +------------------------------------------------------------------------------ + +- use one vim session per context +- use *argument-list* + +============================================================================== +REQUIRED SETTINGS *vim-tabs-requirements* +------------------------------------------------------------------------------ + +This plugin requires two settings > + + set noautochdir + set hidden +< +Also, you should not use plugins which change this behavior or interfere with +it. + +============================================================================== +USAGE *vim-tabs-usage* +------------------------------------------------------------------------------ + +Wip + +============================================================================== +COMMANDS *vim-tabs-commands* +------------------------------------------------------------------------------ + +:TabHistoryGotoNext *:TabHistoryGotoNext* + Go to the next buffer in the history of the current tab, by buffer number. + +:TabHistoryGotoPrev *:TabHistoryGotoPrev* + Go to the previous buffer in the history of the current tab, by buffer + nunmber. + +:TabHistoryClear *:TabHistoryClear* + Clear the history of seen buffers by current tab. + +:TabHistoryList *:TabHistoryList* + List the buffers seen by the current tab, similarly to |:ls|, but + restricted to the current context. + +============================================================================== +MAPPINGS *vim-tabs-mappings* +------------------------------------------------------------------------------ + +vim-tabs does not map any of the commands by default. If you want, you can map +them to something that makes sense to you, or just use the commands directly. + +Example configuration: > + nnoremap tn :TabHistoryGotoNext + nnoremap tp :TabHistoryGotoPrev + nnoremap tc :TabHistoryClear + nnoremap tl :TabHistoryList +< + +============================================================================== +EXAMPLE CONFIGS *vim-tabs-config* +------------------------------------------------------------------------------ + +vim-tabs does not offer any additional configuration currently. Everything is +achieved via commands, which you can map to your liking. + +============================================================================== +RECOMMENDATIONS *vim-tabs-recommendations* +------------------------------------------------------------------------------ + +The following plugins are fitting into the workflow proposed by vim-tabs: + +- CtrlP - finds files starting from the current working directory +- taboo.vim - good for renaming your tabs to better represent the context +- floaterm - starts a shell in the current working directory + + +vim:tw=78:ts=8:ft=help:norl: diff --git a/plugin/tabs.vim b/plugin/tabs.vim new file mode 100644 index 0000000..a475ce6 --- /dev/null +++ b/plugin/tabs.vim @@ -0,0 +1,9 @@ +command! -nargs=0 TabHistoryGotoNext call tabs#GoToNext() +command! -nargs=0 TabHistoryGotoPrev call tabs#GoToPrev() +command! -nargs=0 TabHistoryClear call tabs#ClearHistory() +command! -nargs=0 TabHistoryList call tabs#List() + +autocmd BufEnter * call tabs#OnBufVisible(expand('') + 0) +autocmd BufDelete * call tabs#OnBufDeleted(expand('') + 0) +autocmd BufWipeout * call tabs#OnBufDeleted(expand('') + 0) +autocmd FileType * call tabs#FilterFileTypes(expand('') + 0) diff --git a/t/simple.vim b/t/simple.vim new file mode 100644 index 0000000..e03a3ca --- /dev/null +++ b/t/simple.vim @@ -0,0 +1,14 @@ +function! s:TestFunction() + let tab1 = tabpagenr()+1 + let buf1 = bufnr('$')+1 + messages clear + tabnew + TabHistoryList + let messages = split(execute('messages'), '\n') + messages clear + tabclose + call testify#assert#matches(messages[1], 'Buffer\s\+Visits\s\+File relative to\s\+') + call testify#assert#matches(messages[2], '\s\+'.buf1.'\s\+1\s\+') +endfunction + +call testify#it('Create new tab and list the only buffer in it', function('s:TestFunction'))