1 " Vim script
  2 " Author: Peter Odding
  3 " Last Change: August 31, 2010
  4 " URL: http://peterodding.com/code/vim/session/
  5 
  6 " Now working on:
  7 "  - :mksession doesn't support quickfix/location lists and their windows :-\
  8 
  9 " Public API for session persistence. {{{1
 10 
 11 " The functions in this fold take a single list argument in which the Vim
 12 " script lines are stored that should be executed to restore the (relevant
 13 " parts of the) current Vim editing session. The only exception to this is
 14 " session#save_session() which expects the target filename as 2nd argument:
 15 
 16 function! session#save_session(commands, filename) " {{{2
 17   call add(a:commands, '" ' . a:filename . ': Vim session script.')
 18   call add(a:commands, '" Created by session.vim on ' . strftime('%d %B %Y at %H:%M:%S.'))
 19   call add(a:commands, '" Open this file in Vim and run :source % to restore your session.')
 20   call add(a:commands, '')
 21   call add(a:commands, 'set guioptions=' . escape(&go, ' "\'))
 22   call add(a:commands, 'set guifont=' . escape(&gfn, ' "\'))
 23   call session#save_features(a:commands)
 24   call session#save_colors(a:commands)
 25   let s:saved_qflist = 0
 26   call session#save_state(a:commands)
 27   call session#save_qflist(a:commands)
 28   call session#save_fullscreen(a:commands)
 29   call add(a:commands, '')
 30   call add(a:commands, '" vim: ft=vim ro nowrap smc=128')
 31 endfunction
 32 
 33 function! session#save_features(commands) " {{{2
 34   let template = "if exists('%s') != %i | %s %s | endif"
 35   for [global, command] in [
 36           \ ['g:syntax_on', 'syntax'],
 37           \ ['g:did_load_filetypes', 'filetype'],
 38           \ ['g:did_load_ftplugin', 'filetype plugin'],
 39           \ ['g:did_indent_on', 'filetype indent']]
 40     let active = exists(global)
 41     let toggle = active ? 'on' : 'off'
 42     call add(a:commands, printf(template, global, active, command, toggle))
 43   endfor
 44 endfunction
 45 
 46 function! session#save_colors(commands) " {{{2
 47   if exists('g:colors_name') && type(g:colors_name) == type('') && g:colors_name != ''
 48     let template = "if !exists('g:colors_name') || g:colors_name != %s | colorscheme %s | endif"
 49     call add(a:commands, printf(template, string(g:colors_name), fnameescape(g:colors_name)))
 50   endif
 51 endfunction
 52 
 53 function! session#save_fullscreen(commands) " {{{2
 54   try
 55     if xolox#shell#is_fullscreen()
 56       call add(a:commands, "if has('gui_running')")
 57       call add(a:commands, "  try")
 58       call add(a:commands, "    call xolox#shell#fullscreen()")
 59       " XXX Without this hack Vim on GTK doesn't restore &lines and &columns.
 60       call add(a:commands, "    call feedkeys(\":set lines=" . &lines . " columns=" . &columns . "\\<CR>\")")
 61       call add(a:commands, "  catch " . '/^Vim\%((\a\+)\)\=:E117/')
 62       call add(a:commands, "    \" Ignore missing full-screen plug-in.")
 63       call add(a:commands, "  endtry")
 64       call add(a:commands, "endif")
 65     endif
 66   catch /^Vim\%((\a\+)\)\=:E117/
 67     " Ignore missing full-screen functionality.
 68   endtry
 69 endfunction
 70 
 71 function! session#save_qflist(commands) " {{{2
 72   if has('quickfix') && !s:saved_qflist
 73     call add(a:commands, 'call setqflist(' . s:save_qflist(getqflist()) . ')')
 74     let s:saved_qflist = 1
 75   endif
 76 endfunction
 77 
 78 function! s:save_qflist(input)
 79   let result = []
 80   for entry in a:input
 81     if has_key(entry, 'bufnr')
 82       if !has_key(entry, 'filename')
 83         let entry.filename = bufname(entry.bufnr)
 84       endif
 85       unlet entry.bufnr
 86     endif
 87     call add(result, entry)
 88   endfor
 89   return string(result)
 90 endfunction
 91 
 92 function! session#save_state(commands) " {{{2
 93   let tempfile = tempname()
 94   let ssop_save = &sessionoptions
 95   try
 96     " The default value of &sessionoptions includes "options" which causes
 97     " :mksession to include all Vim options and mappings in generated session
 98     " scripts. This can significantly increase the size of session scripts
 99     " which makes them slower to generate and evaluate. It can also be a bit
100     " buggy, e.g. it breaks Ctrl-S when :runtime mswin.vim has been used. The
101     " value of &sessionoptions is changed temporarily to avoid these issues.
102     set ssop-=options ssop+=resize
103     execute 'mksession' fnameescape(tempfile)
104     let lines = readfile(tempfile)
105     if lines[-1] == '" vim: set ft=vim :'
106       call remove(lines, -1)
107     endif
108     call s:persist_special_windows(lines)
109     call extend(a:commands, lines)
110     return 1
111   finally
112     let &sessionoptions = ssop_save
113     call delete(tempfile)
114   endtry
115 endfunction
116 
117 " Integration between :mksession, :NERDTree and :Project. {{{3
118 
119 function! s:persist_special_windows(session) " {{{4
120   if exists(':NERDTree') == 2 && match(a:session, '\<NERD_tree_\d\+$') >= 0
121           \ || exists(':Project') == 2 && exists('g:proj_running')
122     let original_tabpage = tabpagenr()
123     let original_window = winnr()
124     try
125       if &sessionoptions =~ '\<tabpages\>'
126         tabdo call s:foreach_tabpage(a:session)
127       else
128         call s:foreach_tabpage(a:session)
129       endif
130     finally
131       execute 'tabnext' original_tabpage
132       execute original_window . 'wincmd w'
133       call s:jump_to_window(a:session, original_tabpage, original_window)
134     endtry
135   endif
136 endfunction
137 
138 function! s:foreach_tabpage(session) " {{{4
139   let original_window = winnr()
140   try
141     " FIXME Don't duplicate location list dictionaries!
142     let s:window_to_info = {}
143     let s:loclist_to_window = {}
144     windo call s:foreach_window(a:session)
145     if !empty(s:window_to_info)
146       for window in reverse(sort(keys(s:window_to_info)))
147         let [entries, title] = s:window_to_info[window]
148         call s:jump_to_window(a:session, tabpagenr(), window)
149         call add(a:session, 'bwipeout')
150         if entries == '[]'
151           call session#save_qflist(a:session)
152           call add(a:session, 'copen')
153         else
154           let other_window = s:loclist_to_window[entries]
155           call add(a:session, other_window . 'wincmd w')
156           call add(a:session, 'botright lopen')
157         endif
158         call add(a:session, 'let w:quickfix_title = ' . string(title))
159       endfor
160       call add(a:session, winrestcmd())
161     endif
162   finally
163     execute original_window . 'wincmd w'
164   endtry
165 endfunction
166 
167 function! s:foreach_window(session) " {{{4
168   if exists('b:NERDTreeRoot')
169     call s:save_plugin_window(a:session, 'NERDTree', b:NERDTreeRoot.path.str())
170   elseif exists('g:proj_running') && g:proj_running == bufnr('%')
171     call s:save_plugin_window(a:session, 'Project', expand('%:p'))
172   elseif has('quickfix')
173     let loclist = s:save_qflist(getloclist(0))
174     if &bt == 'quickfix' && &ft == 'qf' && &bh == 'wipe' && !&swf
175       " Remember which windows show a quickfix/location list.
176       let s:window_to_info[winnr()] = [loclist, w:quickfix_title]
177     elseif loclist != '[]'
178       " Restore location lists except inside location list windows.
179       call s:jump_to_window(a:session, tabpagenr(), winnr())
180       call add(a:session, 'call setloclist(0, ' . loclist . ')')
181       let s:loclist_to_window[loclist] = winnr()
182     endif
183   endif
184 endfunction
185 
186 function! s:save_plugin_window(session, command, argument) " {{{4
187   call s:jump_to_window(a:session, tabpagenr(), winnr())
188   call add(a:session, 'bwipeout')
189   let argument = fnamemodify(a:argument, ':~')
190   if &sessionoptions =~ '\<slash\>'
191     let argument = substitute(argument, '\', '/', 'g')
192   endif
193   call add(a:session, a:command . ' ' . fnameescape(argument))
194 endfunction
195 
196 function! s:jump_to_window(session, tabpage, window) " {{{4
197   if &sessionoptions =~ '\<tabpages\>'
198     call add(a:session, 'tabnext ' . a:tabpage)
199   endif
200   call add(a:session, a:window . 'wincmd w')
201 endfunction
202 
203 " Automatic commands to manage the default session. {{{1
204 
205 function! session#auto_load() " {{{2
206   " Check that the user has started Vim without editing any files.
207   if bufnr('$') == 1 && bufname('%') == '' && !&mod && getline(1, '$') == ['']
208     " Check whether a session matching the user-specified server name exists.
209     if v:servername !~ '^\cgvim\d*$'
210       for session in session#get_names()
211         if v:servername ==? session
212           execute 'OpenSession' fnameescape(session)
213           return
214         endif
215       endfor
216     endif
217     " Check whether the default session should be loaded.
218     let path = session#name_to_path('default')
219     if filereadable(path) && !s:session_is_locked(path)
220       let msg = "Do you want to restore your default editing session?"
221       if s:prompt(msg, 'g:session_autoload')
222         OpenSession default
223       endif
224     endif
225   endif
226 endfunction
227 
228 function! session#auto_save() " {{{2
229   if !v:dying
230     let name = s:get_name('', 0)
231     if name != '' && exists('s:session_is_dirty')
232       let msg = "Do you want to save your editing session before quitting Vim?"
233       if s:prompt(msg, 'g:session_autosave')
234         execute 'SaveSession' fnameescape(name)
235       endif
236     endif
237   endif
238 endfunction
239 
240 function! session#auto_unlock() " {{{2
241   let i = 0
242   while i < len(s:lock_files)
243     let lock_file = s:lock_files[i]
244     if delete(lock_file) == 0
245       call remove(s:lock_files, i)
246     else
247       let i += 1
248     endif
249   endwhile
250 endfunction
251 
252 function! session#auto_dirty_check() " {{{2
253   " This function is called each time a WinEnter event fires to detect when
254   " the current tab page is changed in some way. This enables the plug-in to
255   " not bother with the auto-save dialog when the session hasn't changed.
256   if v:this_session == ''
257     " Don't waste CPU time when no session is loaded.
258     return
259   elseif !exists('s:cached_layouts')
260     let s:cached_layouts = {}
261   else
262     " Clear non-existing tab pages from s:cached_layouts.
263     let last_tabpage = tabpagenr('$')
264     call filter(s:cached_layouts, 'v:key <= last_tabpage')
265   endif
266   let tabpagenr = tabpagenr()
267   let keys = ['tabpage:' . tabpagenr]
268   let buflist = tabpagebuflist()
269   for winnr in range(1, winnr('$'))
270     " Create a string that describes the state of the window {winnr}.
271     call add(keys, printf('width:%i,height:%i,buffer:%i',
272           \ winwidth(winnr), winheight(winnr), buflist[winnr - 1]))
273   endfor
274   let layout = join(keys, "\n")
275   let cached_layout = get(s:cached_layouts, tabpagenr, '')
276   if cached_layout != '' && cached_layout != layout
277     let s:session_is_dirty = 1
278   endif
279   let s:cached_layouts[tabpagenr] = layout
280 endfunction
281 
282 function! s:prompt(msg, var) " {{{2
283   if eval(a:var)
284     return 1
285   else
286     let format = "%s Note that you can permanently disable this dialog by adding the following line to your %s script:\n\n\t:let %s = 1"
287     let vimrc = has('win32') || has('win64') ? '~\_vimrc' : '~/.vimrc'
288     let prompt = printf(format, a:msg, vimrc, a:var)
289     return confirm(prompt, "&Yes\n&No", 1, 'Question') == 1
290   endif
291 endfunction
292 
293 " Commands that enable users to manage multiple sessions. {{{1
294 
295 function! session#open_cmd(name, bang) abort " {{{2
296   let name = s:select_name(s:unescape(a:name), 'restore')
297   if name != ''
298     let path = session#name_to_path(name)
299     if !filereadable(path)
300       let msg = "session.vim: The %s session at %s doesn't exist!"
301       call xolox#warning(msg, string(name), fnamemodify(path, ':~'))
302     elseif a:bang == '!' || !s:session_is_locked(path, 'OpenSession')
303       call session#close_cmd(a:bang, 1)
304       call s:lock_session(path)
305       execute 'source' fnameescape(path)
306       unlet! s:session_is_dirty
307       call xolox#message("session.vim: Opened %s session from %s.", string(name), fnamemodify(path, ':~'))
308     endif
309   endif
310 endfunction
311 
312 function! session#view_cmd(name) abort " {{{2
313   let name = s:select_name(s:get_name(s:unescape(a:name), 0), 'view')
314   if name != ''
315     let path = session#name_to_path(name)
316     if !filereadable(path)
317       let msg = "session.vim: The %s session at %s doesn't exist!"
318       call xolox#warning(msg, string(name), fnamemodify(path, ':~'))
319     else
320       execute 'tab drop' fnameescape(path)
321       call xolox#message("session.vim: Viewing session script %s.", fnamemodify(path, ':~'))
322     endif
323   endif
324 endfunction
325 
326 function! session#save_cmd(name, bang) abort " {{{2
327   let name = s:get_name(s:unescape(a:name), 1)
328   let path = session#name_to_path(name)
329   let friendly_path = fnamemodify(path, ':~')
330   if a:bang == '!' || !s:session_is_locked(path, 'SaveSession')
331     let lines = []
332     call session#save_session(lines, friendly_path)
333     let is_dos = has('dos16') || has('dos32')
334     let is_windows = has('win32') || has('win64')
335     if (is_dos || is_windows) && &ssop !~ '\<unix\>'
336       call map(lines, 'v:val . "\r"')
337     endif
338     if writefile(lines, path) != 0
339       let msg = "session.vim: Failed to save %s session to %s!"
340       call xolox#warning(msg, string(name), friendly_path)
341     else
342       let msg = "session.vim: Saved %s session to %s."
343       call xolox#message(msg, string(name), friendly_path)
344       let v:this_session = path
345       call s:lock_session(path)
346       unlet! s:session_is_dirty
347     endif
348   endif
349 endfunction
350 
351 function! session#delete_cmd(name, bang) " {{{2
352   let name = s:select_name(s:unescape(a:name), 'delete')
353   if name != ''
354     let path = session#name_to_path(name)
355     if !filereadable(path)
356       let msg = "session.vim: The %s session at %s doesn't exist!"
357       call xolox#warning(msg, string(name), fnamemodify(path, ':~'))
358     elseif a:bang == '!' || !s:session_is_locked(path, 'DeleteSession')
359       if delete(path) != 0
360         let msg = "session.vim: Failed to delete %s session at %s!"
361         call xolox#warning(msg, string(name), fnamemodify(path, ':~'))
362       else
363         call s:unlock_session(path)
364         let msg = "session.vim: Deleted %s session at %s."
365         call xolox#message(msg, string(name), fnamemodify(path, ':~'))
366       endif
367     endif
368   endif
369 endfunction
370 
371 function! session#close_cmd(bang, silent) abort " {{{2
372   let name = s:get_name('', 0)
373   if name != '' && exists('s:session_is_dirty')
374     let msg = "Do you want to save your current editing session before closing it?"
375     if s:prompt(msg, 'g:session_autosave')
376       SaveSession
377     endif
378     call s:unlock_session(session#name_to_path(name))
379   endif
380   if tabpagenr('$') > 1
381     execute 'tabonly' . a:bang
382   endif
383   if winnr('$') > 1
384     execute 'only' . a:bang
385   endif
386   execute 'enew' . a:bang
387   unlet! s:session_is_dirty
388   if v:this_session == ''
389     if !a:silent
390       let msg = "session.vim: Closed session."
391       call xolox#message(msg)
392     endif
393   else
394     if !a:silent
395       let msg = "session.vim: Closed session %s."
396       call xolox#message(msg, fnamemodify(v:this_session, ':~'))
397     endif
398     let v:this_session = ''
399   endif
400   return 1
401 endfunction
402 
403 function! session#restart_cmd(bang) abort " {{{2
404   let name = s:get_name('', 0)
405   if name == '' | let name = 'restart' | endif
406   execute 'SaveSession' . a:bang fnameescape(name)
407   let progname = shellescape(fnameescape(v:progname))
408   let servername = shellescape(fnameescape(name))
409   let command = progname . ' --servername ' . servername
410   let command .= ' -c ' . shellescape('OpenSession\! ' . fnameescape(name))
411   if has('win32') || has('win64')
412     execute '!start' command
413   else
414     let term = shellescape(fnameescape($TERM))
415     let encoding = "--cmd ':set enc=" . escape(&enc, '\ ') . "'"
416     silent execute '! TERM=' . term command encoding '&'
417   endif
418   execute 'CloseSession' . a:bang
419   quitall
420 endfunction
421 
422 " Miscellaneous functions. {{{1
423 
424 function! s:unescape(s) " {{{2
425   return substitute(a:s, '\\\(.\)', '\1', 'g')
426 endfunction
427 
428 function! s:select_name(name, action) " {{{2
429   if a:name != ''
430     return a:name
431   endif
432   let sessions = sort(session#get_names())
433   if empty(sessions)
434     return 'default'
435   elseif len(sessions) == 1
436     return sessions[0]
437   endif
438   let lines = copy(sessions)
439   for i in range(len(sessions))
440     let lines[i] = ' ' . (i + 1) . '. ' . lines[i]
441   endfor
442   redraw
443   sleep 100 m
444   echo "\nPlease select the session to " . a:action . ":"
445   sleep 100 m
446   let i = inputlist([''] + lines + [''])
447   return i >= 1 && i <= len(sessions) ? sessions[i - 1] : ''
448 endfunction
449 
450 function! s:get_name(name, use_default) " {{{2
451   let name = a:name
452   if name == '' && v:this_session != ''
453     let this_session_dir = fnamemodify(v:this_session, ':p:h')
454     if xolox#path#equals(this_session_dir, g:session_directory)
455       let name = session#path_to_name(v:this_session)
456     endif
457   endif
458   return name != '' ? name : a:use_default ? 'default' : ''
459 endfunction
460 
461 function! session#name_to_path(name) " {{{2
462   let directory = xolox#path#absolute(g:session_directory)
463   let filename = xolox#path#encode(a:name) . '.vim'
464   return xolox#path#merge(directory, filename)
465 endfunction
466 
467 function! session#path_to_name(path) " {{{2
468   return xolox#path#decode(fnamemodify(a:path, ':t:r'))
469 endfunction
470 
471 function! session#get_names() " {{{2
472   let directory = xolox#path#absolute(g:session_directory)
473   let filenames = split(glob(xolox#path#merge(directory, '*.vim')), "\n")
474   return map(filenames, 'fnameescape(xolox#path#decode(fnamemodify(v:val, ":t:r")))')
475 endfunction
476 
477 function! session#complete_names(arg, line, pos) " {{{2
478   return filter(session#get_names(), 'v:val =~ a:arg')
479 endfunction
480 
481 " Lock file management: {{{2
482 
483 if !exists('s:lock_files')
484   let s:lock_files = []
485 endif
486 
487 function! s:lock_session(session_path)
488   let lock_file = a:session_path . '.lock'
489   if writefile([v:servername], lock_file) == 0
490     if index(s:lock_files, lock_file) == -1
491       call add(s:lock_files, lock_file)
492     endif
493     return 1
494   endif
495 endfunction
496 
497 function! s:unlock_session(session_path)
498   let lock_file = a:session_path . '.lock'
499   if delete(lock_file) == 0
500     let idx = index(s:lock_files, lock_file)
501     if idx >= 0
502       call remove(s:lock_files, idx)
503     endif
504     return 1
505   endif
506 endfunction
507 
508 function! s:session_is_locked(session_path, ...)
509   let lock_file = a:session_path . '.lock'
510   if filereadable(lock_file)
511     let lines = readfile(lock_file)
512     if lines[0] !=? v:servername
513       if a:0 >= 1
514         let msg = "session.vim: The %s session is locked by another Vim instance named %s! Use :%s! to override."
515         call xolox#warning(msg, string(fnamemodify(a:session_path, ':t:r')), string(lines[0]), a:1)
516       endif
517       return 1
518     endif
519   endif
520 endfunction
521 
522 " vim: ts=2 sw=2 et