Simple Vim Session Management: Part 2

By: Jay Sitter
Stack of suitcases

In our last post, we set up a couple remaps that would help us quickly save and restore session files. (If you haven’t read it, I recommend that you do before continuing.) At the beginning of the post I listed a few pain points of Vim’s session support; we’ve already addressed the first two, but a third one remains:

  1. Session updating: Sessions don’t “auto-update” when you make changes to your environment, so if I open a new tab, for instance, and I want that to be part of the session I saved earlier, I have to remember to do another :mksession, remembering the location and name of the file in the process.

Wouldn’t it be great if, once we saved a session, we remained “in” that session, with Vim saving any changes to tabs, windows, folds, etc., all in the background? Fortunately, there’s a plugin for that.

The inimitable Tim Pope maintains vim-obsession, a focused, lightweight Vim plugin (only 123 lines) that does exactly this, and nothing more. There are of course more full-fledged session management plugins out there, but I prefer to keep things as dependency-free as possible; and you can’t go wrong with any of Tim Pope’s plugins, so this is a good place to start, at least.

Once you get the plugin installed, three new functions become available:

  • :Obsession [file name]: This will write a session file to the filename you specify and begin auto-updating it as you make changes to your environment.
  • :Obsession: With no filename specified, this will either (a) pause your current session, or (b) start a new session in your current working directory called Session.vim.
  • :Obsession!: This will delete the session file you’re currently “in” and turn off session tracking.

We’re going to replace the usual :mksession command in our existing remaps to use vim-obsession, as well as modify our tabline (the setting that dictates what text is displayed in the row of tabs at the top of Vim) to always show us which session we’re “in” at any given time.

Updating Remaps to Work With Vim-Obsession

Here are our existing remaps from the last post:

let g:sessions_dir = '~/vim-sessions'
exec 'nnoremap <Leader>ss :mks! ' . g:session_dir . '/*.vim<C-D><BS><BS><BS><BS><BS>'
exec 'nnoremap <Leader>sr :so ' . g:session_dir. '/*.vim<C-D><BS><BS><BS><BS><BS>'

Our session restore remap, <Leader>sr, won’t need to change, because Obsession doesn’t handle the restoring of sessions at all. We’re only going to have to slightly change our session save remap:

exec 'nnoremap <Leader>ss :Obsession ' . g:session_dir . '/*.vim<C-D><BS><BS><BS><BS><BS>'

We don’t need an exclamation point after :Obsession like we did after :mks, because vim-obsession will overwrite existing files automatically (as long as they look like Vim session files).

Let’s also add a remap for pausing and unpausing our current session if we’re in one. Let’s us <Leader>sp for “session pause” (alternatively you might try sf for “session freeze,” to keep the keys on your left hand):

nnoremap <Leader>sp :Obsession<CR>

Displaying Current Session in the Tabline

This is all great, but it would be even better if we could at a glance always know whether we’re in a session and which one it is. vim-obsession gives us the function ObsessionStatus(), which will tell us whether we are or are not in a session with the strings [$] and [S], respectively. From :h Obsession:

Add %{ObsessionStatus()} to ‘statusline’, ‘tabline’, or ‘titlestring’ to get an indicator when Obsession is active or paused. Pass an argument to override the text of the indicator and a second argument to override the text of the paused indictor.

There isn’t any more guidance than this, so we’re kind of on our own.

To get the name of the session file itself, we can look to the variable v:this_session, which, natively as part of Vim, gets set after every session save and restore, with or without vim-obsession installed.

Our vim-obsession status only needs to be displayed once, so I think the tabline makes the most sense for this; since tabs are aligned to the left side of the screen, let’s put our vim-obsession status to the far right.

Modifying tabline is a bit hairy, so we’re going to start with the example given by Vim’s own documentation in :h setting-tabline. Let’s copy it verbatim into our .vimrc:

function MyTabLine()
  let s = ''
  for i in range(tabpagenr('$'))
    " select the highlighting
    if i + 1 == tabpagenr()
      let s .= '%#TabLineSel#'
    else
      let s .= '%#TabLine#'
    endif

    " set the tab page number (for mouse clicks)
    let s .= '%' . (i + 1) . 'T'

    " the label is made by MyTabLabel()
    let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
  endfor

  " after the last tab fill with TabLineFill and reset tab page nr
  let s .= '%#TabLineFill#%T'

  " right-align the label to close the current tab page
  if tabpagenr('$') > 1
    let s .= '%=%#TabLine#%999Xclose'
  endif

  return s
endfunction

