Skip to content

Commit

Permalink
Overhaul tracking of buffer <-> window association
Browse files Browse the repository at this point in the history
Leaving bad diagnostic highlights in other buffers, or failing to
highlight when switching to a buffer cause frequent bugs. The
pessimistic approach so far has been to update every visible window very
frequently - but this approach doesn't work as well for keeping each
window's location list up to date.

- Track an incrementing version number of the diagnostics associated
  with each files to tell when a window's location list is stale.
- After setting the location list store window local variables with the
  current file and version of diagnostics.
- When Setting the location list, first check if it needs any change.
  This reduces the cost of repeated updates.
- Add a `clear` method for diagnostics so they can be cleaned up for
  untracked filetypes.
- Replace the frequent `lsc#highlights#updatedDisplayed` with a more
  targeted method.
- Delete the lsc#file#onLeave method since highlights will always be
  corrected for the new buffer.

New approach for correcting state:

The previous autocmd did not have the `IfEnabled` condition so the
autoload file would always be parsed. Now there are checks for window
local variables for both highlights and location list to only call
autoloaded functions when necessary. The more precisely targetd updates
also mean that in the case of buffer changes only the current window
needs to be updated.

Some state can't be reliably updated across tabs, so when the tab
changes stay a bit pessimistic and do a sanity check across all windows.
  • Loading branch information
natebosch committed May 14, 2017
1 parent d1824b0 commit aa27962
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 19 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 0.1.2-dev
# 0.1.2

- Bug fix: Leave a jump in the jumplist when moving to a definition in the same
file
Expand All @@ -8,6 +8,8 @@
- Improve heuristics for start of completion range
- Flush file changes after completion
- Bug fix: Don't change window highlights when in select mode
- Bug fix: Location list is cleared when switching to a non-tracked filetype,
and kept up to date across windows and tabs showing the same buffer

# 0.1.1

Expand Down
71 changes: 61 additions & 10 deletions autoload/lsc/diagnostics.vim
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
" file path -> line number -> [diagnostic]
"
" Diagnostics are dictionaries with:
" 'group': The highlight group, like 'lscDiagnosticError'
" 'range': 1-based [start line, start column, length]
" 'message': The message to display
" 'type': Single letter representation of severity for location list
let s:file_diagnostics = {}
if !exists('s:file_diagnostsics')
" file path -> line number -> [diagnostic]
"
" Diagnostics are dictionaries with:
" 'group': The highlight group, like 'lscDiagnosticError'
" 'range': 1-based [start line, start column, length]
" 'message': The message to display
" 'type': Single letter representation of severity for location list
let s:file_diagnostics = {}

" file path -> incrementing version number
let s:diagnostic_versions = {}
endif

" Converts between an LSP diagnostic and the internal representation used for
" highlighting.
Expand Down Expand Up @@ -61,6 +66,13 @@ function! lsc#diagnostics#forFile(file_path) abort
return s:file_diagnostics[a:file_path]
endfunction

function! s:DiagnosticsVersion(file_path) abort
if !has_key(s:diagnostic_versions, a:file_path)
return 0
endif
return s:diagnostic_versions[a:file_path]
endfunction

function! lsc#diagnostics#setForFile(file_path, diagnostics) abort
call map(a:diagnostics, 'lsc#diagnostics#convert(v:val)')
let diagnostics_by_line = {}
Expand All @@ -71,6 +83,11 @@ function! lsc#diagnostics#setForFile(file_path, diagnostics) abort
call add(diagnostics_by_line[diagnostic.range[0]], diagnostic)
endfor
let s:file_diagnostics[a:file_path] = diagnostics_by_line
if has_key(s:diagnostic_versions, a:file_path)
let s:diagnostic_versions[a:file_path] += 1
else
let s:diagnostic_versions[a:file_path] = 1
endif
call lsc#highlights#updateDisplayed()
call lsc#diagnostics#updateLocationList(a:file_path)
endfunction
Expand All @@ -84,11 +101,45 @@ function! lsc#diagnostics#updateLocationList(file_path) abort
call add(items, s:locationListItem(bufnr, diagnostic))
endfor
endfor
for window in lsc#util#windowsForFile(a:file_path)
call setloclist(window, items)
let diagnostics_version = s:DiagnosticsVersion(a:file_path)
for window_id in lsc#util#windowsForFile(a:file_path)
if !s:WindowIsCurrent(window_id, a:file_path, diagnostics_version)
call setloclist(window_id, items)
call s:MarkManagingLocList(window_id, a:file_path, diagnostics_version)
else
endif
endfor
endfunction

