From aa27962cabc75458433088a4a4314b40b930567d Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Sat, 13 May 2017 22:43:06 -0700 Subject: [PATCH] Overhaul tracking of buffer <-> window association 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. --- CHANGELOG.md | 4 +- autoload/lsc/diagnostics.vim | 71 +++++++++++++++++++++++++++++++----- autoload/lsc/file.vim | 5 --- plugin/lsc.vim | 53 +++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 627b2933..942ac126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/autoload/lsc/diagnostics.vim b/autoload/lsc/diagnostics.vim index 750eca94..54ece98c 100644 --- a/autoload/lsc/diagnostics.vim +++ b/autoload/lsc/diagnostics.vim @@ -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. @@ -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 = {} @@ -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 @@ -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. diff --git a/autoload/lsc/file.vim b/autoload/lsc/file.vim index e0af4bb1..f279fa70 100644 --- a/autoload/lsc/file.vim +++ b/autoload/lsc/file.vim @@ -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') diff --git a/plugin/lsc.vim b/plugin/lsc.vim index 19f95f49..4f99c6e2 100644 --- a/plugin/lsc.vim +++ b/plugin/lsc.vim @@ -27,18 +27,65 @@ endfunction augroup LSC autocmd! - autocmd BufWinEnter,TabEnter,WinEnter,WinLeave * - \ call 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 IfEnabled('lsc#file#onOpen') autocmd TextChanged,TextChangedI,CompleteDone * \ call IfEnabled('lsc#file#onChange') - autocmd BufLeave * call IfEnabled('lsc#file#onLeave') + autocmd BufLeave * call IfEnabled('lsc#file#flushChanges') + autocmd CursorMoved * call IfEnabled('lsc#cursor#onMove') + autocmd TextChangedI * call IfEnabled('lsc#complete#textChanged') autocmd InsertCharPre * call IfEnabled('lsc#complete#insertCharPre') + autocmd VimLeave * call 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)