function MyTabLabel(n)
  let buflist = tabpagebuflist(a:n)
  let winnr = tabpagewinnr(a:n)
  return bufname(buflist[winnr - 1])
endfunction

This can look overwhelming if you haven’t dealt with statusline or tabline before, but don’t worry if you don’t understand it; this gives us a good, basic tabline that is very close to the default, and our modifications will be minimal. We want to stick v:this_session in there somewhere.

Notice the close button toward the end of the MyTabLine() function: “right-align the label to close the current tab page.” It’s that %= string that right-aligns the remainder of the string s, so because we want our vim-obsession status to be right-aligned, let’s put it after that. We will change this:

" right-align the label to close the current tab page
if tabpagenr('$') > 1
  let s .= '%=%#TabLine#%999Xclose'
endif

To this:

let s .= '%=' " Right-align after this
" Show vim-obsession status if we're able
if exists(':Obsession')
    let s .= '%{ObsessionStatus()}'
endif
" the label to close the current tab page
if tabpagenr('$') > 1
  let s .= '%#TabLine#%999Xclose'
endif

(We want to make sure we’re only adding this if vim-obsession is installed, hence the check for the existence of the :Obsession function — otherwise, if the plugin becomes uninstalled or doesn’t load properly, we will see an error.)

We also want to show the name of the session file itself, so let’s add that:

let s .= '%=' " Right-align after this
" Show vim-obsession status if we're able
if exists(':Obsession')
    let s .= '%{ObsessionStatus()}'
    if exists('v:this_session') && v:this_session != ''
        let s .= ' ' . v:this_session
    endif
endif

This gives us something like this:

Hm, that session file includes the full path. Let’s trim that down to just the filename with some splitting. I’m also going to add some color based on whether or not we are in a session:

let s .= '%=' " Right-align after this

if exists('g:this_obsession')
    let s .= '%#diffadd#' " Use the "DiffAdd" color if in a session
endif

if exists(':Obsession')
    let s .= "%{ObsessionStatus()}"
    if exists('v:this_session') && v:this_session != ''
        let s:obsession_string = v:this_session
        let s:obsession_parts = split(s:obsession_string, '/')
        let s:obsession_filename = s:obsession_parts[-1]
        let s .= ' ' . s:obsession_filename . ' '
        let s .= '%*' " Restore default color
    endif
endif

Much better.

Now when we hit <Leader>sp, our session will pause, and our tabline will look like this:


Awesome. Now with a couple quick keystrokes, we can create, pause, and restore a Vim session, all while keeping our current session updated with any changes we make to our environment.

Here’s our final .vimrc so far, including our work from the last post:

let g:sessions_dir = '~/vim-sessions'

" Remaps for Sessions
exec 'nnoremap <Leader>ss :Obsession ' . g:session_dir . '/*.vim<C-D><BS><BS><BS><BS><BS>'
exec 'nnoremap <Leader>sr :so ' . g:session_dir. '/*.vim<C-D><BS><BS><BS><BS><BS>'

" Our custom TabLine function
function MyTabLine()
  let s = ''
  for i in range(tabpagenr('$'))
    " select the highlighting
    if i + 1 == tabpagenr()
      let s .= '%#TabLineSel#'
    else
      let s .= '%#TabLine#'
    endif

    " set the tab page number (for mouse clicks)
    let s .= '%' . (i + 1) . 'T'

    " the label is made by MyTabLabel()
    let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
  endfor

  " after the last tab fill with TabLineFill and reset tab page nr
  let s .= '%#TabLineFill#%T'

    let s .= '%=' " Right-align after this

    if exists('g:this_obsession')
        let s .= '%#diffadd#' " Use the "DiffAdd" color if in a session
    endif

    " add vim-obsession status if available
    if exists(':Obsession')
        let s .= "%{ObsessionStatus()}"
        if exists('v:this_session') && v:this_session != ''
            let s:obsession_string = v:this_session
            let s:obsession_parts = split(s:obsession_string, '/')
            let s:obsession_filename = s:obsession_parts[-1]
            let s .= ' ' . s:obsession_filename . ' '
            let s .= '%*' " Restore default color
        endif
    endif

  return s
endfunction

" Required for MyTabLine()
function MyTabLabel(n)
  let buflist = tabpagebuflist(a:n)
  let winnr = tabpagewinnr(a:n)
  return bufname(buflist[winnr - 1])
endfunction

Happy Vimming.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including Seattle, Portland, Los Angeles, Denver, Chicago, Austin, Dallas, New York, and Boston.