function! s:MarkManagingLocList(window_id, file_path, version) abort
let window_info = getwininfo(a:window_id)[0]
let tabnr = window_info.tabnr
let winnr = window_info.winnr
call settabwinvar(tabnr, winnr, 'lsc_diagnostics_file', a:file_path)
call settabwinvar(tabnr, winnr, 'lsc_diagnostics_version', a:version)
endfunction

" Whether the location list has the most up to date diagnostics.
"
" Multiple events can cause the location list for a window to get updated. Track
" the currently held file and version for diagnostics and block updates if they
" are already current.
function! s:WindowIsCurrent(window_id, file_path, version) abort
let window_info = getwininfo(a:window_id)[0]
let tabnr = window_info.tabnr
let winnr = window_info.winnr
return gettabwinvar(tabnr, winnr, 'lsc_diagnostics_version', -1) == a:version
\ && gettabwinvar(tabnr, winnr, 'lsc_diagnostics_file', '') == a:file_path
endfunction


" Remove the LSC controlled location list for the current window.
function! lsc#diagnostics#clear() abort
call setloclist(0, [])
unlet w:lsc_diagnostics_version
unlet w:lsc_diagnostics_file
endfunction

" Finds the first diagnostic which is under the cursor on the current line. If
" no diagnostic is directly under the cursor returns the last seen diagnostic
" on this line.
Expand Down
5 changes: 0 additions & 5 deletions autoload/lsc/file.vim
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ function! lsc#file#onChange() abort
\ timer_start(500, 'lsc#file#flushChanges', {'repeat': 1})
endfunction

function! lsc#file#onLeave() abort
call lsc#file#flushChanges()
call lsc#highlights#clear()
endfunction

" Changes are flushed after 500ms of inactivity or before leaving the buffer.
function! lsc#file#flushChanges(...) abort
if !exists('b:lsc_flush_timer')
Expand Down
53 changes: 50 additions & 3 deletions plugin/lsc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,65 @@ endfunction

augroup LSC
autocmd!
autocmd BufWinEnter,TabEnter,WinEnter,WinLeave *
\ call <SID>IfEnabled('lsc#highlights#updateDisplayed')
" Some state which is logically owned by a buffer is attached to the window in
" practice and needs to be manage manually:
"
" 1. Diagnostic highlights
" 2. Diagnostic location list
"
" The `BufWinEnter` event indicates most times when the buffer <-> window
" relationship can change. There are some exceptions where this event is not
" fired such as `:split` and `:lopen` so `WinEnter` is used as a fallback with
" a block to ensure it only happens once.
autocmd BufWinEnter * call LSCEnsureCurrentWindowState()
autocmd WinEnter * call timer_start(1, 'LSCOnWinEnter')

" Window local state is only correctly maintained for the current tab.
autocmd TabEnter * call lsc#util#winDo('call LSCEnsureCurrentWindowState()')

autocmd BufNewFile,BufReadPost * call <SID>IfEnabled('lsc#file#onOpen')
autocmd TextChanged,TextChangedI,CompleteDone *
\ call <SID>IfEnabled('lsc#file#onChange')
autocmd BufLeave * call <SID>IfEnabled('lsc#file#onLeave')
autocmd BufLeave * call <SID>IfEnabled('lsc#file#flushChanges')

autocmd CursorMoved * call <SID>IfEnabled('lsc#cursor#onMove')

autocmd TextChangedI * call <SID>IfEnabled('lsc#complete#textChanged')
autocmd InsertCharPre * call <SID>IfEnabled('lsc#complete#insertCharPre')

autocmd VimLeave * call <SID>OnVimQuit()
augroup END

" Set window local state only if this is a brand new window which has not
" already been initialized for LSC.
"
" This function must be called on a delay since critical values like
" `expand('%')` and `&filetype` are not correctly set when the event fires. The
" delay means that in the cases where `BufWinEnter` actually runs this will run
" later and do nothing.
function! LSCOnWinEnter(timer) abort
if exists('w:lsc_window_initialized')
return
endif
call LSCEnsureCurrentWindowState()
endfunction

" Update or clear state local to the current window.
function! LSCEnsureCurrentWindowState() abort
let w:lsc_window_initialized = v:true
if !has_key(g:lsc_server_commands, &filetype)
if exists('w:lsc_diagnostic_matches')
call lsc#highlights#clear()
endif
if exists('w:lsc_diagnostics_version')
call lsc#diagnostics#clear()
endif
return
endif
call lsc#highlights#update()
call lsc#diagnostics#updateLocationList(expand('%:p'))
endfunction

" Run `function` if LSC is enabled for the current filetype.
function! s:IfEnabled(function) abort
if has_key(g:lsc_server_commands, &filetype)
Expand Down

0 comments on commit aa27962

Please sign in to comment.