From 48cbf7bfc5eeb3c1f4fbd49953a49321021b74e1 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 27 Jan 2018 16:59:15 +0000 Subject: [PATCH 01/39] Remove `shell=True` for python 3.5+ subprocess * `shell=True` means arguments are passed to the *shell* process, rather than RTags. * This breaks `get_diagnostics`, because `rc` just gets a blank list of arguments. --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 6f120a5b..d7fb5dce 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -42,7 +42,7 @@ def run_rc_command(arguments, content = None): out = out.decode(encoding) if not err is None: err = err.decode(encoding) - +o elif sys.version_info.major == 3 and sys.version_info.minor < 5: r = subprocess.Popen( cmdline.split(), From b596af75dc6f1f3916843a9017c1f524a937e1c2 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 27 Jan 2018 17:45:42 +0000 Subject: [PATCH 02/39] Support `--diagnose-all` * Add `rtags#DiagnosticsAll` function (default keymap `rD`) to show diagnostics for all files in the quickfix list. * Only files that are currently open in buffers will have their gutter updated with `E`/`W` symbols. --- plugin/rtags.vim | 17 ++++++---- plugin/vimrtags.py | 82 +++++++++++++++++++++++++++------------------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index ebcaa5f8..210292dc 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -59,7 +59,7 @@ endif if g:rtagsAutoLaunchRdm call system(g:rtagsRcCmd." -w") - if v:shell_error != 0 + if v:shell_error != 0 call system(g:rtagsRdmCmd." --daemon > /dev/null") end end @@ -96,6 +96,7 @@ if g:rtagsUseDefaultMappings == 1 noremap rC :call rtags#FindSuperClasses() noremap rc :call rtags#FindSubClasses() noremap rd :call rtags#Diagnostics() + noremap rD :call rtags#DiagnosticsAll() endif let s:script_folder_path = escape( expand( ':p:h' ), '\' ) @@ -534,7 +535,7 @@ function! rtags#saveLocation() endfunction function! rtags#pushToStack(location) - let jumpListLen = len(g:rtagsJumpStack) + let jumpListLen = len(g:rtagsJumpStack) if jumpListLen > g:rtagsJumpStackMaxSize call remove(g:rtagsJumpStack, 0) endif @@ -727,7 +728,7 @@ function! rtags#ExecuteHandlers(output, handlers) return endtry endif - endfor + endfor endfunction function! rtags#ExecuteThen(args, handlers) @@ -880,6 +881,10 @@ function! rtags#Diagnostics() return s:Pyeval("vimrtags.get_diagnostics()") endfunction +function! rtags#DiagnosticsAll() + return s:Pyeval("vimrtags.get_diagnostics_all()") +endfunction + " " This function assumes it is invoked from insert mode " @@ -887,7 +892,7 @@ function! rtags#CompleteAtCursor(wordStart, base) let flags = "--synchronous-completions -l" let file = expand("%:p") let pos = getpos('.') - let line = pos[1] + let line = pos[1] let col = pos[2] if index(['.', '::', '->'], a:base) != -1 @@ -932,7 +937,7 @@ function! s:Pyeval( eval_string ) return pyeval( a:eval_string ) endif endfunction - + function! s:RcExecuteJobCompletion() call rtags#SetJobStateFinish() if ! empty(b:rtags_state['stdout']) && mode() == 'i' @@ -1062,7 +1067,7 @@ endf " - invoke completion through rc " - filter out options that start with meth (in this case). " - show completion options -" +" " Reason: rtags returns all options regardless of already type method name " portion """ diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index d7fb5dce..7463b35b 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -34,15 +34,14 @@ def run_rc_command(arguments, content = None): cmdline.split(), input = content, stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - shell = True + stderr = subprocess.PIPE ) out, err = r.stdout, r.stderr if not out is None: out = out.decode(encoding) if not err is None: err = err.decode(encoding) -o + elif sys.version_info.major == 3 and sys.version_info.minor < 5: r = subprocess.Popen( cmdline.split(), @@ -134,14 +133,21 @@ def display_locations(errors, buffer): max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) height = min(max_height, len(errors)) - if int(get_rtags_variable('UseLocationList')) == 1: + if buffer is not None and int(get_rtags_variable('UseLocationList')) == 1: vim.eval('setloclist(%d, %s)' % (buffer.number, error_data)) vim.command('lopen %d' % height) else: vim.eval('setqflist(%s)' % error_data) vim.command('copen %d' % height) -def display_diagnostics_results(data, buffer): +def find_buffer(name): + for buffer in vim.buffers: + if buffer.name == name: + return buffer + else: + return None + +def display_diagnostics_results(data, buffer=None): data = json.loads(data) logging.debug(data) @@ -152,48 +158,58 @@ def display_diagnostics_results(data, buffer): if check_style == None: return - filename, errors = list(check_style.items())[0] quickfix_errors = [] vim.command('sign define fixit text=F texthl=FixIt') vim.command('sign define warning text=W texthl=Warning') vim.command('sign define error text=E texthl=Error') - for i, e in enumerate(errors): - if e['type'] == 'skipped': - continue - - # strip error prefix - s = ' Issue: ' - index = e['message'].find(s) - if index != -1: - e['message'] = e['message'][index + len(s):] - error_type = 'E' if e['type'] == 'error' else 'W' - quickfix_errors.append({'lnum': e['line'], 'col': e['column'], - 'nr': i, 'text': e['message'], 'filename': filename, - 'type': error_type}) - cmd = 'sign place %d line=%s name=%s file=%s' % (i + 1, e['line'], e['type'], filename) - vim.command(cmd) + for filename, errors in check_style.items(): + for i, e in enumerate(errors): + if e['type'] == 'skipped': + continue + + # strip error prefix + s = ' Issue: ' + index = e['message'].find(s) + if index != -1: + e['message'] = e['message'][index + len(s):] + error_type = 'E' if e['type'] == 'error' else 'W' + quickfix_errors.append({'lnum': e['line'], 'col': e['column'], + 'nr': i, 'text': e['message'], 'filename': filename, + 'type': error_type}) + if find_buffer(filename) is not None: + cmd = 'sign place %d line=%s name=%s file=%s' % ( + i + 1, e['line'], e['type'], filename) + vim.command(cmd) display_locations(quickfix_errors, buffer) def get_diagnostics(): filename = vim.eval('s:file') - for buffer in vim.buffers: - if buffer.name == filename: - is_modified = bool(int((vim.eval('getbufvar(%d, "&mod")' % buffer.number)))) - cmd = '--diagnose %s --synchronous-diagnostics --json' % filename + buffer = find_buffer(filename) + if buffer is None: + return None + is_modified = bool(int((vim.eval('getbufvar(%d, "&mod")' % buffer.number)))) + cmd = '--diagnose %s --synchronous-diagnostics --json' % filename - content = '' - if is_modified: - content = '\n'.join([x for x in buffer]) - cmd += ' --unsaved-file=%s:%d' % (filename, len(content)) + content = '' + if is_modified: + content = '\n'.join([x for x in buffer]) + cmd += ' --unsaved-file=%s:%d' % (filename, len(content)) - content = run_rc_command(cmd, content) - if content == None: - return None + content = run_rc_command(cmd, content) + if content == None: + return None - display_diagnostics_results(content, buffer) + display_diagnostics_results(content, buffer) return 0 + +def get_diagnostics_all(): + content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') + if content is None: + return None + display_diagnostics_results(content) + return 0 From 317426a741939a1d61cb407e9ad01a10be3a16b8 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 27 Jan 2018 17:53:38 +0000 Subject: [PATCH 03/39] Sort errors above warnings in quickfix/location list --- plugin/vimrtags.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 7463b35b..61764323 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -129,6 +129,7 @@ def display_locations(errors, buffer): if len(errors) == 0: return + errors = sorted(errors, key=lambda e: e['type']) error_data = json.dumps(errors) max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) height = min(max_height, len(errors)) From 94b6a77b17c899b8ec51bee2cc2b4f8bff889458 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 27 Jan 2018 18:44:51 +0000 Subject: [PATCH 04/39] Support location list for `DiagnosticsAll` --- plugin/rtags.vim | 1 + plugin/vimrtags.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 210292dc..fe66f116 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -882,6 +882,7 @@ function! rtags#Diagnostics() endfunction function! rtags#DiagnosticsAll() + let s:file = expand("%:p") return s:Pyeval("vimrtags.get_diagnostics_all()") endfunction diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 61764323..ec38d45c 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -134,21 +134,23 @@ def display_locations(errors, buffer): max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) height = min(max_height, len(errors)) - if buffer is not None and int(get_rtags_variable('UseLocationList')) == 1: + if int(get_rtags_variable('UseLocationList')) == 1: vim.eval('setloclist(%d, %s)' % (buffer.number, error_data)) vim.command('lopen %d' % height) else: vim.eval('setqflist(%s)' % error_data) vim.command('copen %d' % height) -def find_buffer(name): +def find_buffer(filename=None): + if filename is None: + filename = vim.eval('s:file') for buffer in vim.buffers: - if buffer.name == name: + if buffer.name == filename: return buffer else: return None -def display_diagnostics_results(data, buffer=None): +def display_diagnostics_results(data, buffer): data = json.loads(data) logging.debug(data) @@ -187,9 +189,7 @@ def display_diagnostics_results(data, buffer=None): display_locations(quickfix_errors, buffer) def get_diagnostics(): - filename = vim.eval('s:file') - - buffer = find_buffer(filename) + buffer = find_buffer() if buffer is None: return None is_modified = bool(int((vim.eval('getbufvar(%d, "&mod")' % buffer.number)))) @@ -209,8 +209,11 @@ def get_diagnostics(): return 0 def get_diagnostics_all(): + buffer = find_buffer() + if buffer is None: + return None content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') if content is None: return None - display_diagnostics_results(content) + display_diagnostics_results(content, buffer) return 0 From ed7187c4371972dfa16ddf7b8043a8d69e05a0c3 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 27 Jan 2018 19:45:36 +0000 Subject: [PATCH 05/39] Improve error message handling * Assume there is always an active buffer. * Echo messages on success/fail for getting diagnostics. * Detect unindexed file "error" from RTags and handle it as if it was a non-zero exit code. --- plugin/vimrtags.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index ec38d45c..f03642c5 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -8,7 +8,8 @@ import logging tempdir = tempfile.gettempdir() -logging.basicConfig(filename='%s/vim-rtags-python.log' % tempdir,level=logging.DEBUG) +logfile = '%s/vim-rtags-python.log' % tempdir +logging.basicConfig(filename=logfile, level=logging.DEBUG) def get_identifier_beginning(): line = vim.eval('s:line') @@ -67,6 +68,9 @@ def run_rc_command(arguments, content = None): if r.returncode != 0: logging.debug(err) return None + elif "is not indexed" in out: + logging.debug(out) + return None return out @@ -159,7 +163,7 @@ def display_diagnostics_results(data, buffer): # There are no errors if check_style == None: - return + return message('No errors found') quickfix_errors = [] @@ -186,14 +190,16 @@ def display_diagnostics_results(data, buffer): i + 1, e['line'], e['type'], filename) vim.command(cmd) - display_locations(quickfix_errors, buffer) + # There are no errors + if not quickfix_errors: + return message('No errors found') + else: + display_locations(quickfix_errors, buffer) def get_diagnostics(): buffer = find_buffer() - if buffer is None: - return None is_modified = bool(int((vim.eval('getbufvar(%d, "&mod")' % buffer.number)))) - cmd = '--diagnose %s --synchronous-diagnostics --json' % filename + cmd = '--diagnose %s --synchronous-diagnostics --json' % buffer.name content = '' if is_modified: @@ -202,18 +208,21 @@ def get_diagnostics(): content = run_rc_command(cmd, content) if content == None: - return None + return error('Failed to get diagnostics for "%s"' % buffer.name) display_diagnostics_results(content, buffer) return 0 def get_diagnostics_all(): - buffer = find_buffer() - if buffer is None: - return None content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') if content is None: - return None - display_diagnostics_results(content, buffer) + return error('Failed to get diagnostics') + display_diagnostics_results(content, find_buffer()) return 0 + +def error(msg): + message('%s: see log file at "%s" for more information' % (msg, logfile)) + +def message(msg): + vim.command("echom '%s'" % msg) From 70f126bf364230b22f58008f57903f03d9870964 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Fri, 23 Feb 2018 22:32:35 +0000 Subject: [PATCH 06/39] Add automatic diagnostics via complete refactor of python code + extras * Add automatic diagnostics for files: - Enabled by default, toggled using `g:rtagsAutoDiagnostics`. - Relies mostly on `CursorHold` event, rather than any clever async/polling. - Use nice title in location list - dual use as a tag to determine whether to update list at top of stack or create a new one. * Show current line's diagnostic message in message window. * Add support for RTags fixits - Applies only to current buffer. - Opens loclist with list of fixits applied. - Default keymap `rx`. * Support preferring omnifunc for cpp filetypes - Preferred by default, toggled using `g:rtagsCppOmnifunc`. - Otherwise sets completefunc, but still only if it's available to be used. * Improve rendering of diagnostic gutter signs. - Make the background of the sign "transparent" (i.e. match the background of the sign column). Took the algorithm from vim-gitgutter, just converted to python. - Keep a record of placed signs and only unplace those when updating diagnostics, so we don't splat other plugins. * Improved error messages. - Jump to definition/symbol/etc will notify if file is not indexed. - Show errors if rc returns no valid content. * Improved completion popup text to include parent and signature (i.e. class and function with arguments). * Support logging rdm output to a file if `g:rtagsAutoLaunchRdm` is used. - Defaults to /dev/null. Set path using `g:rtagsRdmLogFile`. * Improve python logging - Don't use root logger, so we don't splat config of other plugins. - Include timestamp and log level in messages. * Group buffer related commands and events into a `Buffer` class. - Maintain a cache of `Buffer` objects wrapping vim `buffer` objects, keyed by buffer number, and periodically lazily cleaned of wiped-out buffers. - Get the RTags "project" used for the current buffer, for use in determining if the index has been updated since last checked. - `Buffer.on_idle` is triggered on `CursorHold` autocmd to update the diagnostics or reindex the (dirty) buffer. * Wrap quickfix/gutter sign RTags diagnostics using `Diagnostic` class, with convenience methods to convert from/to rtags/quickfix+signs. * Wrap RTags project in `Project` class, with method to determine if it has been updated on disk. * Fix utf8 encoding when sending buffer contents to rc over stdin in python 3.5+. * Handy util function to get string output of a command (as opposed to result of evaluating an expression) `rtags#getCommandOutput`. --- plugin/rtags.vim | 72 ++++- plugin/vimrtags.py | 636 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 617 insertions(+), 91 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index fe66f116..3c16d398 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -27,6 +27,10 @@ if !exists("g:rtagsRdmCmd") let g:rtagsRdmCmd = "rdm" endif +if !exists("g:rtagsRdmLogFile") + let g:rtagsRdmLogFile = "/dev/null" +endif + if !exists("g:rtagsAutoLaunchRdm") let g:rtagsAutoLaunchRdm = 0 endif @@ -57,10 +61,18 @@ if !exists("g:rtagsMaxSearchResultWindowHeight") let g:rtagsMaxSearchResultWindowHeight = 10 endif +if !exists("g:rtagsAutoDiagnostics") + let g:rtagsAutoDiagnostics = 1 +endif + +if !exists("g:rtagsCppOmnifunc") + let g:rtagsCppOmnifunc = 1 +endif + if g:rtagsAutoLaunchRdm call system(g:rtagsRcCmd." -w") if v:shell_error != 0 - call system(g:rtagsRdmCmd." --daemon > /dev/null") + call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLogFile) end end @@ -96,7 +108,8 @@ if g:rtagsUseDefaultMappings == 1 noremap rC :call rtags#FindSuperClasses() noremap rc :call rtags#FindSubClasses() noremap rd :call rtags#Diagnostics() - noremap rD :call rtags#DiagnosticsAll() + noremap rD :call rtags#DiagnosticsAll() + noremap rx :call rtags#ApplyFixit() endif let s:script_folder_path = escape( expand( ':p:h' ), '\' ) @@ -482,6 +495,10 @@ function! rtags#JumpToHandler(results, args) if len(results) > 1 call rtags#DisplayResults(results) elseif len(results) == 1 + if results[0] == "Not indexed" + echom "Failed to jump - file is not indexed" + return + endif let [location; symbol_detail] = split(results[0], '\s\+') let [jump_file, lnum, col; rest] = split(location, ':') @@ -877,13 +894,50 @@ function! rtags#FindSymbolsOfWordUnderCursor() endfunction function! rtags#Diagnostics() - let s:file = expand("%:p") - return s:Pyeval("vimrtags.get_diagnostics()") + return s:Pyeval("vimrtags.Buffer.current().show_diagnostics_list()") endfunction function! rtags#DiagnosticsAll() - let s:file = expand("%:p") - return s:Pyeval("vimrtags.get_diagnostics_all()") + return s:Pyeval("vimrtags.Buffer.show_all_diagnostics()") +endfunction + +function! rtags#ApplyFixit() + return s:Pyeval("vimrtags.Buffer.current().apply_fixits()") +endfunction + +function! rtags#NotifyEdit() + return s:Pyeval("vimrtags.Buffer.current().on_edit()") +endfunction + +function! rtags#NotifyWrite() + return s:Pyeval("vimrtags.Buffer.current().on_write()") +endfunction + +function! rtags#NotifyIdle() + return s:Pyeval("vimrtags.Buffer.current().on_idle()") +endfunction + +function! rtags#NotifyCursorMoved() + return s:Pyeval("vimrtags.Buffer.current().on_cursor_moved()") +endfunction + +if g:rtagsAutoDiagnostics == 1 + augroup rtags_auto_diagnostics + autocmd! + autocmd BufWritePost *.cpp,*.c,*.hpp,*.h call rtags#NotifyWrite() + autocmd TextChanged,TextChangedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyEdit() + autocmd CursorHold,CursorHoldI,BufEnter *.cpp,*.c,*.hpp,*.h call rtags#NotifyIdle() + autocmd CursorMoved,CursorMovedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyCursorMoved() + augroup END +endif + +" Generic function to get output of a command. +" Used in python for things that can't be read directly via vim.eval(...) +function! rtags#getCommandOutput(cmd_txt) abort + redir => output + silent execute a:cmd_txt + redir END + return output endfunction " @@ -1098,7 +1152,11 @@ function! s:RtagsCompleteFunc(findstart, base, async) endif endfunction -if &completefunc == "" +" Prefer omnifunc, if enabled. +if g:rtagsCppOmnifunc == 1 + autocmd Filetype cpp setlocal omnifunc=RtagsCompleteFunc +" Override completefunc if it's available to be used. +elseif &completefunc == "" set completefunc=RtagsCompleteFunc endif diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index f03642c5..b6141056 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -5,18 +5,32 @@ import os import sys import tempfile - +import re import logging -tempdir = tempfile.gettempdir() -logfile = '%s/vim-rtags-python.log' % tempdir -logging.basicConfig(filename=logfile, level=logging.DEBUG) +from time import time + + +logfile = '%s/vim-rtags-python.log' % tempfile.gettempdir() +loglevel = logging.DEBUG +logger = logging.getLogger(__name__) + + +def configure_logger(): + handler = logging.FileHandler(logfile) + formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(loglevel) + +configure_logger() + def get_identifier_beginning(): line = vim.eval('s:line') column = int(vim.eval('s:start')) - logging.debug(line) - logging.debug(column) + logger.debug(line) + logger.debug(column) while column >= 0 and (line[column].isalnum() or line[column] == '_'): column -= 1 @@ -30,10 +44,11 @@ def run_rc_command(arguments, content = None): encoding = 'utf-8' out = None err = None + logger.debug("RTags command: %s" % cmdline.split()) if sys.version_info.major == 3 and sys.version_info.minor >= 5: r = subprocess.run( cmdline.split(), - input = content, + input = content and content.encode("utf-8"), stdout = subprocess.PIPE, stderr = subprocess.PIPE ) @@ -66,10 +81,10 @@ def run_rc_command(arguments, content = None): out, err = r.communicate(input=content) if r.returncode != 0: - logging.debug(err) + logger.debug(err) return None elif "is not indexed" in out: - logging.debug(out) + logger.debug(out) return None return out @@ -80,7 +95,7 @@ def get_rtags_variable(name): def parse_completion_result(data): result = json.loads(data) - logging.debug(result) + logger.debug(result) completions = [] for c in result['completions']: @@ -99,7 +114,7 @@ def parse_completion_result(data): elif k == 'TypedefDecl' or k == 'StructDecl' or k == 'EnumConstantDecl': kind = 't' - match = {'menu': c['completion'], 'word': c['completion'], 'kind': kind} + match = {'menu': " ".join([c['parent'], c['signature']]), 'word': c['completion'], 'kind': kind} completions.append(match) return completions @@ -111,7 +126,7 @@ def send_completion_request(): prefix = vim.eval('s:prefix') for buffer in vim.buffers: - logging.debug(buffer.name) + logger.debug(buffer.name) if buffer.name == filename: lines = [x for x in buffer] content = '\n'.join(lines[:line - 1] + [lines[line - 1] + prefix] + lines[line:]) @@ -122,6 +137,7 @@ def send_completion_request(): cmd += ' --code-complete-prefix %s' % prefix content = run_rc_command(cmd, content) + logger.debug("Got completion: %s" % content) if content == None: return None @@ -129,100 +145,552 @@ def send_completion_request(): assert False -def display_locations(errors, buffer): - if len(errors) == 0: - return - - errors = sorted(errors, key=lambda e: e['type']) - error_data = json.dumps(errors) - max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) - height = min(max_height, len(errors)) - if int(get_rtags_variable('UseLocationList')) == 1: - vim.eval('setloclist(%d, %s)' % (buffer.number, error_data)) +class Buffer(object): + _cache = {} + _cache_last_cleaned = time() + _CACHE_CLEAN_PERIOD = 30 + _DIAGNOSTICS_CHECK_PERIOD = 5 + _DIAGNOSTICS_LIST_TITLE = "RTags diagnostics" + _DIAGNOSTICS_ALL_LIST_TITLE = "RTags diagnostics for all files" + _FIXITS_LIST_TITLE = "RTags fixits applied" + + @staticmethod + def current(): + return Buffer.get(vim.current.buffer.number) + + @staticmethod + def find(name): + """ Find a Buffer by vim buffer (file)name. + """ + for buffer in vim.buffers: + if buffer.name == name: + return Buffer.get(buffer.number) + else: + return None + + @staticmethod + def get(id_): + """ Get a Buffer wrapping a vim buffer, by id_. + + Create the Buffer if necessary. + """ + # Get from cache or create if it's not there. + buff = Buffer._cache.get(id_) + if buff is not None: + return buff + + # Periodically clean closed buffers + if time() - Buffer._cache_last_cleaned > Buffer._CACHE_CLEAN_PERIOD: + logger.debug("Cleaning invalid buffers") + for id_ in Buffer._cache.keys(): + if not Buffer._cache[id_]._vimbuffer.valid: + logger.debug("Cleaning invalid buffer %s" % id_) + del Buffer._cache[id_] + Buffer._cache_last_cleaned = time() + + buff = Buffer(vim.buffers[id_]) + Buffer._cache[id_] = buff + return buff + + @staticmethod + def show_all_diagnostics(): + """ Get all diagnostics for all files and show in quickfix list. + """ + # Get the diagnostics from rtags. + content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') + if content is None: + return error('Failed to get diagnostics') + data = json.loads(content) + + # Construct Diagnostic objects from rtags errors. + diagnostics = [] + for filename, errors in data['checkStyle'].items(): + diagnostics += Diagnostic.from_rtags_errors(filename, errors) + + if not diagnostics: + return message("No errors to show") + + # Convert list of Diagnostic objects to quickfix-compatible dict. + height, lines = Diagnostic.to_qlist_errors(diagnostics) + + # Show diagnostics in location list or quickfix list, depending on user preference. + if int(get_rtags_variable('UseLocationList')) == 1: + vim.eval( + 'setloclist(%d, [], " ", {"items": %s, "title": "%s"})' + % (vim.current.window.number, lines, Buffer._DIAGNOSTICS_ALL_LIST_TITLE) + ) + vim.command('lopen %d' % height) + else: + vim.eval( + 'setqflist([], " ", {"items": %s, "title": "%s"})' + % (lines, Buffer._DIAGNOSTICS_ALL_LIST_TITLE) + ) + vim.command('copen %d' % height) + + def __init__(self, buffer): + self._vimbuffer = buffer + self._signs = [] + self._diagnostics = {} + self._last_diagnostics_time = 0 + self._line_num_last = -1 + self._is_line_diagnostic_shown = False + self._is_dirty = False + + self._project = Project.get(self._vimbuffer.name) + + def on_write(self): + self._is_dirty = False + + def on_edit(self): + """ Mark buffer dirty, potentially flagging a reindex using unsaved content. + """ + if self._project is None: + return + self._is_dirty = True + + def on_idle(self): + """ Reindex or update diagnostics for this buffer. + """ + if self._project is None: + return + + if self._is_dirty: + # `TextChange` autocmd also triggers just by switching buffers, so we have to be sure. + is_really_dirty = vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number) + else: + is_really_dirty = False + + if is_really_dirty: + # Reindex dirty buffer - no point refreshing diagnostics at this point. + logger.debug("Buffer %s needs dirty reindex" % self._vimbuffer.number) + self._rtags_dirty_reindex() + # No point getting diagnostics any time soon. + self._last_diagnostics_time = time() + + elif self._last_diagnostics_time < self._project.last_updated_time(): + # Update diagnostics signs/list. + logger.debug( + "Project updated, checking for updated diagnostics for %s" % self._vimbuffer.name + ) + self._update_diagnostics() + + def on_cursor_moved(self): + """ Print diagnostic info for current line + """ + if self._project is None: + return + line_num = vim.current.window.cursor[0] + if line_num != self._line_num_last: + self._line_num_last = line_num + diagnostic = self._diagnostics.get(line_num) + if diagnostic is not None: + print(diagnostic.text) + self._is_line_diagnostic_shown = True + elif self._is_line_diagnostic_shown: + # If there is no diagnostic for this line, clear the message. + print("") + # Make sure we only clear the message when we've recently shown one, so we don't + # splat other plugins messages. + self._is_line_diagnostic_shown = False + + def show_diagnostics_list(self): + """ Show diagnostics for this buffer in location/quickfix list. + + Diagnostics are updated before being displayed, just in case. + """ + if self._project is None: + return invalid_buffer_message(self._vimbuffer.name) + + self._update_diagnostics(open_loclist=True) + if not self._diagnostics: + return message("No errors to display") + + def apply_fixits(self): + """ Fetch fixits from rtags, apply them, and show changed lines in quickfix/location list. + """ + if self._project is None: + return invalid_buffer_message(self._vimbuffer.name) + # Not safe to apply fixits if file is not indexed, make extra sure it is. + if ( + self._rtags_is_reindexing() or ( + int(vim.eval("g:rtagsAutoDiagnostics")) and + self._last_diagnostics_time < self._project.last_updated_time() + ) + ): + return message( + "File is currently reindexing, so fixits are unsafe, please try again shortly" + ) + + # Get the fixits from rtags. + content = run_rc_command('--fixits %s' % self._vimbuffer.name) + if content is None: + return error("Failed to fetch fixits") + content = content.strip() + if not content: + return message("No fixits to apply to this file") + + logger.debug("Fixits found:\n%s" % content) + fixits = content.split('\n') + lines = [] + + # Loop over fixits applying each in turn and record what was done for qlist/loclist. + for i, fixit in enumerate(fixits): + # Regex parse the fixits. + fixit = re.match("^(\d+):(\d+) (\d+) (.+)$", fixit) + if fixit is None: + continue + line_num = int(fixit.group(1)) + line_idx = line_num - 1 + char_num = int(fixit.group(2)) + char_idx = char_num - 1 + length = int(fixit.group(3)) + text = fixit.group(4) + + # Edit the buffer to apply the fixit. + self._vimbuffer[line_idx] = ( + self._vimbuffer[line_idx][:char_idx] + text + + self._vimbuffer[line_idx][char_idx + length:] + ) + # Construct quickfix list compatible dict. + lines.append({ + 'lnum': line_num, 'col': char_num, 'text': text, 'filename': self._vimbuffer.name + }) + + # Calculate height of quickfix list. + max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) + height = min(max_height, len(lines)) + lines = json.dumps(lines) + + # Render lines fixed to location list and open it. + vim.eval( + 'setloclist(%d, [], " ", {"items": %s, "title": "%s"})' + % (vim.current.window.number, lines, Buffer._FIXITS_LIST_TITLE) + ) vim.command('lopen %d' % height) - else: - vim.eval('setqflist(%s)' % error_data) - vim.command('copen %d' % height) - -def find_buffer(filename=None): - if filename is None: - filename = vim.eval('s:file') - for buffer in vim.buffers: - if buffer.name == filename: - return buffer - else: - return None -def display_diagnostics_results(data, buffer): - data = json.loads(data) - logging.debug(data) + message("Fixits applied") + + def _place_signs(self): + """ Add gutter indicator signs next to lines that have diagnostics. + """ + self._reset_signs() + used_ids = Sign.used_ids(self._vimbuffer.number) + for diagnostic in self._diagnostics.values(): + self._place_sign(diagnostic.line_num, diagnostic.type, used_ids) + + def _rtags_dirty_reindex(self): + """ Reindex unsaved buffer contents in rtags. + """ + content = "\n".join([x for x in self._vimbuffer]) + result = run_rc_command( + '--json --reindex {0} --unsaved-file={0}:{1}'.format( + self._vimbuffer.name, len(content) + ), content + ) + self._is_dirty = False + logger.debug("Rtags responded to reindex request: %s" % result) + + def _rtags_is_reindexing(self): + """ Check if rtags has this buffer queued for reindexing. + """ + # Unfortunately, --check-reindex doesn't work if --unsaved-file used with --reindex. + content = run_rc_command('--status jobs') + if content is None: + return error("Failed to check if %s needs reindex" % self._vimbuffer.name) + return self._vimbuffer.name in content + + def _update_diagnostics(self, open_loclist=False): + """ Fetch new diagnostics from rtags and update gutter signs and location list. + """ + # Reset current diagnostics. + self._diagnostics = {} + # Reset diagnostic timer, so we don't query too often. + self._last_diagnostics_time = time() + + # Get the diagnostics from rtags. + content = run_rc_command( + '--diagnose %s --synchronous-diagnostics --json' % self._vimbuffer.name + ) + if content is None: + return error('Failed to get diagnostics for "%s"' % self._vimbuffer.name) + logger.debug("Diagnostics for %s from rtags: %s" % (self._vimbuffer.name, content)) + data = json.loads(content) + errors = data['checkStyle'][self._vimbuffer.name] + + # Construct Diagnostic objects from rtags response, and cache keyed by line number. + for diagnostic in Diagnostic.from_rtags_errors(self._vimbuffer.name, errors): + self._diagnostics[diagnostic.line_num] = diagnostic + + # Update location list with new diagnostics, if ours is currently at the top of the stack. + self._update_loclist(open_loclist) + # Place gutter signs next to lines with diagnostics to show. + self._place_signs() + # Flag that we've changed cursor line to trick into rerendering diagnostic message. + self._line_num_last = -1 + self.on_cursor_moved() + + def _update_loclist(self, force): + """ Update the location list for the current buffer with updated diagnostics. + + Unfortunately, there doesn't seem to be a simple way to know if the location list for + the current window is actually visible, so just always update it if it's at the top of + the stack. + + If `force` is given then always update, and show, the location list. + """ + # Only bother updating if the active window is showing this buffer. + if self._vimbuffer.number != vim.current.window.buffer.number: + return + # Get title of this buffer's location list, if available. + loclist_info = vim.eval( + 'getloclist(%s, {"title": 0})' % vim.current.window.number + ) + loclist_title = loclist_info.get('title') + # If the title matches our location list, then we want to update, otherwise either create + # or quit. + is_rtags_loclist_open = loclist_title == Buffer._DIAGNOSTICS_LIST_TITLE + if not force and not is_rtags_loclist_open: + logger.debug("Location list not open (title=%s) so not updating it" % loclist_title) + return + + logger.debug("Updating location list with %s diagnostics" % len(self._diagnostics)) + + # Get our diagnostics as quicklist/loclist formatted dict. + height, lines = Diagnostic.to_qlist_errors(self._diagnostics.values()) + # If our loclist is open, we want to replace the contents, otherwise create a new loclist. + if is_rtags_loclist_open: + action = "r" + else: + action = " " + + # Create/replace the loclist with our diagnostics. + vim.eval( + 'setloclist(%d, [], "%s", {"items": %s, "title": "%s"})' + % (vim.current.window.number, action, lines, Buffer._DIAGNOSTICS_LIST_TITLE) + ) - check_style = data['checkStyle'] - vim.command('sign unplace *') + # If we want to open the loclist and we have something to show, then open it. + if force: + if height > 0: + vim.command('lopen %d' % height) + else: + message("No errors to show") + + def _place_sign(self, line_num, name, used_ids): + """ Create, place and remember a diagnostic gutter sign. + """ + # Get last sign ID that we used in this buffer. + id_ = self._signs and self._signs[-1].id or Sign.START_ID + # We need a new ID. + id_ += 1 + # Other plugins could have added signs, so make sure we don't splat them. + while id_ in used_ids: + id_ += 1 + logger.debug( + 'Appending sign %s on line %s with id %s in buffer %s (%s)' % ( + name, line_num, id_, self._vimbuffer.number, self._vimbuffer.name + ) + ) + # Construct a Sign, which will also render it in the buffer, and cache it. + self._signs.append(Sign(id_, line_num, name, self._vimbuffer.number)) + + def _reset_signs(self): + """ Remove all diagnostic signs from buffer gutter and reset our cache of them. + """ + for sign in self._signs: + sign.unplace() + self._signs = [] + + +class Project(object): + """ Wrapper for an rtags "project". + + Used to track last modification time of rtags index database. + """ + + _rtags_data_dir = None + _cache = {} + + @staticmethod + def get(filepath): + # Get rtags project that given file belongs to, if any. + project_root = run_rc_command('--project %s' % filepath) + # If rc command line failed, then assume nothing to do. + if project_root is None: + return None + # If no rtags project, then nothing to do. + if project_root.startswith("No matches"): + logger.debug("No rtags project found for %s" % filepath) + return None + + # Lazily find the location of the rtags database. We check the modification date of the DB + # to decide if/when we need to update our cache. + if Project._rtags_data_dir is None: + info = run_rc_command('--status info') + logger.debug("RTags info:\n%s" % info) + match = re.search("^dataDir: (.*)$", info, re.MULTILINE) + Project._rtags_data_dir = match.group(1) + logger.info("RTags data directory set to %s" % Project._rtags_data_dir) + + project_root = project_root.strip() + # Get the project for the given file from the cache, if available. + project = Project._cache.get(project_root) + if project is not None: + return project + + # Create a new Project and cache it. + logger.info("Found RTags project %s for %s" % (project_root, filepath)) + project = Project(project_root) + Project._cache[project_root] = project + return project + + def __init__(self, project_root): + self._project_root = project_root + # Calculate the path of project database in the RTags data directory. + self._db_path = os.path.join( + Project._rtags_data_dir, project_root.replace("/", "_"), "project" + ) + logger.debug("Project %s db path set to %s" % (self._project_root, self._db_path)) - # There are no errors - if check_style == None: - return message('No errors found') + def last_updated_time(self): + """ Unix timestamp when the rtags database was last updated. + """ + return os.path.getmtime(self._db_path) - quickfix_errors = [] - vim.command('sign define fixit text=F texthl=FixIt') - vim.command('sign define warning text=W texthl=Warning') - vim.command('sign define error text=E texthl=Error') +class Diagnostic(object): - for filename, errors in check_style.items(): - for i, e in enumerate(errors): + @staticmethod + def from_rtags_errors(filename, errors): + diagnostics = [] + for e in errors: if e['type'] == 'skipped': continue - # strip error prefix s = ' Issue: ' index = e['message'].find(s) if index != -1: e['message'] = e['message'][index + len(s):] - error_type = 'E' if e['type'] == 'error' else 'W' - quickfix_errors.append({'lnum': e['line'], 'col': e['column'], - 'nr': i, 'text': e['message'], 'filename': filename, - 'type': error_type}) - if find_buffer(filename) is not None: - cmd = 'sign place %d line=%s name=%s file=%s' % ( - i + 1, e['line'], e['type'], filename) - vim.command(cmd) - - # There are no errors - if not quickfix_errors: - return message('No errors found') - else: - display_locations(quickfix_errors, buffer) + diagnostics.append( + Diagnostic(filename, e['line'], e['column'], e['type'], e['message']) + ) + return diagnostics + + @staticmethod + def to_qlist_errors(diagnostics): + num_diagnostics = len(diagnostics) + lines = [d._to_qlist_dict() for d in diagnostics] + lines = sorted(lines, key=lambda d: (d['type'], d['filename'], d['lnum'])) + lines = json.dumps(lines) + + max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) + height = min(max_height, num_diagnostics) + + return height, lines + + def __init__(self, filename, line_num, char_num, type_, text): + self.filename = filename + self.line_num = line_num + self.char_num = char_num + self.type = type_ + self.text = text + + def _to_qlist_dict(self): + error_type = self.type[0].upper() + return { + 'lnum': self.line_num, 'col': self.char_num, 'text': self.text, + 'filename': self.filename, 'type': error_type + } + + +class Sign(object): + START_ID = 2000 + _is_signs_defined = False + + @staticmethod + def _define_signs(): + """ Define highlight group and gutter signs for diagnostics. + + Must do this lazily because other plugins can go and change SignColumn highlight + group on initialisation (e.g. gitgutter). + """ + logger.debug("Defining gutter diagnostic signs") + def get_bgcolour(group): + logger.debug("Scanning highlight group %s for background colour" % group) + output = get_command_output("highlight %s" % group) + logger.debug("Highlight group output:\n%s" % output) + match = re.search(r"links to (\S+)", output) + if match is None: + ctermbg_match = re.search(r"ctermbg=(\S+)", output) + guibg_match = re.search(r"guibg=(\S+)", output) + return ( + ctermbg_match and ctermbg_match.group(1), + guibg_match and guibg_match.group(1) + ) + return get_bgcolour(match.group(1)) + + ctermbg, guibg = get_bgcolour("SignColumn") + bg = "" + if guibg is not None: + bg += " guibg=%s" % guibg + if ctermbg is not None: + bg += " ctermbg=%s" % ctermbg + + logger.debug("Background colours are %s, %s" % (ctermbg, guibg)) + vim.command( + "highlight rtags_fixit guifg=#ff00ff ctermfg=5 %s" % bg + ) + vim.command( + "highlight rtags_warning guifg=#fff000 ctermfg=11 %s" % bg + ) + vim.command( + "highlight rtags_error guifg=#ff0000 ctermfg=1 %s" % bg + ) -def get_diagnostics(): - buffer = find_buffer() - is_modified = bool(int((vim.eval('getbufvar(%d, "&mod")' % buffer.number)))) - cmd = '--diagnose %s --synchronous-diagnostics --json' % buffer.name + vim.command("sign define rtags_fixit text=Fx texthl=rtags_fixit") + vim.command("sign define rtags_warning text=W texthl=rtags_warning") + vim.command("sign define rtags_error text=E texthl=rtags_error") + Sign._is_signs_defined = True + + @staticmethod + def used_ids(buffer_num): + signs_txt = get_command_output("sign place buffer=%s" % buffer_num) + sign_ids = set() + for sign_match in re.finditer("id=(\d+)", signs_txt): + sign_ids.add(int(sign_match.group(1))) + return sign_ids + + def __init__(self, id, line_num, name, buffer_num): + self.id = id + self._vimbuffer_num = buffer_num + if not Sign._is_signs_defined: + Sign._define_signs() + vim.command( + 'sign place %d line=%s name=rtags_%s buffer=%s' % ( + self.id, line_num, name, self._vimbuffer_num + ) + ) - content = '' - if is_modified: - content = '\n'.join([x for x in buffer]) - cmd += ' --unsaved-file=%s:%d' % (filename, len(content)) + def unplace(self): + vim.command('sign unplace %s buffer=%s' % (self.id, self._vimbuffer_num)) - content = run_rc_command(cmd, content) - if content == None: - return error('Failed to get diagnostics for "%s"' % buffer.name) - display_diagnostics_results(content, buffer) +def get_command_output(cmd_txt): + return vim.eval('rtags#getCommandOutput("%s")' % cmd_txt) - return 0 -def get_diagnostics_all(): - content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') - if content is None: - return error('Failed to get diagnostics') - display_diagnostics_results(content, find_buffer()) - return 0 +def invalid_buffer_message(filename): + print( + "No RTags project for file: %s" % filename + if filename else "Please select a file buffer and try again" + ) + def error(msg): - message('%s: see log file at "%s" for more information' % (msg, logfile)) + message("""%s: see log file at" "%s" for more information""" % (msg, logfile)) + def message(msg): - vim.command("echom '%s'" % msg) + vim.command("""echom '%s'""" % msg) + From 26af7bbdf188ea35088b13d0d561f1deca98bb6b Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 24 Feb 2018 11:29:12 +0000 Subject: [PATCH 07/39] Fix getting project for buffers with blank filenames (e.g. loclist) * RTags returns the last used project if given a blank filename, but we want no project in this case. --- plugin/vimrtags.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index b6141056..4047db5f 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -514,6 +514,9 @@ class Project(object): @staticmethod def get(filepath): + # A blank filename (e.g. loclist) has no project. + if not filepath: + return None # Get rtags project that given file belongs to, if any. project_root = run_rc_command('--project %s' % filepath) # If rc command line failed, then assume nothing to do. From a99f31ac595b8625d7b10a40d1d37beacbc39707 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 24 Feb 2018 11:32:38 +0000 Subject: [PATCH 08/39] Fix quickfix/loclist sort order and improve rendering * Was being sorted with errors at bottom, for some reason. * Add error `nr`, which shows up in the qlist, and is maybe useful for navigation. * Revert error `type` to a choice of "E" or "W", since "F" shows up as a single character, rather than a word, unlike "E"=>"error" and "E"=>"warning". * Append "FIXIT" to the end of error messages for errors with a fixit available. --- plugin/vimrtags.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 4047db5f..82add3b9 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -583,14 +583,20 @@ def from_rtags_errors(filename, errors): @staticmethod def to_qlist_errors(diagnostics): num_diagnostics = len(diagnostics) - lines = [d._to_qlist_dict() for d in diagnostics] - lines = sorted(lines, key=lambda d: (d['type'], d['filename'], d['lnum'])) - lines = json.dumps(lines) - + # Sort diagnostics into display order. Errors at top, grouped by filename in line num order. + diagnostics = sorted(diagnostics, key=lambda d: (d.type, d.filename, d.line_num)) + # Convert Diagnostic objects into quickfix compatible dicts. + diagnostics = [d._to_qlist_dict() for d in diagnostics] + # Add error number to diagnostic (shows in list, maybe useful for navigation?). + for idx, diagnostic in enumerate(diagnostics): + diagnostic['nr'] = idx + 1 + diagnostics = json.dumps(diagnostics) + + # Calculate max height of list window. max_height = int(get_rtags_variable('MaxSearchResultWindowHeight')) height = min(max_height, num_diagnostics) - return height, lines + return height, diagnostics def __init__(self, filename, line_num, char_num, type_, text): self.filename = filename @@ -600,9 +606,10 @@ def __init__(self, filename, line_num, char_num, type_, text): self.text = text def _to_qlist_dict(self): - error_type = self.type[0].upper() + error_type = "W" if self.type == "warning" else "E" + text = self.text if self.type != "fixit" else self.text + " [FIXIT]" return { - 'lnum': self.line_num, 'col': self.char_num, 'text': self.text, + 'lnum': self.line_num, 'col': self.char_num, 'text': text, 'filename': self.filename, 'type': error_type } From 4841cef676e33b745c7b53abd8fb1165abaa7d38 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 24 Feb 2018 12:51:14 +0000 Subject: [PATCH 09/39] Construct list for rc command arguments from the get-go * In particular, this allows support for filenames with spaces. * Also removed some debug logging spam. --- plugin/vimrtags.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 82add3b9..e4731f57 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -39,15 +39,15 @@ def get_identifier_beginning(): def run_rc_command(arguments, content = None): rc_cmd = os.path.expanduser(vim.eval('g:rtagsRcCmd')) - cmdline = rc_cmd + " " + arguments + cmdline = [rc_cmd] + arguments encoding = 'utf-8' out = None err = None - logger.debug("RTags command: %s" % cmdline.split()) + logger.debug("RTags command: %s" % cmdline) if sys.version_info.major == 3 and sys.version_info.minor >= 5: r = subprocess.run( - cmdline.split(), + cmdline, input = content and content.encode("utf-8"), stdout = subprocess.PIPE, stderr = subprocess.PIPE @@ -60,7 +60,7 @@ def run_rc_command(arguments, content = None): elif sys.version_info.major == 3 and sys.version_info.minor < 5: r = subprocess.Popen( - cmdline.split(), + cmdline, bufsize=0, stdout=subprocess.PIPE, stdin=subprocess.PIPE, @@ -73,7 +73,7 @@ def run_rc_command(arguments, content = None): err = err.decode(encoding) else: r = subprocess.Popen( - cmdline.split(), + cmdline, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT @@ -198,7 +198,7 @@ def show_all_diagnostics(): """ Get all diagnostics for all files and show in quickfix list. """ # Get the diagnostics from rtags. - content = run_rc_command('--diagnose-all --synchronous-diagnostics --json') + content = run_rc_command(['--diagnose-all', '--synchronous-diagnostics', '--json']) if content is None: return error('Failed to get diagnostics') data = json.loads(content) @@ -323,7 +323,7 @@ def apply_fixits(self): ) # Get the fixits from rtags. - content = run_rc_command('--fixits %s' % self._vimbuffer.name) + content = run_rc_command(['--fixits', self._vimbuffer.name]) if content is None: return error("Failed to fetch fixits") content = content.strip() @@ -376,6 +376,7 @@ def _place_signs(self): """ self._reset_signs() used_ids = Sign.used_ids(self._vimbuffer.number) + logger.debug("Appending %s signs to %s" % (len(self._diagnostics), self._vimbuffer.name)) for diagnostic in self._diagnostics.values(): self._place_sign(diagnostic.line_num, diagnostic.type, used_ids) @@ -383,11 +384,10 @@ def _rtags_dirty_reindex(self): """ Reindex unsaved buffer contents in rtags. """ content = "\n".join([x for x in self._vimbuffer]) - result = run_rc_command( - '--json --reindex {0} --unsaved-file={0}:{1}'.format( - self._vimbuffer.name, len(content) - ), content - ) + result = run_rc_command([ + '--json', '--reindex', self._vimbuffer.name, + '--unsaved-file=%s:%d' % (self._vimbuffer.name, len(content)) + ], content) self._is_dirty = False logger.debug("Rtags responded to reindex request: %s" % result) @@ -395,7 +395,7 @@ def _rtags_is_reindexing(self): """ Check if rtags has this buffer queued for reindexing. """ # Unfortunately, --check-reindex doesn't work if --unsaved-file used with --reindex. - content = run_rc_command('--status jobs') + content = run_rc_command(['--status', 'jobs']) if content is None: return error("Failed to check if %s needs reindex" % self._vimbuffer.name) return self._vimbuffer.name in content @@ -410,7 +410,7 @@ def _update_diagnostics(self, open_loclist=False): # Get the diagnostics from rtags. content = run_rc_command( - '--diagnose %s --synchronous-diagnostics --json' % self._vimbuffer.name + ['--diagnose', self._vimbuffer.name, '--synchronous-diagnostics', '--json'] ) if content is None: return error('Failed to get diagnostics for "%s"' % self._vimbuffer.name) @@ -487,11 +487,6 @@ def _place_sign(self, line_num, name, used_ids): # Other plugins could have added signs, so make sure we don't splat them. while id_ in used_ids: id_ += 1 - logger.debug( - 'Appending sign %s on line %s with id %s in buffer %s (%s)' % ( - name, line_num, id_, self._vimbuffer.number, self._vimbuffer.name - ) - ) # Construct a Sign, which will also render it in the buffer, and cache it. self._signs.append(Sign(id_, line_num, name, self._vimbuffer.number)) @@ -518,7 +513,7 @@ def get(filepath): if not filepath: return None # Get rtags project that given file belongs to, if any. - project_root = run_rc_command('--project %s' % filepath) + project_root = run_rc_command(['--project', filepath]) # If rc command line failed, then assume nothing to do. if project_root is None: return None @@ -530,7 +525,7 @@ def get(filepath): # Lazily find the location of the rtags database. We check the modification date of the DB # to decide if/when we need to update our cache. if Project._rtags_data_dir is None: - info = run_rc_command('--status info') + info = run_rc_command(['--status', 'info']) logger.debug("RTags info:\n%s" % info) match = re.search("^dataDir: (.*)$", info, re.MULTILINE) Project._rtags_data_dir = match.group(1) @@ -626,10 +621,10 @@ def _define_signs(): group on initialisation (e.g. gitgutter). """ logger.debug("Defining gutter diagnostic signs") + # Recursively search for background colours through group links. def get_bgcolour(group): logger.debug("Scanning highlight group %s for background colour" % group) output = get_command_output("highlight %s" % group) - logger.debug("Highlight group output:\n%s" % output) match = re.search(r"links to (\S+)", output) if match is None: ctermbg_match = re.search(r"ctermbg=(\S+)", output) From 6835c3b98db88f9c36bc19b9c052c821d2a54473 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 24 Feb 2018 12:54:56 +0000 Subject: [PATCH 10/39] Fix detection of buffer modification state * "0" is truthy, so `int(` it. --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index e4731f57..1328a6db 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -257,7 +257,7 @@ def on_idle(self): if self._is_dirty: # `TextChange` autocmd also triggers just by switching buffers, so we have to be sure. - is_really_dirty = vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number) + is_really_dirty = int(vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number)) else: is_really_dirty = False From 51c2a3970df5804ece5b09b4f23db79bb20366b8 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 24 Feb 2018 23:44:20 +0000 Subject: [PATCH 11/39] PEP8 fixes * Stop linter complaining --- plugin/vimrtags.py | 64 ++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 1328a6db..0667b7c9 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -1,7 +1,6 @@ import vim import json import subprocess -import io import os import sys import tempfile @@ -22,6 +21,7 @@ def configure_logger(): logger.addHandler(handler) logger.setLevel(loglevel) + configure_logger() @@ -37,7 +37,8 @@ def get_identifier_beginning(): return column + 1 -def run_rc_command(arguments, content = None): + +def run_rc_command(arguments, content=None): rc_cmd = os.path.expanduser(vim.eval('g:rtagsRcCmd')) cmdline = [rc_cmd] + arguments @@ -48,14 +49,14 @@ def run_rc_command(arguments, content = None): if sys.version_info.major == 3 and sys.version_info.minor >= 5: r = subprocess.run( cmdline, - input = content and content.encode("utf-8"), - stdout = subprocess.PIPE, - stderr = subprocess.PIPE + input=content and content.encode("utf-8"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE ) out, err = r.stdout, r.stderr - if not out is None: + if out is not None: out = out.decode(encoding) - if not err is None: + if err is not None: err = err.decode(encoding) elif sys.version_info.major == 3 and sys.version_info.minor < 5: @@ -67,9 +68,9 @@ def run_rc_command(arguments, content = None): stderr=subprocess.STDOUT ) out, err = r.communicate(input=content.encode(encoding)) - if not out is None: + if out is not None: out = out.decode(encoding) - if not err is None: + if err is not None: err = err.decode(encoding) else: r = subprocess.Popen( @@ -93,32 +94,34 @@ def run_rc_command(arguments, content = None): def get_rtags_variable(name): return vim.eval('g:rtags' + name) + def parse_completion_result(data): result = json.loads(data) logger.debug(result) completions = [] for c in result['completions']: - k = c['kind'] - kind = '' - if k == 'FunctionDecl' or k == 'FunctionTemplate': - kind = 'f' - elif k == 'CXXMethod' or k == 'CXXConstructor': - kind = 'm' - elif k == 'VarDecl': - kind = 'v' - elif k == 'macro definition': - kind = 'd' - elif k == 'EnumDecl': - kind = 'e' - elif k == 'TypedefDecl' or k == 'StructDecl' or k == 'EnumConstantDecl': - kind = 't' - - match = {'menu': " ".join([c['parent'], c['signature']]), 'word': c['completion'], 'kind': kind} - completions.append(match) + k = c['kind'] + kind = '' + if k == 'FunctionDecl' or k == 'FunctionTemplate': + kind = 'f' + elif k == 'CXXMethod' or k == 'CXXConstructor': + kind = 'm' + elif k == 'VarDecl': + kind = 'v' + elif k == 'macro definition': + kind = 'd' + elif k == 'EnumDecl': + kind = 'e' + elif k == 'TypedefDecl' or k == 'StructDecl' or k == 'EnumConstantDecl': + kind = 't' + + match = {'menu': " ".join([c['parent'], c['signature']]), 'word': c['completion'], 'kind': kind} + completions.append(match) return completions + def send_completion_request(): filename = vim.eval('s:file') line = int(vim.eval('s:line')) @@ -131,14 +134,15 @@ def send_completion_request(): lines = [x for x in buffer] content = '\n'.join(lines[:line - 1] + [lines[line - 1] + prefix] + lines[line:]) - cmd = ('--synchronous-completions -l %s:%d:%d --unsaved-file=%s:%d --json' - % (filename, line, column, filename, len(content))) + cmd = '--synchronous-completions -l %s:%d:%d --unsaved-file=%s:%d --json' % ( + filename, line, column, filename, len(content) + ) if len(prefix) > 0: cmd += ' --code-complete-prefix %s' % prefix content = run_rc_command(cmd, content) logger.debug("Got completion: %s" % content) - if content == None: + if content is None: return None return parse_completion_result(content) @@ -621,6 +625,7 @@ def _define_signs(): group on initialisation (e.g. gitgutter). """ logger.debug("Defining gutter diagnostic signs") + # Recursively search for background colours through group links. def get_bgcolour(group): logger.debug("Scanning highlight group %s for background colour" % group) @@ -698,4 +703,3 @@ def error(msg): def message(msg): vim.command("""echom '%s'""" % msg) - From 6b839dd4417691f42e52c522c0ef3d5a265c8e3d Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 02:53:36 +0000 Subject: [PATCH 12/39] Fix completion request to use list for rc args * Missed that one. --- plugin/vimrtags.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 0667b7c9..133e3eea 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -115,9 +115,12 @@ def parse_completion_result(data): kind = 'e' elif k == 'TypedefDecl' or k == 'StructDecl' or k == 'EnumConstantDecl': kind = 't' + description = _completion_description(c, ['parent']) + else: + description = _completion_description(c, []) - match = {'menu': " ".join([c['parent'], c['signature']]), 'word': c['completion'], 'kind': kind} - completions.append(match) + match = {'menu': description, 'word': c['completion'], 'kind': kind} + completions.append(match) return completions @@ -134,11 +137,13 @@ def send_completion_request(): lines = [x for x in buffer] content = '\n'.join(lines[:line - 1] + [lines[line - 1] + prefix] + lines[line:]) - cmd = '--synchronous-completions -l %s:%d:%d --unsaved-file=%s:%d --json' % ( - filename, line, column, filename, len(content) - ) + cmd = [ + '--synchronous-completions', '-l', '%s:%d:%d' % (filename, line, column), + '--unsaved-file=%s:%d' % (filename, len(content)), '--json' + ] + if len(prefix) > 0: - cmd += ' --code-complete-prefix %s' % prefix + cmd += ['--code-complete-prefix', prefix] content = run_rc_command(cmd, content) logger.debug("Got completion: %s" % content) From eefacacfc74c823ac12df5a9a128f99e22021d47 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 03:04:17 +0000 Subject: [PATCH 13/39] Unify + tidy vim + python logging * Add error message displayed to user when no jump to symbols found. * Log vim and python to same file. * Default log file to vim `tempname`. * Add timestamp to vim logs, like python, and add a tag to show whether the log comes from vim or from python. * Rename rdm log file var to be in line with standard log file var. * Remove some of the more spammy logs. --- plugin/rtags.vim | 18 +++++++++++------- plugin/vimrtags.py | 23 +++++++++-------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 3c16d398..33239409 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -27,8 +27,12 @@ if !exists("g:rtagsRdmCmd") let g:rtagsRdmCmd = "rdm" endif -if !exists("g:rtagsRdmLogFile") - let g:rtagsRdmLogFile = "/dev/null" +if !exists("g:rtagsLog") + let g:rtagsLog = tempname() +endif + +if !exists("g:rtagsRdmLog") + let g:rtagsRdmLog = tempname() endif if !exists("g:rtagsAutoLaunchRdm") @@ -72,7 +76,7 @@ endif if g:rtagsAutoLaunchRdm call system(g:rtagsRcCmd." -w") if v:shell_error != 0 - call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLogFile) + call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLog) end end @@ -130,9 +134,7 @@ call rtags#InitPython() " Logging routine """ function! rtags#Log(message) - if exists("g:rtagsLog") - call writefile([string(a:message)], g:rtagsLog, "a") - endif + call writefile([strftime("%Y-%m-%d %H:%M:%S", localtime()) . " | vim | " . string(a:message)], g:rtagsLog, "a") endfunction " @@ -491,7 +493,7 @@ function! rtags#JumpToHandler(results, args) if len(results) >= 0 && open_opt != g:SAME_WINDOW call rtags#cloneCurrentBuffer(open_opt) endif - + call rtags#Log("JumpTo results with ".json_encode(a:args).": ".json_encode(results)) if len(results) > 1 call rtags#DisplayResults(results) elseif len(results) == 1 @@ -507,6 +509,8 @@ function! rtags#JumpToHandler(results, args) if rtags#jumpToLocation(jump_file, lnum, col) normal! zz endif + else + echom "Failed to jump - cannot follow symbol" endif endfunction diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 133e3eea..a37733ce 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -3,20 +3,19 @@ import subprocess import os import sys -import tempfile import re import logging from time import time -logfile = '%s/vim-rtags-python.log' % tempfile.gettempdir() loglevel = logging.DEBUG logger = logging.getLogger(__name__) def configure_logger(): + logfile = vim.eval('g:rtagsLog') handler = logging.FileHandler(logfile) - formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") + formatter = logging.Formatter("%(asctime)s | py | %(levelname)s | %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(loglevel) @@ -29,8 +28,7 @@ def get_identifier_beginning(): line = vim.eval('s:line') column = int(vim.eval('s:start')) - logger.debug(line) - logger.debug(column) + logger.debug("Completion line:column=%s:%s" % (line, column)) while column >= 0 and (line[column].isalnum() or line[column] == '_'): column -= 1 @@ -97,7 +95,6 @@ def get_rtags_variable(name): def parse_completion_result(data): result = json.loads(data) - logger.debug(result) completions = [] for c in result['completions']: @@ -146,7 +143,7 @@ def send_completion_request(): cmd += ['--code-complete-prefix', prefix] content = run_rc_command(cmd, content) - logger.debug("Got completion: %s" % content) + #logger.debug("Got completion: %s" % content) if content is None: return None @@ -218,7 +215,7 @@ def show_all_diagnostics(): diagnostics += Diagnostic.from_rtags_errors(filename, errors) if not diagnostics: - return message("No errors to show") + return message("No errors to display") # Convert list of Diagnostic objects to quickfix-compatible dict. height, lines = Diagnostic.to_qlist_errors(diagnostics) @@ -423,9 +420,10 @@ def _update_diagnostics(self, open_loclist=False): ) if content is None: return error('Failed to get diagnostics for "%s"' % self._vimbuffer.name) - logger.debug("Diagnostics for %s from rtags: %s" % (self._vimbuffer.name, content)) + #logger.debug("Diagnostics for %s from rtags: %s" % (self._vimbuffer.name, content)) data = json.loads(content) errors = data['checkStyle'][self._vimbuffer.name] + logger.debug("Got %d diagnostics for %s" % (len(errors), self._vimbuffer.name)) # Construct Diagnostic objects from rtags response, and cache keyed by line number. for diagnostic in Diagnostic.from_rtags_errors(self._vimbuffer.name, errors): @@ -480,11 +478,8 @@ def _update_loclist(self, force): ) # If we want to open the loclist and we have something to show, then open it. - if force: - if height > 0: - vim.command('lopen %d' % height) - else: - message("No errors to show") + if force and height > 0: + vim.command('lopen %d' % height) def _place_sign(self, line_num, name, used_ids): """ Create, place and remember a diagnostic gutter sign. From 1e44d39df0160d8145b593d2e4390fabbc43fbec Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 03:04:46 +0000 Subject: [PATCH 14/39] Remove unused vim `CompleteAtCursor` function - python is used instead --- plugin/rtags.vim | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 33239409..9cfb8cf8 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -944,51 +944,6 @@ function! rtags#getCommandOutput(cmd_txt) abort return output endfunction -" -" This function assumes it is invoked from insert mode -" -function! rtags#CompleteAtCursor(wordStart, base) - let flags = "--synchronous-completions -l" - let file = expand("%:p") - let pos = getpos('.') - let line = pos[1] - let col = pos[2] - - if index(['.', '::', '->'], a:base) != -1 - let col += 1 - endif - - let rcRealCmd = rtags#getRcCmd() - - exec "normal! \" - let stdin_lines = join(getline(1, "$"), "\n").a:base - let offset = len(stdin_lines) - - exec "startinsert!" - " echomsg getline(line) - " sleep 1 - " echomsg "DURING INVOCATION POS: ".pos[2] - " sleep 1 - " echomsg stdin_lines - " sleep 1 - " sed command to remove CDATA prefix and closing xml tag from rtags output - let sed_cmd = "sed -e 's/.*CDATA\\[//g' | sed -e 's/.*\\/completions.*//g'" - let cmd = printf("%s %s %s:%s:%s --unsaved-file=%s:%s | %s", rcRealCmd, flags, file, line, col, file, offset, sed_cmd) - call rtags#Log("Command line:".cmd) - - let result = split(system(cmd, stdin_lines), '\n\+') - " echomsg "Got ".len(result)." completions" - " sleep 1 - call rtags#Log("-----------") - "call rtags#Log(result) - call rtags#Log("-----------") - return result - " for r in result - " echo r - " endfor - " call rtags#DisplayResults(result) -endfunction - function! s:Pyeval( eval_string ) if g:rtagsPy == 'python3' return py3eval( a:eval_string ) From f20546a0617784e7c51c56a3127e6c76eaf4633d Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 03:05:51 +0000 Subject: [PATCH 15/39] Improve completion popup text description * Try to include more relevant info, such as function signatures and Doxygen docstrings. --- plugin/vimrtags.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index a37733ce..0b344c1a 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -102,15 +102,20 @@ def parse_completion_result(data): kind = '' if k == 'FunctionDecl' or k == 'FunctionTemplate': kind = 'f' + description = _completion_description(c, ['parent', 'signature']) elif k == 'CXXMethod' or k == 'CXXConstructor': kind = 'm' + description = _completion_description(c, ['parent', 'signature']) elif k == 'VarDecl': kind = 'v' + description = _completion_description(c, ['parent', 'signature']) elif k == 'macro definition': kind = 'd' + description = _completion_description(c, ['signature']) elif k == 'EnumDecl': kind = 'e' - elif k == 'TypedefDecl' or k == 'StructDecl' or k == 'EnumConstantDecl': + description = _completion_description(c, ['parent']) + elif k in ('TypedefDecl', 'StructDecl', 'EnumConstantDecl', 'ClassDecl', 'FieldDecl'): kind = 't' description = _completion_description(c, ['parent']) else: @@ -122,6 +127,12 @@ def parse_completion_result(data): return completions +def _completion_description(completion, fields): + fields = [field for field in fields if completion[field] != completion['completion']] + fields = ['completion'] + fields + ['brief_comment'] + return " -- ".join(filter(None, [completion[field] for field in fields])) + + def send_completion_request(): filename = vim.eval('s:file') line = int(vim.eval('s:line')) From fa013a2583bcc6c32db6af9a23994cd4949bbebb Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 19:35:52 +0000 Subject: [PATCH 16/39] Fix cleaning wiped out buffers * `keys()` in python3 returns an iterator, so it's not safe to delete items whilst iterating through them. * So take a copy and iterate over that. --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 0b344c1a..74439334 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -200,7 +200,7 @@ def get(id_): # Periodically clean closed buffers if time() - Buffer._cache_last_cleaned > Buffer._CACHE_CLEAN_PERIOD: logger.debug("Cleaning invalid buffers") - for id_ in Buffer._cache.keys(): + for id_ in list(Buffer._cache.keys()): if not Buffer._cache[id_]._vimbuffer.valid: logger.debug("Cleaning invalid buffer %s" % id_) del Buffer._cache[id_] From 056fc61a2fdd50f85f1f391cdf0930b71e0df56d Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 19:36:36 +0000 Subject: [PATCH 17/39] Fix error logging * File name is no longer determined within python, it's an option, so use that. --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 74439334..72e95722 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -709,7 +709,7 @@ def invalid_buffer_message(filename): def error(msg): - message("""%s: see log file at" "%s" for more information""" % (msg, logfile)) + message("""%s: see log file at" "%s" for more information""" % (msg, vim.eval('g:rtagsLog'))) def message(msg): From fbde89010704d1fc2bfba50a71336569b6de9813 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 22:43:19 +0000 Subject: [PATCH 18/39] Add polling for diagnostics * Add regular polling for updated diagnostics, with interval configured by `g:rtagsDiagnosticsPollingInterval` (set to <=0 to disable). * Move auto launch rdm, python init and autocmd definitions to bottom of `rtags.vim` file, so all functions are already defined by the time they are called. Just good practice, and needed for the new `Poll` call. --- plugin/rtags.vim | 52 ++++++++++++++++++++++++++++-------------- plugin/vimrtags.py | 56 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 9cfb8cf8..326809ec 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -69,16 +69,14 @@ if !exists("g:rtagsAutoDiagnostics") let g:rtagsAutoDiagnostics = 1 endif +if !exists("g:rtagsDiagnosticsPollingInterval") + let g:rtagsDiagnosticsPollingInterval = 3000 +endif + if !exists("g:rtagsCppOmnifunc") let g:rtagsCppOmnifunc = 1 endif -if g:rtagsAutoLaunchRdm - call system(g:rtagsRcCmd." -w") - if v:shell_error != 0 - call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLog) - end -end let g:SAME_WINDOW = 'same_window' let g:H_SPLIT = 'hsplit' @@ -128,8 +126,6 @@ function! rtags#InitPython() exe g:rtagsPy." ".s:pyInitScript endfunction -call rtags#InitPython() - """ " Logging routine """ @@ -925,15 +921,10 @@ function! rtags#NotifyCursorMoved() return s:Pyeval("vimrtags.Buffer.current().on_cursor_moved()") endfunction -if g:rtagsAutoDiagnostics == 1 - augroup rtags_auto_diagnostics - autocmd! - autocmd BufWritePost *.cpp,*.c,*.hpp,*.h call rtags#NotifyWrite() - autocmd TextChanged,TextChangedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyEdit() - autocmd CursorHold,CursorHoldI,BufEnter *.cpp,*.c,*.hpp,*.h call rtags#NotifyIdle() - autocmd CursorMoved,CursorMovedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyCursorMoved() - augroup END -endif +function! rtags#Poll(timer) + call s:Pyeval("vimrtags.Buffer.current().on_poll()") + call timer_start(g:rtagsDiagnosticsPollingInterval, "rtags#Poll") +endfunction " Generic function to get output of a command. " Used in python for things that can't be read directly via vim.eval(...) @@ -1142,3 +1133,30 @@ command! -nargs=1 -complete=dir RtagsLoadCompilationDb call rtags#LoadCompilatio " The most commonly used find operation command! -nargs=1 -complete=customlist,rtags#CompleteSymbols Rtag RtagsIFindSymbols + +if g:rtagsAutoLaunchRdm + call system(g:rtagsRcCmd." -w") + if v:shell_error != 0 + call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLog) + end +end + + +call rtags#InitPython() + + +if g:rtagsAutoDiagnostics == 1 + augroup rtags_auto_diagnostics + autocmd! + autocmd BufWritePost *.cpp,*.c,*.hpp,*.h call rtags#NotifyWrite() + autocmd TextChanged,TextChangedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyEdit() + autocmd CursorHold,CursorHoldI,BufEnter *.cpp,*.c,*.hpp,*.h call rtags#NotifyIdle() + autocmd CursorMoved,CursorMovedI *.cpp,*.c,*.hpp,*.h call rtags#NotifyCursorMoved() + augroup END + + if g:rtagsDiagnosticsPollingInterval > 0 + call rtags#Log("Starting async update checking") + call rtags#Poll(0) + endif +endif + diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 72e95722..c7a590ae 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -154,7 +154,7 @@ def send_completion_request(): cmd += ['--code-complete-prefix', prefix] content = run_rc_command(cmd, content) - #logger.debug("Got completion: %s" % content) + # logger.debug("Got completion: %s" % content) if content is None: return None @@ -167,7 +167,6 @@ class Buffer(object): _cache = {} _cache_last_cleaned = time() _CACHE_CLEAN_PERIOD = 30 - _DIAGNOSTICS_CHECK_PERIOD = 5 _DIAGNOSTICS_LIST_TITLE = "RTags diagnostics" _DIAGNOSTICS_ALL_LIST_TITLE = "RTags diagnostics for all files" _FIXITS_LIST_TITLE = "RTags fixits applied" @@ -206,6 +205,7 @@ def get(id_): del Buffer._cache[id_] Buffer._cache_last_cleaned = time() + logger.debug("Wrapping new buffer: %s" % id_) buff = Buffer(vim.buffers[id_]) Buffer._cache[id_] = buff return buff @@ -268,27 +268,47 @@ def on_edit(self): def on_idle(self): """ Reindex or update diagnostics for this buffer. + + Check if the buffer is modified and needs reindexing, if so then reindex, otherwise + check if the RTags project for this file has been updated on disk, if so then update + diagnostics. """ + # logger.debug("Idle callback for %s" % self._vimbuffer.name) if self._project is None: return - if self._is_dirty: - # `TextChange` autocmd also triggers just by switching buffers, so we have to be sure. - is_really_dirty = int(vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number)) - else: - is_really_dirty = False - - if is_really_dirty: + if self._is_really_dirty(): # Reindex dirty buffer - no point refreshing diagnostics at this point. logger.debug("Buffer %s needs dirty reindex" % self._vimbuffer.number) - self._rtags_dirty_reindex() - # No point getting diagnostics any time soon. self._last_diagnostics_time = time() + self._rtags_dirty_reindex() elif self._last_diagnostics_time < self._project.last_updated_time(): # Update diagnostics signs/list. logger.debug( - "Project updated, checking for updated diagnostics for %s" % self._vimbuffer.name + "Project updated (idle), checking for updated diagnostics for %s" + % self._vimbuffer.name + ) + self._update_diagnostics() + + def on_poll(self): + """ Update diagnostics for this buffer. + + Only update if the buffer does not need reindexing and the RTags project has changed + on disk. + """ + # logger.debug(Poll callback for %s" % self._vimbuffer.name) + if self._project is None: + return + + if ( + not self._is_really_dirty() and + self._last_diagnostics_time < self._project.last_updated_time() + ): + # Update diagnostics signs/list. + logger.debug( + "Project updated (polling), checking for updated diagnostics for %s" + % self._vimbuffer.name ) self._update_diagnostics() @@ -388,6 +408,16 @@ def apply_fixits(self): message("Fixits applied") + def _is_really_dirty(self): + """ Check the dirty flag and confirm the buffer really is modified. + + `TextChange` autocmd also triggers just by switching buffers, so we have to be sure. + """ + self._is_dirty = self._is_dirty and bool(int( + vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number) + )) + return self._is_dirty + def _place_signs(self): """ Add gutter indicator signs next to lines that have diagnostics. """ @@ -431,7 +461,7 @@ def _update_diagnostics(self, open_loclist=False): ) if content is None: return error('Failed to get diagnostics for "%s"' % self._vimbuffer.name) - #logger.debug("Diagnostics for %s from rtags: %s" % (self._vimbuffer.name, content)) + # logger.debug("Diagnostics for %s from rtags: %s" % (self._vimbuffer.name, content)) data = json.loads(content) errors = data['checkStyle'][self._vimbuffer.name] logger.debug("Got %d diagnostics for %s" % (len(errors), self._vimbuffer.name)) From 1c71cddb088078b09fefa6e4d919a987bdf1c984 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 22:43:54 +0000 Subject: [PATCH 19/39] Fix cleaning wiped out buffers * Don't re-use `id_` variable, we still need it! --- plugin/vimrtags.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index c7a590ae..1fd802d3 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -199,10 +199,10 @@ def get(id_): # Periodically clean closed buffers if time() - Buffer._cache_last_cleaned > Buffer._CACHE_CLEAN_PERIOD: logger.debug("Cleaning invalid buffers") - for id_ in list(Buffer._cache.keys()): - if not Buffer._cache[id_]._vimbuffer.valid: - logger.debug("Cleaning invalid buffer %s" % id_) - del Buffer._cache[id_] + for id_old in list(Buffer._cache.keys()): + if not Buffer._cache[id_old]._vimbuffer.valid: + logger.debug("Cleaning invalid buffer %s" % id_old) + del Buffer._cache[id_old] Buffer._cache_last_cleaned = time() logger.debug("Wrapping new buffer: %s" % id_) From b2e4772300407ea8a1792038fba7a8a78e047670 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 22:48:05 +0000 Subject: [PATCH 20/39] Move method down for easier reading + update comment on dirty check. --- plugin/vimrtags.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 1fd802d3..21b6dbad 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -412,21 +412,15 @@ def _is_really_dirty(self): """ Check the dirty flag and confirm the buffer really is modified. `TextChange` autocmd also triggers just by switching buffers, so we have to be sure. + Unfortunately, even this isn't good - if the buffer hasn't been saved and we switch to + it, then this check will be True, even if the file hasn't changed since the last + reindex. """ self._is_dirty = self._is_dirty and bool(int( vim.eval('getbufvar(%d, "&mod")' % self._vimbuffer.number) )) return self._is_dirty - def _place_signs(self): - """ Add gutter indicator signs next to lines that have diagnostics. - """ - self._reset_signs() - used_ids = Sign.used_ids(self._vimbuffer.number) - logger.debug("Appending %s signs to %s" % (len(self._diagnostics), self._vimbuffer.name)) - for diagnostic in self._diagnostics.values(): - self._place_sign(diagnostic.line_num, diagnostic.type, used_ids) - def _rtags_dirty_reindex(self): """ Reindex unsaved buffer contents in rtags. """ @@ -522,6 +516,15 @@ def _update_loclist(self, force): if force and height > 0: vim.command('lopen %d' % height) + def _place_signs(self): + """ Add gutter indicator signs next to lines that have diagnostics. + """ + self._reset_signs() + used_ids = Sign.used_ids(self._vimbuffer.number) + logger.debug("Appending %s signs to %s" % (len(self._diagnostics), self._vimbuffer.name)) + for diagnostic in self._diagnostics.values(): + self._place_sign(diagnostic.line_num, diagnostic.type, used_ids) + def _place_sign(self, line_num, name, used_ids): """ Create, place and remember a diagnostic gutter sign. """ From 2add51b6f71a61ec1cb95faf42aefc64eb81b176 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 25 Feb 2018 23:53:16 +0000 Subject: [PATCH 21/39] Update some documentation --- doc/rtags.txt | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) mode change 100644 => 100755 doc/rtags.txt diff --git a/doc/rtags.txt b/doc/rtags.txt old mode 100644 new mode 100755 index abc7f026..319e63e4 --- a/doc/rtags.txt +++ b/doc/rtags.txt @@ -79,8 +79,8 @@ g:rtagsUseDefaultMappings Otherwise, no mappings are set up and custom mappings can be configured by a user. - *g:rtagsMinCharsForCommandCompletion* - *rtags-variable-min-chars-for-cmd-compl* + *g:rtagsCppOmnifunc* + *rtags-variable-cpp-omnifunc* g:rtagsMinCharsForCommandCompletion Default: 4. @@ -88,6 +88,14 @@ g:rtagsMinCharsForCommandCompletion available for commands provided by the plugin or pluging mappings that require user input. + *g:rtagsMinCharsForCommandCompletion* + *rtags-variable-min-chars-for-cmd-compl* +g:rtagsCppOmnifunc + + Default: 1. + Override the vim completion |omnifunc| for the cpp filetype. If disabled, then + the |completefunc| will be used instead, but only if it's not already set. + *g:rtagsMaxSearchResultWindowHeight* *rtags-variable-max-search-result-window-height* g:rtagsMaxSearchResultWindowHeight @@ -104,6 +112,15 @@ g:rtagsLog Default: empty When set to filename, rtags will put its logs in that file. + *g:rtagsAutoDiagnostics* + *rtags-variable-rtags-auto-diagnostics* +g:rtagsAutoDiagnostics + + Default: 1 + If enabled then diagnostic gutter signs will be updated in the currently active + buffer automatically, and the diagnostic message will show in the message + window as the cursor moves through the file. + *rtags-mappings* 4. Mappings *rtags-leader-ri* @@ -192,6 +209,24 @@ g:rtagsLog *rtags-FindSubClasses* rc Find the subclasses of the class under the cursor. + *rtags-leader-rd* + *rtags-Diagnostics* + rd Show diagnostics for the current buffer in the location list, + along withing diagnostic signs in the current window's + gutter. + + *rtags-leader-rD* + *rtags-DiagnosticsAll* + rD Show diagnostics for the whole project in the location or + quickfix list (depending on the value of + |g:rtagsUseLocationList|). That is, all the errors and + warnings from rdm's clang build of the current project. + + *rtags-leader-rx* + *rtags-FixIts* + rx Apply diagnostic clang fixits to the current buffer. + + *rtags-commands* 5. Commands From 2e21362440cd170df42a77d1a5ba75c4c8d2df09 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Mon, 26 Feb 2018 22:51:20 +0000 Subject: [PATCH 22/39] More documentation updates --- doc/rtags.txt | 56 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/doc/rtags.txt b/doc/rtags.txt index 319e63e4..ed0ee641 100755 --- a/doc/rtags.txt +++ b/doc/rtags.txt @@ -47,8 +47,8 @@ g:rtagsRdmCmd location if it is installed to a non standard location or the location doesn't appear in the PATH. - *g:rtagsAutoLaunchRdm* - *rtags-variable-auto-launch-rdm* + *g:rtagsAutoLaunchRdm* + *rtags-variable-auto-launch-rdm* g:rtagsAutoLaunchRdm Default: 0. @@ -79,8 +79,8 @@ g:rtagsUseDefaultMappings Otherwise, no mappings are set up and custom mappings can be configured by a user. - *g:rtagsCppOmnifunc* - *rtags-variable-cpp-omnifunc* + *g:rtagsCppOmnifunc* + *rtags-variable-cpp-omnifunc* g:rtagsMinCharsForCommandCompletion Default: 4. @@ -93,8 +93,9 @@ g:rtagsMinCharsForCommandCompletion g:rtagsCppOmnifunc Default: 1. - Override the vim completion |omnifunc| for the cpp filetype. If disabled, then - the |completefunc| will be used instead, but only if it's not already set. + Override the vim completion |omnifunc| for the cpp filetype. If disabled, + then the |completefunc| will be used instead, but only if it's not already + set. *g:rtagsMaxSearchResultWindowHeight* *rtags-variable-max-search-result-window-height* @@ -104,22 +105,39 @@ g:rtagsMaxSearchResultWindowHeight Determines the maximum height of the search result window. When number of results is less than this parameter, the height is set to the number of results. + *g:rtagsAutoDiagnostics* + *rtags-variable-auto-diagnostics* +g:rtagsAutoDiagnostics + + Default: 1 + If enabled then diagnostic gutter signs will be updated in the currently + active buffer automatically, and each line's diagnostic message will show + in the message window as the cursor moves through the file. + + *g:rtagsDiagnosticsPollingInterval* + *rtags-variable-diagnostics-polling-interval* +g:rtagsDiagnosticsPollingInterval + + Default: 3000 + If |g:rtagsAutoDiagnostics| is enabled, then poll for changes to the RTags + project with this interval and update diagnostic signs and lists if changes + are found. Set to <=0 to disable polling. If disabled, updates to + diagnostics will be checked only after |CursorHold| and |BufEnter| events. *g:rtagsLog* *rtags-variable-rtags-log* g:rtagsLog - Default: empty - When set to filename, rtags will put its logs in that file. + Default: |tempname| + File to log vim-rtags output. - *g:rtagsAutoDiagnostics* - *rtags-variable-rtags-auto-diagnostics* -g:rtagsAutoDiagnostics + *g:rtagsRdmLog* + *rtags-variable-rtags-rdm-log* +g:rtagsRdmLog - Default: 1 - If enabled then diagnostic gutter signs will be updated in the currently active - buffer automatically, and the diagnostic message will show in the message - window as the cursor moves through the file. + Default: |tempname| + If |g:rtagsAutoLaunchRdm| is enabled, then the RTags rdm daemon will log + its output to this file. *rtags-mappings* 4. Mappings @@ -222,9 +240,11 @@ g:rtagsAutoDiagnostics |g:rtagsUseLocationList|). That is, all the errors and warnings from rdm's clang build of the current project. - *rtags-leader-rx* - *rtags-FixIts* - rx Apply diagnostic clang fixits to the current buffer. + *rtags-leader-rx* + *rtags-FixIts* + rx Apply diagnostic clang fixits to the current buffer. Fixits + are identified with "Fx" in the sign column, and are + suffixed with "[FIXIT]" in the diagnostics lists. *rtags-commands* From 24eb630e92cf92db0e119312c1d1a606af3c9af9 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 00:40:12 +0000 Subject: [PATCH 23/39] Minor tidying --- plugin/vimrtags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 21b6dbad..d0d1a445 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -47,7 +47,7 @@ def run_rc_command(arguments, content=None): if sys.version_info.major == 3 and sys.version_info.minor >= 5: r = subprocess.run( cmdline, - input=content and content.encode("utf-8"), + input=content and content.encode(encoding), stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -742,7 +742,7 @@ def invalid_buffer_message(filename): def error(msg): - message("""%s: see log file at" "%s" for more information""" % (msg, vim.eval('g:rtagsLog'))) + message("""%s: see log file at "%s" for more information""" % (msg, vim.eval('g:rtagsLog'))) def message(msg): From b81e0920eafc36bf15a25c265f387bf8a3c4162c Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 00:41:06 +0000 Subject: [PATCH 24/39] Don't include docstring in completion if it just repeats the completion --- plugin/vimrtags.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index d0d1a445..323c6fcf 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -128,8 +128,9 @@ def parse_completion_result(data): def _completion_description(completion, fields): + fields += ['brief_comment'] fields = [field for field in fields if completion[field] != completion['completion']] - fields = ['completion'] + fields + ['brief_comment'] + fields = ['completion'] + fields return " -- ".join(filter(None, [completion[field] for field in fields])) From 8fcab219e392e5b0bbb73a9d8a57b3db97d20c37 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 00:42:16 +0000 Subject: [PATCH 25/39] Separate out buffer cache cleaning for easier testing and readability --- plugin/vimrtags.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 323c6fcf..e7a98289 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -198,6 +198,17 @@ def get(id_): return buff # Periodically clean closed buffers + Buffer._clean_cache() + + logger.debug("Wrapping new buffer: %s" % id_) + buff = Buffer(vim.buffers[id_]) + Buffer._cache[id_] = buff + return buff + + @staticmethod + def _clean_cache(): + """ Periodically clean closed buffers + """ if time() - Buffer._cache_last_cleaned > Buffer._CACHE_CLEAN_PERIOD: logger.debug("Cleaning invalid buffers") for id_old in list(Buffer._cache.keys()): @@ -206,11 +217,6 @@ def get(id_): del Buffer._cache[id_old] Buffer._cache_last_cleaned = time() - logger.debug("Wrapping new buffer: %s" % id_) - buff = Buffer(vim.buffers[id_]) - Buffer._cache[id_] = buff - return buff - @staticmethod def show_all_diagnostics(): """ Get all diagnostics for all files and show in quickfix list. From a65d808e03f2b04874df0bd009437d81e73cf6f8 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 00:46:17 +0000 Subject: [PATCH 26/39] Unit tests of python code --- plugin/__init__.py | 0 plugin/vimrtags.py | 2 + tests/test_vimrtags.py | 1246 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1248 insertions(+) create mode 100644 plugin/__init__.py create mode 100644 tests/test_vimrtags.py diff --git a/plugin/__init__.py b/plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index e7a98289..5054b2b6 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -8,6 +8,8 @@ from time import time +print = print # For unit tests + loglevel = logging.DEBUG logger = logging.getLogger(__name__) diff --git a/tests/test_vimrtags.py b/tests/test_vimrtags.py new file mode 100644 index 00000000..675089a9 --- /dev/null +++ b/tests/test_vimrtags.py @@ -0,0 +1,1246 @@ +import json +import logging +import sys +from unittest import TestCase +from unittest.mock import Mock, MagicMock, patch, call + +from nose.tools import set_trace + +# Create a mocked vim in the environment. +vim = MagicMock() +sys.modules["vim"] = vim + +with patch.object(logging, "FileHandler"): + from plugin import vimrtags + +vim.reset_mock() + + +class VimRtagsTest(TestCase): + def setUp(self): + vim.reset_mock() + + +@patch("plugin.vimrtags.logging", autospec=True) +@patch("plugin.vimrtags.logger", autospec=True) +class Test_configure_logger(VimRtagsTest): + + def test_logs_to_user_defined_file(self, logger, logging): + vimrtags.configure_logger() + + vim.eval.assert_called_once_with('g:rtagsLog') + logging.FileHandler.assert_called_once_with(vim.eval.return_value) + logger.addHandler.assert_called_once_with(logging.FileHandler.return_value) + + +class Test_parse_completion_result(VimRtagsTest): + + def test(self): + completions = [] + for kind in ( + 'FunctionDecl', 'FunctionTemplate', 'CXXMethod', 'CXXConstructor', 'VarDecl', + 'macro definition', 'EnumDecl', 'TypedefDecl', 'StructDecl', 'EnumConstantDecl', + 'ClassDecl', 'FieldDecl', 'Unknown' + ): + completions += self._completion(kind) + + completions = {'completions': completions} + + parsed = vimrtags.parse_completion_result(json.dumps(completions)) + + self.assertListEqual( + parsed, [ + self._with_parent_sig('f'), self._simple('f'), self._simple('f'), + self._with_parent_sig('f'), self._simple('f'), self._simple('f'), + self._with_parent_sig('m'), self._simple('m'), self._simple('m'), + self._with_parent_sig('m'), self._simple('m'), self._simple('m'), + self._with_parent_sig('v'), self._simple('v'), self._simple('v'), + self._with_sig('d'), self._simple('d'), self._simple('d'), + self._with_parent('e'), self._simple('e'), self._simple('e'), + self._with_parent('t'), self._simple('t'), self._simple('t'), + self._with_parent('t'), self._simple('t'), self._simple('t'), + self._with_parent('t'), self._simple('t'), self._simple('t'), + self._with_parent('t'), self._simple('t'), self._simple('t'), + self._with_parent('t'), self._simple('t'), self._simple('t'), + self._with_comment(''), self._simple(''), self._simple('') + ] + ) + + def _completion(self, kind): + return [{ + 'kind': kind, + 'completion': 'mock completion', + 'parent': 'mock parent', + 'signature': 'mock signature', + 'brief_comment': 'mock comment', + }, { + 'kind': kind, + 'completion': 'mock completion', + 'parent': 'mock completion', + 'signature': 'mock completion', + 'brief_comment': 'mock completion', + }, { + 'kind': kind, + 'completion': 'mock completion', + 'parent': '', + 'signature': '', + 'brief_comment': '', + }] + + def _with_parent_sig(self, kind): + return { + 'menu': "mock completion -- mock parent -- mock signature -- mock comment", + 'word': "mock completion", 'kind': kind + } + + def _with_sig(self, kind): + return { + 'menu': "mock completion -- mock signature -- mock comment", 'word': "mock completion", + 'kind': kind + } + + def _with_parent(self, kind): + return { + 'menu': "mock completion -- mock parent -- mock comment", 'word': "mock completion", + 'kind': kind + } + + def _with_comment(self, kind): + return { + 'menu': "mock completion -- mock comment", 'word': "mock completion", 'kind': kind + } + + def _simple(self, kind): + return { + 'menu': "mock completion", 'word': "mock completion", 'kind': kind + } + + +class Test_Buffer_find(VimRtagsTest): + def setUp(self): + super(Test_Buffer_find, self).setUp() + first = Mock() + second = Mock(number=9) + third = Mock() + first.name = "first buf" + second.name = "second buf" + third.name = "third buf" + vim.buffers = [first, second, third] + + def test_not_found(self): + buffer = vimrtags.Buffer.find("wont find") + self.assertIsNone(buffer) + + @patch("plugin.vimrtags.Buffer.get") + def test_found(self, get): + buffer = vimrtags.Buffer.find("second buf") + get.assert_called_once_with(9) + self.assertIs(buffer, get.return_value) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Buffer._cache", {}) +class Test_Buffer_get(VimRtagsTest): + def test_found(self): + cached_buffer = Mock() + vimrtags.Buffer._cache = {3: cached_buffer} + buffer = vimrtags.Buffer.get(3) + self.assertIs(buffer, cached_buffer) + + @patch("plugin.vimrtags.Project", MagicMock()) + @patch("plugin.vimrtags.Buffer._clean_cache") + def test_not_found(self, _clean_cache): + vimbuffer = Mock() + vim.buffers.append(vimbuffer) + other_buffer = Mock() + vimrtags.Buffer._cache = {4: other_buffer} + + buffer = vimrtags.Buffer.get(3) + buffer_again = vimrtags.Buffer.get(3) + + _clean_cache.assert_called_once_with() + self.assertIsInstance(buffer, vimrtags.Buffer) + self.assertIs(buffer._vimbuffer, vimbuffer) + self.assertDictEqual(vimrtags.Buffer._cache, {3: buffer, 4: other_buffer}) + self.assertIs(buffer, buffer_again) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.time", autospec=True) +@patch("plugin.vimrtags.Buffer._cache", {}) +@patch("plugin.vimrtags.Buffer._cache_last_cleaned", 6) +@patch("plugin.vimrtags.Buffer._CACHE_CLEAN_PERIOD", 4) +class Test_Buffer__clean_cache(VimRtagsTest): + def prepare(self): + self.buffer = Mock(_vimbuffer=Mock(valid=False)) + vimrtags.Buffer._cache = {3: self.buffer} + + def test_throttled(self, time): + self.prepare() + time.return_value = 10 + vimrtags.Buffer._clean_cache() + self.assertIs(vimrtags.Buffer._cache[3], self.buffer) + + def test_removes(self, time): + self.prepare() + time.return_value = 11 + vimrtags.Buffer._clean_cache() + self.assertDictEqual(vimrtags.Buffer._cache, {}) + + +@patch("plugin.vimrtags.run_rc_command", autospec=True) +class Test_Buffer_show_all_diagnostics(VimRtagsTest): + + @patch("plugin.vimrtags.error", autospec=True) + def test_rc_fails(self, error, run_rc_command): + run_rc_command.return_value = None + vimrtags.Buffer.show_all_diagnostics() + error.assert_called() + self.assertFalse(vim.command.called) + + @patch("plugin.vimrtags.message", autospec=True) + def test_no_diagnostics(self, message, run_rc_command): + run_rc_command.return_value = '{"checkStyle": {}}' + vimrtags.Buffer.show_all_diagnostics() + message.assert_called() + self.assertFalse(vim.command.called) + + def prepare(self, get_rtags_variable, Diagnostic, run_rc_command): + run_rc_command.return_value = ( + '{"checkStyle": {"file 1": ["error 1", "error 2"], "file 2": ["error 3"]}}' + ) + Diagnostic.from_rtags_errors.side_effect = [["diag 1", "diag 2"], ["diag 3"]] + Diagnostic.to_qlist_errors.return_value = (123, "lines") + vim.current.window.number = 7 + + @patch("plugin.vimrtags.Buffer._DIAGNOSTICS_ALL_LIST_TITLE", "list title") + @patch("plugin.vimrtags.Diagnostic", autospec=True) + @patch("plugin.vimrtags.get_rtags_variable", autospec=True) + def test_use_loclist(self, get_rtags_variable, Diagnostic, run_rc_command): + self.prepare(get_rtags_variable, Diagnostic, run_rc_command) + + get_rtags_variable.return_value = 1 + vimrtags.Buffer.show_all_diagnostics() + + self.assert_diagnostics_constructed(Diagnostic) + vim.eval.assert_called_once_with( + 'setloclist(7, [], " ", {"items": lines, "title": "list title"})' + ) + vim.command.assert_called_once_with("lopen 123") + + @patch("plugin.vimrtags.Buffer._DIAGNOSTICS_ALL_LIST_TITLE", "list title") + @patch("plugin.vimrtags.Diagnostic", autospec=True) + @patch("plugin.vimrtags.get_rtags_variable", autospec=True) + def test_use_qlist(self, get_rtags_variable, Diagnostic, run_rc_command): + self.prepare(get_rtags_variable, Diagnostic, run_rc_command) + + get_rtags_variable.return_value = 0 + vimrtags.Buffer.show_all_diagnostics() + + self.assert_diagnostics_constructed(Diagnostic) + vim.eval.assert_called_once_with( + 'setqflist([], " ", {"items": lines, "title": "list title"})' + ) + vim.command.assert_called_once_with("copen 123") + + def assert_diagnostics_constructed(self, Diagnostic): + self.assertListEqual( + Diagnostic.from_rtags_errors.call_args_list, [ + call("file 1", ["error 1", "error 2"]), + call("file 2", ["error 3"]) + ] + ) + Diagnostic.to_qlist_errors.assert_called_once_with(["diag 1", "diag 2", "diag 3"]) + + +@patch("plugin.vimrtags.Project", autospec=True) +class Test_Buffer_init(VimRtagsTest): + def test(self, Project): + vimbuffer = Mock() + vimbuffer.name = "mock buffer" + + buffer = vimrtags.Buffer(vimbuffer) + + self.assertIs(buffer._vimbuffer, vimbuffer) + Project.get.assert_called_once_with("mock buffer") + self.assertIs(buffer._project, Project.get.return_value) + + +class MockVimBuffer(list): + def __init__(self): + super(MockVimBuffer, self).__init__() + self.name = "mock buffer" + self.number = 7 + + +class BufferInstanceTest(VimRtagsTest): + def setUp(self): + super(BufferInstanceTest, self).setUp() + self.project = Mock(spec=vimrtags.Project) + self.vimbuffer = MockVimBuffer() + + with patch("plugin.vimrtags.Project.get", Mock(return_value=self.project)): + self.buffer = vimrtags.Buffer(self.vimbuffer) + + +class Test_Buffer_on_write(BufferInstanceTest): + def test(self): + self.buffer._is_dirty = True + + self.buffer.on_write() + + self.assertFalse(self.buffer._is_dirty) + + +class Test_Buffer_on_edit(BufferInstanceTest): + def test_no_project(self): + self.buffer._project = None + + self.buffer.on_edit() + + self.assertFalse(self.buffer._is_dirty) + + def test_has_project(self): + self.buffer.on_edit() + + self.assertTrue(self.buffer._is_dirty) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Buffer._is_really_dirty", autospec=True) +@patch("plugin.vimrtags.Buffer._rtags_dirty_reindex", autospec=True) +@patch("plugin.vimrtags.Buffer._update_diagnostics", autospec=True) +class Test_Buffer_on_idle(BufferInstanceTest): + def test_no_project(self, _update_diagnostics, _rtags_dirty_reindex, _is_really_dirty): + self.buffer._project = None + + self.buffer.on_idle() + + self.assertFalse(_is_really_dirty.called) + self.assertFalse(_update_diagnostics.called) + self.assertFalse(_rtags_dirty_reindex.called) + + def test_nothing_to_do(self, _update_diagnostics, _rtags_dirty_reindex, _is_really_dirty): + _is_really_dirty.return_value = False + self.buffer._last_diagnostics_time = 1 + self.project.last_updated_time.return_value = 1 + + self.buffer.on_idle() + + self.assertFalse(_update_diagnostics.called) + self.assertFalse(_rtags_dirty_reindex.called) + + @patch("plugin.vimrtags.time", autospec=True) + def test_reindex(self, time, _update_diagnostics, _rtags_dirty_reindex, _is_really_dirty): + _is_really_dirty.return_value = True + + self.buffer.on_idle() + + self.assertIs(self.buffer._last_diagnostics_time, time.return_value) + _rtags_dirty_reindex.assert_called_once_with(self.buffer) + self.assertFalse(_update_diagnostics.called) + + def test_update_diagnostics(self, _update_diagnostics, _rtags_dirty_reindex, _is_really_dirty): + _is_really_dirty.return_value = False + self.buffer._last_diagnostics_time = 1 + self.project.last_updated_time.return_value = 2 + + self.buffer.on_idle() + + self.assertFalse(_rtags_dirty_reindex.called) + _update_diagnostics.assert_called_once_with(self.buffer) + + +@patch("plugin.vimrtags.Buffer._is_really_dirty", autospec=True) +@patch("plugin.vimrtags.Buffer._update_diagnostics", autospec=True) +class Test_Buffer_on_poll(BufferInstanceTest): + def test_no_project(self, _update_diagnostics, _is_really_dirty): + self.buffer._project = None + + self.buffer.on_poll() + + self.assertFalse(_is_really_dirty.called) + self.assertFalse(_update_diagnostics.called) + + def test_is_dirty(self, _update_diagnostics, _is_really_dirty): + _is_really_dirty.return_value = True + self.buffer._last_diagnostics_time = 1 + self.project.last_updated_time.return_value = 2 + + self.buffer.on_poll() + + self.assertFalse(_update_diagnostics.called) + + def test_too_soon(self, _update_diagnostics, _is_really_dirty): + _is_really_dirty.return_value = False + self.buffer._last_diagnostics_time = 2 + self.project.last_updated_time.return_value = 2 + + self.buffer.on_poll() + + self.assertFalse(_update_diagnostics.called) + + @patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) + def test_update(self, _update_diagnostics, _is_really_dirty): + _is_really_dirty.return_value = False + self.buffer._last_diagnostics_time = 1 + self.project.last_updated_time.return_value = 2 + + self.buffer.on_poll() + + _update_diagnostics.assert_called_once_with(self.buffer) + + +@patch("plugin.vimrtags.print", autospec=True) +class Test_Buffer_on_cursor_moved(BufferInstanceTest): + def test_no_project(self, print_): + self.buffer._project = None + + self.buffer.on_cursor_moved() + + self.assertFalse(print_.called) + + def test_line_num_not_changed(self, print_): + self.buffer._line_num_last = 2 + vim.current.window.cursor = (2, 123) + + self.buffer.on_cursor_moved() + + self.assertFalse(print_.called) + + def test_no_diagnostic_now_or_before(self, print_): + self.buffer._line_num_last = 1 + vim.current.window.cursor = (2, 123) + self.buffer._diagnostics = {3: Mock(text="mock diagnostic")} + + self.buffer.on_cursor_moved() + + self.assertFalse(print_.called) + + def test_has_diagnostic(self, print_): + self.buffer._line_num_last = 1 + vim.current.window.cursor = (3, 123) + self.buffer._diagnostics = {3: Mock(text="mock diagnostic")} + + self.buffer.on_cursor_moved() + + print_.assert_called_once_with("mock diagnostic") + self.assertTrue(self.buffer._is_line_diagnostic_shown) + + def test_no_diagnostic_now_but_had_one_before(self, print_): + self.buffer._line_num_last = 1 + vim.current.window.cursor = (2, 123) + self.buffer._diagnostics = {3: Mock(text="mock diagnostic")} + self.buffer._is_line_diagnostic_shown = True + + self.buffer.on_cursor_moved() + + print_.assert_called_once_with("") + self.assertFalse(self.buffer._is_line_diagnostic_shown) + + +@patch("plugin.vimrtags.Buffer._update_diagnostics", autospec=True) +class Test_Buffer_show_diagnostics_list(BufferInstanceTest): + + @patch("plugin.vimrtags.invalid_buffer_message", autospec=True) + def test_no_project(self, invalid_buffer_message, _update_diagnostics): + self.buffer._project = None + + self.buffer.show_diagnostics_list() + + self.assertFalse(_update_diagnostics.called) + invalid_buffer_message.assert_called_once_with("mock buffer") + + @patch("plugin.vimrtags.message", autospec=True) + def test_has_diagnostics(self, message, _update_diagnostics): + + def set_diagnostics(*args, **kwargs): + self.buffer._diagnostics = "something" + + _update_diagnostics.side_effect = set_diagnostics + + self.buffer.show_diagnostics_list() + + _update_diagnostics.assert_called_once_with(self.buffer, open_loclist=True) + self.assertFalse(message.called) + + @patch("plugin.vimrtags.message", autospec=True) + def test_no_diagnostics(self, message, _update_diagnostics): + + def set_diagnostics(*args, **kwargs): + self.buffer._diagnostics = None + + _update_diagnostics.side_effect = set_diagnostics + + self.buffer.show_diagnostics_list() + + _update_diagnostics.assert_called_once_with(self.buffer, open_loclist=True) + self.assertTrue(message.called) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.get_rtags_variable", autospec=True) +@patch("plugin.vimrtags.run_rc_command", autospec=True) +@patch("plugin.vimrtags.Buffer._rtags_is_reindexing", autospec=True) +class Test_Buffer_apply_fixits(BufferInstanceTest): + + def prepare(self, _rtags_is_reindexing, get_rtags_variable): + _rtags_is_reindexing.return_value = False + get_rtags_variable.side_effect = ["1", "20"] + self.buffer._last_diagnostics_time = 1 + self.buffer._project.last_updated_time.return_value = 1 + + @patch("plugin.vimrtags.invalid_buffer_message", autospec=True) + def test_no_project( + self, invalid_buffer_message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + self.buffer._project = None + + self.buffer.apply_fixits() + + invalid_buffer_message.assert_called_once_with("mock buffer") + self.assertFalse(_rtags_is_reindexing.called) + self.assertFalse(run_rc_command.called) + self.assertFalse(vim.eval.called) + self.assertFalse(vim.command.called) + + @patch("plugin.vimrtags.message", autospec=True) + def test_is_reindexing( + self, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + _rtags_is_reindexing.return_value = True + + self.buffer.apply_fixits() + + self.assertTrue(message.called) + self.assertFalse(run_rc_command.called) + self.assertFalse(vim.command.called) + + @patch("plugin.vimrtags.message", autospec=True) + def test_has_changes_to_project( + self, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + self.buffer._project.last_updated_time.return_value = 2 + + self.buffer.apply_fixits() + + self.assertTrue(message.called) + self.assertFalse(run_rc_command.called) + self.assertFalse(vim.command.called) + + @patch("plugin.vimrtags.error", autospec=True) + def test_rc_fails( + self, error, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + run_rc_command.return_value = None + + self.buffer.apply_fixits() + + run_rc_command.assert_called_once_with(['--fixits', 'mock buffer']) + self.assertTrue(error.called) + self.assertFalse(vim.command.called) + + @patch("plugin.vimrtags.message", autospec=True) + def test_no_fixits_found( + self, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + run_rc_command.return_value = " " + vim.current.window.number = 5 + + self.buffer.apply_fixits() + + run_rc_command.assert_called_once_with(['--fixits', 'mock buffer']) + self.assertTrue(message.called) + self.assertFalse(vim.command.called) + + def prepare_buffer(self, run_rc_command, dumps): + fixit_txt = """ +some junk +1:2 3 first +10:11 12 second +""" + run_rc_command.return_value = fixit_txt + vim.current.window.number = 5 + dumps.return_value = "some json" + self.vimbuffer += [ + "some mock text to be fixed", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "some other mock text to be fixed", + "not modified", + "not modified" + ] + + def assert_buffer_and_loclist(self, dumps, message): + self.assertListEqual( + self.vimbuffer, [ + "sfirst mock text to be fixed", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "not modified", + "some othersecondo be fixed", + "not modified", + "not modified" + ]) + + dumps.assert_called_once_with([ + {'lnum': 1, 'col': 2, 'text': "first", 'filename': "mock buffer"}, + {'lnum': 10, 'col': 11, 'text': "second", 'filename': "mock buffer"} + ]) + vim.eval.assert_called_once_with( + 'setloclist(5, [], " ", {"items": some json, "title": "list title"})' + ) + self.assertTrue(message.called) + + @patch("plugin.vimrtags.Buffer._FIXITS_LIST_TITLE", "list title") + @patch("plugin.vimrtags.message", autospec=True) + @patch("plugin.vimrtags.json.dumps", autospec=True) + def test_fixits_found_fits_in_max_height( + self, dumps, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + self.prepare_buffer(run_rc_command, dumps) + + self.buffer.apply_fixits() + + self.assert_buffer_and_loclist(dumps, message) + vim.command.assert_called_once_with('lopen 2') + + @patch("plugin.vimrtags.Buffer._FIXITS_LIST_TITLE", "list title") + @patch("plugin.vimrtags.message", autospec=True) + @patch("plugin.vimrtags.json.dumps", autospec=True) + def test_fixits_found_doesnt_fit_in_max_height( + self, dumps, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + self.prepare_buffer(run_rc_command, dumps) + get_rtags_variable.side_effect = ["1", "1"] + + self.buffer.apply_fixits() + + self.assert_buffer_and_loclist(dumps, message) + vim.command.assert_called_once_with('lopen 1') + + @patch("plugin.vimrtags.Buffer._FIXITS_LIST_TITLE", "list title") + @patch("plugin.vimrtags.message", autospec=True) + @patch("plugin.vimrtags.json.dumps", autospec=True) + def test_fixits_found_when_auto_diagnostics_disabled( + self, dumps, message, _rtags_is_reindexing, run_rc_command, get_rtags_variable + ): + self.prepare(_rtags_is_reindexing, get_rtags_variable) + self.prepare_buffer(run_rc_command, dumps) + get_rtags_variable.side_effect = ["0", "20"] + self.buffer._project.last_updated_time.return_value = 2 + + self.buffer.apply_fixits() + + self.assert_buffer_and_loclist(dumps, message) + vim.command.assert_called_once_with('lopen 2') + + +class Test_Buffer__is_really_dirty(BufferInstanceTest): + def test_not_dirty(self): + self.buffer._is_dirty = False + + is_dirty = self.buffer._is_really_dirty() + + self.assertFalse(is_dirty) + self.assertFalse(vim.eval.called) + + def test_is_dirty_but_not_really(self): + self.buffer._is_dirty = True + vim.eval.return_value = "0" + + is_dirty = self.buffer._is_really_dirty() + + vim.eval.assert_called_once_with('getbufvar(7, "&mod")') + self.assertFalse(is_dirty) + + def test_is_dirty_really(self): + self.buffer._is_dirty = True + vim.eval.return_value = "1" + + is_dirty = self.buffer._is_really_dirty() + + vim.eval.assert_called_once_with('getbufvar(7, "&mod")') + self.assertTrue(is_dirty) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.run_rc_command", autospec=True) +class Test_Buffer__rtags_dirty_reindex(BufferInstanceTest): + def test(self, run_rc_command): + self.vimbuffer += ["ab", "cd"] + self.buffer._is_dirty = True + + self.buffer._rtags_dirty_reindex() + + run_rc_command.assert_called_once_with([ + '--json', '--reindex', "mock buffer", '--unsaved-file', 'mock buffer:5' + ], "ab\ncd") + self.assertFalse(self.buffer._is_dirty) + + +@patch("plugin.vimrtags.run_rc_command", autospec=True) +class Test_Buffer__rtags_is_reindexing(BufferInstanceTest): + + @patch("plugin.vimrtags.error", autospec=True) + def test_rc_fails(self, error, run_rc_command): + run_rc_command.return_value = None + + is_reindexing = self.buffer._rtags_is_reindexing() + + self.assertTrue(error.called) + self.assertIs(is_reindexing, error.return_value) + + def test_not_reindexing(self, run_rc_command): + run_rc_command.return_value = "something" + + is_reindexing = self.buffer._rtags_is_reindexing() + + self.assertFalse(is_reindexing) + + def test_is_reindexing(self, run_rc_command): + run_rc_command.return_value = "something mock buffer something" + + is_reindexing = self.buffer._rtags_is_reindexing() + + self.assertTrue(is_reindexing) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.time", autospec=True) +@patch("plugin.vimrtags.run_rc_command", autospec=True) +@patch("plugin.vimrtags.Diagnostic", autospec=True) +@patch("plugin.vimrtags.Buffer._update_loclist", autospec=True) +@patch("plugin.vimrtags.Buffer._place_signs", autospec=True) +@patch("plugin.vimrtags.Buffer.on_cursor_moved", autospec=True) +class Test_Buffer__update_diagnostics(BufferInstanceTest): + + @patch("plugin.vimrtags.error", autospec=True) + def test_rc_failed( + self, error, on_cursor_moved, _place_signs, _update_loclist, Diagnostic, run_rc_command, + time + ): + run_rc_command.return_value = None + + self.buffer._update_diagnostics() + + self.assertTrue(error.called) + self.assertFalse(_update_loclist.called) + self.assertFalse(_place_signs.called) + + def test_success( + self, on_cursor_moved, _place_signs, _update_loclist, Diagnostic, run_rc_command, time + ): + # setup + + self.buffer._diagnostics = "replace me" + self.buffer._line_num_last = "replace me" + open_loclist = Mock() + + run_rc_command.return_value = '{"checkStyle": {"mock buffer": ["diag 1", "diag 2"]}}' + + diag1 = Mock(line_num=3) + diag2 = Mock(line_num=12) + Diagnostic.from_rtags_errors.return_value = [diag1, diag2] + + on_cursor_moved.side_effect = ( + lambda *a, **k: self.assertEqual(self.buffer._line_num_last, -1) + ) + + # action + + self.buffer._update_diagnostics(open_loclist=open_loclist) + + # confirm + + run_rc_command.assert_called_once_with([ + '--diagnose', "mock buffer", '--synchronous-diagnostics', '--json' + ]) + Diagnostic.from_rtags_errors.assert_called_once_with("mock buffer", ["diag 1", "diag 2"]) + + self.assertDictEqual(self.buffer._diagnostics, {3: diag1, 12: diag2}) + + _update_loclist.assert_called_once_with(self.buffer, open_loclist) + _place_signs.assert_called_once_with(self.buffer) + on_cursor_moved.assert_called_once_with(self.buffer) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Buffer._DIAGNOSTICS_LIST_TITLE", "list title") +@patch("plugin.vimrtags.Diagnostic", autospec=True) +class Test_Buffer__update_loclist(BufferInstanceTest): + def prepare(self, Diagnostic): + vim.current.window.number = 6 + vim.current.window.buffer.number = 7 + vim.eval.return_value = {'title': "list title"} + Diagnostic.to_qlist_errors.return_value = (3, "some json") + + def test_not_current_window(self, Diagnostic): + self.prepare(Diagnostic) + vim.current.window.buffer.number = 123 + + self.buffer._update_loclist(False) + + self.assertFalse(Diagnostic.to_qlist_errors.called) + self.assertFalse(vim.eval.called) + self.assertFalse(vim.command.called) + + def test_not_force_not_already_open(self, Diagnostic): + self.prepare(Diagnostic) + vim.eval.return_value = {'title': "some other loclist"} + + self.buffer._update_loclist(False) + + vim.eval.assert_called_once_with('getloclist(6, {"title": 0})') + self.assertFalse(Diagnostic.to_qlist_errors.called) + self.assertFalse(vim.command.called) + + def test_not_force_and_already_open(self, Diagnostic): + self.prepare(Diagnostic) + + self.buffer._update_loclist(False) + + self.assertListEqual( + vim.method_calls, [ + call.eval('getloclist(6, {"title": 0})'), + call.eval('setloclist(6, [], "r", {"items": some json, "title": "list title"})') + ] + ) + + def test_force_and_already_open_no_results(self, Diagnostic): + self.prepare(Diagnostic) + Diagnostic.to_qlist_errors.return_value = (0, "some json") + + self.buffer._update_loclist(True) + + self.assertListEqual( + vim.method_calls, [ + call.eval('getloclist(6, {"title": 0})'), + call.eval('setloclist(6, [], "r", {"items": some json, "title": "list title"})') + ] + ) + + def test_force_and_already_open_has_results(self, Diagnostic): + self.prepare(Diagnostic) + + self.buffer._update_loclist(True) + + self.assertListEqual( + vim.method_calls, [ + call.eval('getloclist(6, {"title": 0})'), + call.eval('setloclist(6, [], "r", {"items": some json, "title": "list title"})'), + call.command('lopen 3') + ] + ) + + def test_force_not_already_open_has_results(self, Diagnostic): + self.prepare(Diagnostic) + vim.eval.return_value = {'title': "some other loclist"} + + self.buffer._update_loclist(True) + + self.assertListEqual( + vim.method_calls, [ + call.eval('getloclist(6, {"title": 0})'), + call.eval('setloclist(6, [], " ", {"items": some json, "title": "list title"})'), + call.command('lopen 3') + ] + ) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Sign", autospec=True) +@patch("plugin.vimrtags.Buffer._reset_signs") +@patch("plugin.vimrtags.Buffer._place_sign") +class Test_Buffer__place_signs(BufferInstanceTest): + + def test(self, _place_sign, _reset_signs, Sign): + self.buffer._diagnostics = { + 3: Mock(spec=vimrtags.Diagnostic, line_num=4, type="a"), + 56: Mock(spec=vimrtags.Diagnostic, line_num=65, type="b") + } + calls = MagicMock() + calls.attach_mock(_reset_signs, "reset") + calls.attach_mock(_place_sign, "place") + + self.buffer._place_signs() + + Sign.used_ids.assert_called_once_with(7) + self.assertListEqual( + calls.method_calls, [ + call.reset(), + call.place(4, "a", Sign.used_ids.return_value), + call.place(65, "b", Sign.used_ids.return_value) + ] + ) + + +@patch("plugin.vimrtags.Sign", autospec=True) +class Test_Buffer__place_sign(BufferInstanceTest): + def test(self, Sign): + Sign.START_ID = 100 + Sign.side_effect = lambda id_, *a: Mock(id=id_) + used_ids = set([50, 102]) + + self.buffer._place_sign(123, "name 1", used_ids) + self.buffer._place_sign(456, "name 2", used_ids) + self.buffer._place_sign(789, "name 3", used_ids) + + self.assertListEqual( + Sign.call_args_list, [ + call(101, 123, "name 1", 7), + call(103, 456, "name 2", 7), + call(104, 789, "name 3", 7) + ] + ) + self.assertEqual(len(self.buffer._signs), 3) + self.assertEqual(self.buffer._signs[0].id, 101) + self.assertEqual(self.buffer._signs[1].id, 103) + self.assertEqual(self.buffer._signs[2].id, 104) + + +class Test_Buffer__reset_signs(BufferInstanceTest): + def test(self): + sign1 = Mock(spec=vimrtags.Sign) + sign2 = Mock(spec=vimrtags.Sign) + self.buffer._signs = [sign1, sign2] + + self.buffer._reset_signs() + + sign1.unplace.assert_called_once_with() + sign2.unplace.assert_called_once_with() + self.assertListEqual(self.buffer._signs, []) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.run_rc_command", autospec=True) +class Test_Project_get(VimRtagsTest): + + def test_no_filename(self, run_rc_command): + project = vimrtags.Project.get("") + + self.assertFalse(run_rc_command.called) + self.assertIsNone(project) + + def test_rc_fails(self, run_rc_command): + run_rc_command.return_value = None + + project = vimrtags.Project.get("/path/to/file.ext") + + run_rc_command.assert_called_once_with(['--project', "/path/to/file.ext"]) + self.assertIsNone(project) + + def test_no_project_root(self, run_rc_command): + run_rc_command.return_value = "No matches found" + + project = vimrtags.Project.get("/path/to/file.ext") + + run_rc_command.assert_called_once_with(['--project', "/path/to/file.ext"]) + self.assertIsNone(project) + + @patch("plugin.vimrtags.Project._cache", None) + @patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") + def test_known_data_dir_known_project(self, run_rc_command): + run_rc_command.return_value = " /project/root " + cached_project = Mock() + vimrtags.Project._cache = {"/other/project": Mock(), "/project/root": cached_project} + + project = vimrtags.Project.get("/path/to/file.ext") + + run_rc_command.assert_called_once_with(['--project', "/path/to/file.ext"]) + self.assertIs(project, cached_project) + + @patch("plugin.vimrtags.Project._cache", None) + @patch("plugin.vimrtags.Project._rtags_data_dir", None) + def test_unknown_data_dir_known_project(self, run_rc_command): + run_rc_command.side_effect = [ + " /project/root ", + """ +some junk +dataDir: /rtags/data +other junk +""" + ] + cached_project = Mock() + vimrtags.Project._cache = {"/other/project": Mock(), "/project/root": cached_project} + + project = vimrtags.Project.get("/path/to/file.ext") + + self.assertListEqual( + run_rc_command.call_args_list, [ + call(['--project', "/path/to/file.ext"]), + call(['--status', 'info']) + ] + ) + self.assertIs(project, cached_project) + self.assertEqual(vimrtags.Project._rtags_data_dir, "/rtags/data") + + @patch("plugin.vimrtags.Project._cache", None) + @patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") + def test_known_data_dir_unknown_project(self, run_rc_command): + run_rc_command.return_value = " /project/root " + other_project = Mock() + vimrtags.Project._cache = {"/other/project": other_project} + + project = vimrtags.Project.get("/path/to/file.ext") + + run_rc_command.assert_called_once_with(['--project', "/path/to/file.ext"]) + self.assertIsInstance(project, vimrtags.Project) + self.assertEqual(project._project_root, "/project/root") + self.assertDictEqual( + vimrtags.Project._cache, { + "/other/project": other_project, "/project/root": project + } + ) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") +class Test_Project_init(VimRtagsTest): + def test(self): + project = vimrtags.Project("/project/root/") + + self.assertEqual(project._project_root, "/project/root/") + self.assertEqual(project._db_path, "/data/dir/_project_root_/project") + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") +@patch("plugin.vimrtags.os.path.getmtime", autospec=True) +class Test_Project_last_updated_time(VimRtagsTest): + def test(self, getmtime): + project = vimrtags.Project("/file/path/") + + mtime = project.last_updated_time() + + getmtime.assert_called_once_with("/data/dir/_file_path_/project") + self.assertIs(mtime, getmtime.return_value) + + +class Test_Diagnostic_from_rtags_errors(VimRtagsTest): + def test(self): + errors = [ + {'type': "type 1", 'line': 123, 'column': 345, 'message': "a Issue: message 1"}, + {'type': "skipped"}, + {'type': "type 2", 'line': 567, 'column': 789, 'message': "a Issue: message 2"} + ] + + diagnostics = vimrtags.Diagnostic.from_rtags_errors("/some/file.ext", errors) + + self.assertEqual(len(diagnostics), 2) + self.assertEqual(diagnostics[0]._filename, "/some/file.ext") + self.assertEqual(diagnostics[0].line_num, 123) + self.assertEqual(diagnostics[0]._char_num, 345) + self.assertEqual(diagnostics[0].type, "type 1") + self.assertEqual(diagnostics[0].text, "message 1") + self.assertEqual(diagnostics[1]._filename, "/some/file.ext") + self.assertEqual(diagnostics[1].line_num, 567) + self.assertEqual(diagnostics[1]._char_num, 789) + self.assertEqual(diagnostics[1].type, "type 2") + self.assertEqual(diagnostics[1].text, "message 2") + + +@patch("plugin.vimrtags.get_rtags_variable", autospec=True) +@patch("plugin.vimrtags.Diagnostic._to_qlist_dict", autospec=True) +class Test_Diagnostic_to_qlist_errors(VimRtagsTest): + def prepare(self, _to_qlist_dict): + self.diagnostics = [ + vimrtags.Diagnostic("file2", 3, 1, "W", "message"), + vimrtags.Diagnostic("file2", 4, 2, "E", "message"), + vimrtags.Diagnostic("file2", 4, 3, "W", "message"), + vimrtags.Diagnostic("file1", 4, 4, "W", "message") + ] + # Unused char_num property used to record original ordering. + _to_qlist_dict.side_effect = lambda d: {"order": d._char_num} + + def test_within_max_height(self, _to_qlist_dict, get_rtags_variable): + self.prepare(_to_qlist_dict) + get_rtags_variable.return_value = "10" + + height, qlist = vimrtags.Diagnostic.to_qlist_errors(self.diagnostics) + + self.assertEqual(height, 4) + self.assert_diagnostics(qlist) + + def test_outside_max_height(self, _to_qlist_dict, get_rtags_variable): + self.prepare(_to_qlist_dict) + get_rtags_variable.return_value = "2" + + height, qlist = vimrtags.Diagnostic.to_qlist_errors(self.diagnostics) + + self.assertEqual(height, 2) + self.assert_diagnostics(qlist) + + def assert_diagnostics(self, qlist): + self.assertListEqual( + json.loads(qlist), [ + {"order": 2, "nr": 1}, + {"order": 4, "nr": 2}, + {"order": 1, "nr": 3}, + {"order": 3, "nr": 4} + ] + ) + + +class Test_Diagnostic__to_qlist_dict(VimRtagsTest): + def test_warning(self): + diagnostic = vimrtags.Diagnostic("file", 3, 4, "warning", "mock text") + + self.assertDictEqual( + diagnostic._to_qlist_dict(), { + 'lnum': 3, 'col': 4, 'text': "mock text", 'filename': "file", 'type': "W" + } + ) + + def test_error(self): + diagnostic = vimrtags.Diagnostic("file", 3, 4, "error", "mock text") + + self.assertDictEqual( + diagnostic._to_qlist_dict(), { + 'lnum': 3, 'col': 4, 'text': "mock text", 'filename': "file", 'type': "E" + } + ) + + def test_fixit(self): + diagnostic = vimrtags.Diagnostic("file", 3, 4, "fixit", "mock text") + + self.assertDictEqual( + diagnostic._to_qlist_dict(), { + 'lnum': 3, 'col': 4, 'text': "mock text [FIXIT]", 'filename': "file", 'type': "E" + } + ) + + +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Sign._is_signs_defined", False) +@patch("plugin.vimrtags.get_command_output", autospec=True) +class Test_Sign__define_signs(VimRtagsTest): + + def test_no_bg_colours(self, get_command_output): + get_command_output.return_value = ( + "MockGroup xxx term=underline ctermfg=236 guifg=#3F3F3F" + ) + + vimrtags.Sign._define_signs() + + get_command_output.assert_called_once_with("highlight SignColumn") + self.assertEqual(vimrtags.Sign._is_signs_defined, True) + self.assert_signs("") + + def test_no_link(self, get_command_output): + get_command_output.return_value = ( + "MockGroup " + "xxx term=underline ctermfg=236 ctermbg=233 guifg=#3F3F3F guibg=#121212" + ) + + vimrtags.Sign._define_signs() + + get_command_output.assert_called_once_with("highlight SignColumn") + self.assertEqual(vimrtags.Sign._is_signs_defined, True) + self.assert_signs(" guibg=#121212 ctermbg=233") + + def test_with_link(self, get_command_output): + get_command_output.side_effect = [ + "MockGroup xxx ctermfg=141 guifg=#BF81FA guibg=#1F1F1F" + " links to MockLink", + "MockLink " + "xxx term=underline ctermfg=236 ctermbg=233 guifg=#3F3F3F guibg=#121212" + ] + + vimrtags.Sign._define_signs() + + self.assertListEqual( + get_command_output.call_args_list, [ + call("highlight SignColumn"), call("highlight MockLink") + ] + ) + self.assertEqual(vimrtags.Sign._is_signs_defined, True) + self.assert_signs(" guibg=#121212 ctermbg=233") + + def assert_signs(self, bg): + vim.command.assert_has_calls([ + call("highlight rtags_fixit guifg=#ff00ff ctermfg=5 %s" % bg), + call("highlight rtags_warning guifg=#fff000 ctermfg=11 %s" % bg), + call("highlight rtags_error guifg=#ff0000 ctermfg=1 %s" % bg), + call("sign define rtags_fixit text=Fx texthl=rtags_fixit"), + call("sign define rtags_warning text=W texthl=rtags_warning"), + call("sign define rtags_error text=E texthl=rtags_error") + ]) + + +@patch("plugin.vimrtags.get_command_output", autospec=True) +class Test_Sign_used_ids(VimRtagsTest): + def test(self, get_command_output): + get_command_output.return_value = (""" +--- Signs --- +Signs for path/to/file.ext: + line=363 id=3000 name=MockSign + line=439 id=3001 name=MockSign + line=664 id=3005 name=MockSign + line=665 id=3006 name=MockSign +""") + + used_ids = vimrtags.Sign.used_ids(4) + + self.assertSetEqual(used_ids, set([3000, 3001, 3005, 3006])) + + +@patch("plugin.vimrtags.Sign._is_signs_defined", None) +@patch("plugin.vimrtags.Sign._define_signs") +class Test_Sign_init(VimRtagsTest): + def test_signs_already_defined(self, _define_signs): + vimrtags.Sign._is_signs_defined = True + + sign = vimrtags.Sign(123, 234, "mock", 456) + + self.assertFalse(_define_signs.called) + vim.command.assert_called_once_with('sign place 123 line=234 name=rtags_mock buffer=456') + self.assertEqual(sign.id, 123) + self.assertEqual(sign._vimbuffer_num, 456) + + def test_signs_not_defined(self, _define_signs): + vimrtags.Sign._is_signs_defined = False + + sign = vimrtags.Sign(123, 234, "mock", 456) + + _define_signs.assert_called_once_with() + vim.command.assert_called_once_with('sign place 123 line=234 name=rtags_mock buffer=456') + self.assertEqual(sign.id, 123) + self.assertEqual(sign._vimbuffer_num, 456) + + +@patch("plugin.vimrtags.Sign._is_signs_defined", True) +class Test_Sign_unplace(VimRtagsTest): + def test(self): + sign = vimrtags.Sign(123, 456, "mock", 567) + vim.command.reset_mock() + + sign.unplace() + + vim.command.assert_called_once_with('sign unplace 123 buffer=567') + + +class Test_get_command_output(VimRtagsTest): + def test(self): + output = vimrtags.get_command_output("some command") + + vim.eval.assert_called_once_with('rtags#getCommandOutput("some command")') + self.assertIs(output, vim.eval.return_value) From 5f802c17c486077e00f8bb8faf7ecda7a64790a7 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 22:21:18 +0000 Subject: [PATCH 27/39] Use utility rather than raw eval for getting auto diagnostics toggle --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 5054b2b6..e1dcb746 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -360,7 +360,7 @@ def apply_fixits(self): # Not safe to apply fixits if file is not indexed, make extra sure it is. if ( self._rtags_is_reindexing() or ( - int(vim.eval("g:rtagsAutoDiagnostics")) and + int(get_rtags_variable("AutoDiagnostics")) and self._last_diagnostics_time < self._project.last_updated_time() ) ): From 986e2d8bdcb04f2dd04ceb4405fe564d88fb66ed Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 22:23:19 +0000 Subject: [PATCH 28/39] Split `--unsaved-file` filename part to support filenames with spaces --- plugin/vimrtags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index e1dcb746..f853337e 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -436,7 +436,7 @@ def _rtags_dirty_reindex(self): content = "\n".join([x for x in self._vimbuffer]) result = run_rc_command([ '--json', '--reindex', self._vimbuffer.name, - '--unsaved-file=%s:%d' % (self._vimbuffer.name, len(content)) + '--unsaved-file', '%s:%d' % (self._vimbuffer.name, len(content)) ], content) self._is_dirty = False logger.debug("Rtags responded to reindex request: %s" % result) From af6566df86b40f1ca497998f2409428f685732e1 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sat, 3 Mar 2018 22:24:14 +0000 Subject: [PATCH 29/39] Prefix underscore onto private `Diagnostic` members --- plugin/vimrtags.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index f853337e..83e68583 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -636,7 +636,7 @@ def from_rtags_errors(filename, errors): def to_qlist_errors(diagnostics): num_diagnostics = len(diagnostics) # Sort diagnostics into display order. Errors at top, grouped by filename in line num order. - diagnostics = sorted(diagnostics, key=lambda d: (d.type, d.filename, d.line_num)) + diagnostics = sorted(diagnostics, key=lambda d: (d.type, d._filename, d.line_num)) # Convert Diagnostic objects into quickfix compatible dicts. diagnostics = [d._to_qlist_dict() for d in diagnostics] # Add error number to diagnostic (shows in list, maybe useful for navigation?). @@ -651,9 +651,9 @@ def to_qlist_errors(diagnostics): return height, diagnostics def __init__(self, filename, line_num, char_num, type_, text): - self.filename = filename + self._filename = filename self.line_num = line_num - self.char_num = char_num + self._char_num = char_num self.type = type_ self.text = text @@ -661,8 +661,8 @@ def _to_qlist_dict(self): error_type = "W" if self.type == "warning" else "E" text = self.text if self.type != "fixit" else self.text + " [FIXIT]" return { - 'lnum': self.line_num, 'col': self.char_num, 'text': text, - 'filename': self.filename, 'type': error_type + 'lnum': self.line_num, 'col': self._char_num, 'text': text, + 'filename': self._filename, 'type': error_type } @@ -724,8 +724,8 @@ def used_ids(buffer_num): sign_ids.add(int(sign_match.group(1))) return sign_ids - def __init__(self, id, line_num, name, buffer_num): - self.id = id + def __init__(self, id_, line_num, name, buffer_num): + self.id = id_ self._vimbuffer_num = buffer_num if not Sign._is_signs_defined: Sign._define_signs() From b87730bff3c44675a1d84b88ce0cb703f14f66e5 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 00:03:09 +0000 Subject: [PATCH 30/39] Add `RtagsResetCaches` command to clear all `Buffer` etc caches * Maybe useful if e.g the user creates a project during a vim session. --- doc/rtags.txt | 10 +++++- plugin/rtags.vim | 3 ++ plugin/vimrtags.py | 49 ++++++++++++++++++++----- tests/test_vimrtags.py | 81 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/doc/rtags.txt b/doc/rtags.txt index ed0ee641..47edd78e 100755 --- a/doc/rtags.txt +++ b/doc/rtags.txt @@ -230,7 +230,7 @@ g:rtagsRdmLog *rtags-leader-rd* *rtags-Diagnostics* rd Show diagnostics for the current buffer in the location list, - along withing diagnostic signs in the current window's + along with diagnostic signs in the current window's gutter. *rtags-leader-rD* @@ -258,6 +258,14 @@ g:rtagsRdmLog - RtagsIFindRefsByName - RtagsLoadCompilationDb + *RtagsResetCaches* + + RtagsResetCaches + + Reset the buffer and RTags project cache. This may be needed if you create + or modify the RTags project during a vim session. + + *rtags-integrations* 6. Integrations with other plugins diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 326809ec..84df1240 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -1133,6 +1133,9 @@ command! -nargs=1 -complete=dir RtagsLoadCompilationDb call rtags#LoadCompilatio " The most commonly used find operation command! -nargs=1 -complete=customlist,rtags#CompleteSymbols Rtag RtagsIFindSymbols +" Reset all Python caches - maybe useful if the RTags project has been messed with +" after the vim session has started. +command! RtagsResetCaches call s:Pyeval('vimrtags.reset_caches()') if g:rtagsAutoLaunchRdm call system(g:rtagsRcCmd." -w") diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 83e68583..502c6cf5 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -166,6 +166,13 @@ def send_completion_request(): assert False +def reset_caches(): + Buffer.reset() + Project.reset() + Sign.reset() + message("RTags caches have been reset") + + class Buffer(object): _cache = {} _cache_last_cleaned = time() @@ -200,7 +207,7 @@ def get(id_): return buff # Periodically clean closed buffers - Buffer._clean_cache() + Buffer._clean_cache_periodically() logger.debug("Wrapping new buffer: %s" % id_) buff = Buffer(vim.buffers[id_]) @@ -208,16 +215,33 @@ def get(id_): return buff @staticmethod - def _clean_cache(): + def reset(): + """ Clear the Buffer cache. + + Remove all signs in still-valid buffers, then empty the cache. + """ + Buffer._clean_cache() + for buffer in Buffer._cache.values(): + buffer._reset_signs() + Buffer._cache = {} + + @staticmethod + def _clean_cache_periodically(): """ Periodically clean closed buffers """ if time() - Buffer._cache_last_cleaned > Buffer._CACHE_CLEAN_PERIOD: - logger.debug("Cleaning invalid buffers") - for id_old in list(Buffer._cache.keys()): - if not Buffer._cache[id_old]._vimbuffer.valid: - logger.debug("Cleaning invalid buffer %s" % id_old) - del Buffer._cache[id_old] - Buffer._cache_last_cleaned = time() + Buffer._clean_cache() + + @staticmethod + def _clean_cache(): + """ Clean closed buffers + """ + logger.debug("Cleaning invalid buffers") + for id_old in list(Buffer._cache.keys()): + if not Buffer._cache[id_old]._vimbuffer.valid: + logger.debug("Cleaning invalid buffer %s" % id_old) + del Buffer._cache[id_old] + Buffer._cache_last_cleaned = time() @staticmethod def show_all_diagnostics(): @@ -600,6 +624,11 @@ def get(filepath): Project._cache[project_root] = project return project + @staticmethod + def reset(): + Project._rtags_data_dir = None + Project._cache = {} + def __init__(self, project_root): self._project_root = project_root # Calculate the path of project database in the RTags data directory. @@ -716,6 +745,10 @@ def get_bgcolour(group): vim.command("sign define rtags_error text=E texthl=rtags_error") Sign._is_signs_defined = True + @staticmethod + def reset(): + Sign._is_signs_defined = False + @staticmethod def used_ids(buffer_num): signs_txt = get_command_output("sign place buffer=%s" % buffer_num) diff --git a/tests/test_vimrtags.py b/tests/test_vimrtags.py index 675089a9..153bb4ef 100644 --- a/tests/test_vimrtags.py +++ b/tests/test_vimrtags.py @@ -116,6 +116,20 @@ def _simple(self, kind): } +@patch("plugin.vimrtags.Buffer", autospec=True) +@patch("plugin.vimrtags.Project", autospec=True) +@patch("plugin.vimrtags.Sign", autospec=True) +@patch("plugin.vimrtags.message", autospec=True) +class Test_reset_caches(VimRtagsTest): + def test(self, message, Sign, Project, Buffer): + vimrtags.reset_caches() + + Buffer.reset.assert_called_once_with() + Project.reset.assert_called_once_with() + Sign.reset.assert_called_once_with() + self.assertTrue(message.called) + + class Test_Buffer_find(VimRtagsTest): def setUp(self): super(Test_Buffer_find, self).setUp() @@ -148,8 +162,8 @@ def test_found(self): self.assertIs(buffer, cached_buffer) @patch("plugin.vimrtags.Project", MagicMock()) - @patch("plugin.vimrtags.Buffer._clean_cache") - def test_not_found(self, _clean_cache): + @patch("plugin.vimrtags.Buffer._clean_cache_periodically") + def test_not_found(self, _clean_cache_periodically): vimbuffer = Mock() vim.buffers.append(vimbuffer) other_buffer = Mock() @@ -158,19 +172,41 @@ def test_not_found(self, _clean_cache): buffer = vimrtags.Buffer.get(3) buffer_again = vimrtags.Buffer.get(3) - _clean_cache.assert_called_once_with() + _clean_cache_periodically.assert_called_once_with() self.assertIsInstance(buffer, vimrtags.Buffer) self.assertIs(buffer._vimbuffer, vimbuffer) self.assertDictEqual(vimrtags.Buffer._cache, {3: buffer, 4: other_buffer}) self.assertIs(buffer, buffer_again) +@patch("plugin.vimrtags.Buffer._cache", None) +@patch("plugin.vimrtags.Buffer._clean_cache") +class Test_Buffer_reset(VimRtagsTest): + def test(self, _clean_cache): + buffer1 = Mock(spec=vimrtags.Buffer) + buffer2 = Mock(spec=vimrtags.Buffer) + vimrtags.Buffer._cache = {2: buffer1, 7: buffer2} + calls = MagicMock() + calls.attach_mock(_clean_cache, "clean") + calls.attach_mock(buffer1._reset_signs, "reset1") + calls.attach_mock(buffer2._reset_signs, "reset2") + + vimrtags.Buffer.reset() + + self.assertListEqual( + calls.method_calls, [ + call.clean(), call.reset1(), call.reset2() + ] + ) + self.assertDictEqual(vimrtags.Buffer._cache, {}) + + @patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) @patch("plugin.vimrtags.time", autospec=True) -@patch("plugin.vimrtags.Buffer._cache", {}) +@patch("plugin.vimrtags.Buffer._cache", None) @patch("plugin.vimrtags.Buffer._cache_last_cleaned", 6) @patch("plugin.vimrtags.Buffer._CACHE_CLEAN_PERIOD", 4) -class Test_Buffer__clean_cache(VimRtagsTest): +class Test_Buffer__clean_cache_periodically(VimRtagsTest): def prepare(self): self.buffer = Mock(_vimbuffer=Mock(valid=False)) vimrtags.Buffer._cache = {3: self.buffer} @@ -178,16 +214,29 @@ def prepare(self): def test_throttled(self, time): self.prepare() time.return_value = 10 - vimrtags.Buffer._clean_cache() + vimrtags.Buffer._clean_cache_periodically() self.assertIs(vimrtags.Buffer._cache[3], self.buffer) def test_removes(self, time): self.prepare() time.return_value = 11 - vimrtags.Buffer._clean_cache() + vimrtags.Buffer._clean_cache_periodically() self.assertDictEqual(vimrtags.Buffer._cache, {}) +@patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) +@patch("plugin.vimrtags.Buffer._cache", None) +class Test_Buffer__clean_cache(VimRtagsTest): + def test(self): + valid_buffer = Mock(_vimbuffer=Mock(valid=True)) + invalid_buffer = Mock(_vimbuffer=Mock(valid=False)) + vimrtags.Buffer._cache = {3: valid_buffer, 5: invalid_buffer} + + vimrtags.Buffer._clean_cache() + + self.assertDictEqual(vimrtags.Buffer._cache, {3: valid_buffer}) + + @patch("plugin.vimrtags.run_rc_command", autospec=True) class Test_Buffer_show_all_diagnostics(VimRtagsTest): @@ -1012,6 +1061,16 @@ def test_known_data_dir_unknown_project(self, run_rc_command): ) +@patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") +@patch("plugin.vimrtags.Project._cache", {"some": "stuff"}) +class Test_Project_reset(VimRtagsTest): + def test(self): + vimrtags.Project.reset() + + self.assertIsNone(vimrtags.Project._rtags_data_dir) + self.assertDictEqual(vimrtags.Project._cache, {}) + + @patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) @patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") class Test_Project_init(VimRtagsTest): @@ -1186,6 +1245,14 @@ def assert_signs(self, bg): ]) +@patch("plugin.vimrtags.Sign._is_signs_defined", True) +class Test_Sign_reset(VimRtagsTest): + def test(self): + vimrtags.Sign.reset() + + self.assertFalse(vimrtags.Sign._is_signs_defined) + + @patch("plugin.vimrtags.get_command_output", autospec=True) class Test_Sign_used_ids(VimRtagsTest): def test(self, get_command_output): From 1e7173cc55d6e6221b94aa4634cfbc91370db269 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 01:12:34 +0000 Subject: [PATCH 31/39] Fix python units tests for python2 compatibility --- plugin/vimrtags.py | 1 + tests/test_vimrtags.py | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 502c6cf5..64827bb2 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -1,3 +1,4 @@ +from __future__ import print_function # For unit tests import vim import json import subprocess diff --git a/tests/test_vimrtags.py b/tests/test_vimrtags.py index 153bb4ef..e7c66d1a 100644 --- a/tests/test_vimrtags.py +++ b/tests/test_vimrtags.py @@ -1,15 +1,18 @@ import json import logging import sys -from unittest import TestCase -from unittest.mock import Mock, MagicMock, patch, call +try: + from unittest import TestCase + from unittest.mock import Mock, MagicMock, patch, call +except ImportError: + from unittest2 import TestCase + from mock import Mock, MagicMock, patch, call -from nose.tools import set_trace +#from nose.tools import set_trace # Create a mocked vim in the environment. vim = MagicMock() sys.modules["vim"] = vim - with patch.object(logging, "FileHandler"): from plugin import vimrtags @@ -244,14 +247,14 @@ class Test_Buffer_show_all_diagnostics(VimRtagsTest): def test_rc_fails(self, error, run_rc_command): run_rc_command.return_value = None vimrtags.Buffer.show_all_diagnostics() - error.assert_called() + self.assertTrue(error.called) self.assertFalse(vim.command.called) @patch("plugin.vimrtags.message", autospec=True) def test_no_diagnostics(self, message, run_rc_command): run_rc_command.return_value = '{"checkStyle": {}}' vimrtags.Buffer.show_all_diagnostics() - message.assert_called() + self.assertTrue(message.called) self.assertFalse(vim.command.called) def prepare(self, get_rtags_variable, Diagnostic, run_rc_command): @@ -922,10 +925,10 @@ def test_force_not_already_open_has_results(self, Diagnostic): class Test_Buffer__place_signs(BufferInstanceTest): def test(self, _place_sign, _reset_signs, Sign): - self.buffer._diagnostics = { - 3: Mock(spec=vimrtags.Diagnostic, line_num=4, type="a"), - 56: Mock(spec=vimrtags.Diagnostic, line_num=65, type="b") - } + self.buffer._diagnostics = MagicMock(values=Mock(return_value=[ + Mock(spec=vimrtags.Diagnostic, line_num=4, type="a"), + Mock(spec=vimrtags.Diagnostic, line_num=65, type="b") + ])) calls = MagicMock() calls.attach_mock(_reset_signs, "reset") calls.attach_mock(_place_sign, "place") From c2951bacd2bc0c442802e17ce4dd16269251ec55 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 01:17:03 +0000 Subject: [PATCH 32/39] Update README --- README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1ec589ea..d2ced8aa 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ default mappings can be disabled: let g:rtagsUseDefaultMappings = 0 +Diagnostics will be retrieved from rtags automatically and displayed as signs in the gutter column. +To disable this behaviour, set: + + let g:rtagsAutoDiagnostics = 0 + By default, search results are showed in a location list. Location lists are local to the current window. To use the vim QuickFix window, which is shared between all windows, set: @@ -76,6 +81,8 @@ It is possible to set its maximum size (number of entries), default is 100: | <Leader>rw | -e -r --rename | Rename symbol under cursor | | <Leader>rv | -k -r | Find virtuals | | <Leader>rd | --diagnose | Diagnose file for warnings and errors | +| <Leader>rD | --diagnose-all | Diagnose all files in project | +| <Leader>rx | --fixits | Apply diagnostic fixits to current buffer | | <Leader>rb | N/A | Jump to previous location | ## Unite sources @@ -87,15 +94,17 @@ This plugin defines three Unite sources: * `rtags/project` - list/switch projects. ## Code completion -Code completion functionality uses ```completefunc``` (i.e. CTRL-X CTRL-U). If ```completefunc``` -is set, vim-rtags will not override it with ```RtagsCompleteFunc```. This functionality is still -unstable, but if you want to try it you will have to set ```completefunc``` by +The ```omnifunc``` (i.e. CTRL-X CTRL-O) is overridden with ```RtagsCompleteFunc``` for cpp +filetypes by default. This can be toggled using ```let g:rtagsCppOmnifunc = 0```. +If ```g:rtagsCppOmnifunc``` is set to ```0``` then the ```completefunc``` (i.e. CTRL-X CTRL-U) +will be set instead, but only if it's not already used. - set completefunc=RtagsCompleteFunc - -Also ```RtagsCompleteFunc``` can be used as omnifunc. For example, you can use -such approach with [neocomplete](https://github.com/Shougo/neocomplete.vim)(for more details read it's docs): +Compatibility with [YouCompleteMe](https://valloric.github.io/YouCompleteMe/) is just a matter of +disabling their built-in cpp completions and allowing vim-rtags to take over via their fallback to +the ```omnifunc```. +Compatibility with [neocomplete](https://github.com/Shougo/neocomplete.vim) can be achieved with +(for more details read it's docs): ``` function! SetupNeocompleteForCppWithRtags() " Enable heavy omni completion. @@ -105,14 +114,13 @@ function! SetupNeocompleteForCppWithRtags() let g:neocomplete#sources#omni#input_patterns = {} endif let l:cpp_patterns='[^.[:digit:] *\t]\%(\.\|->\)\|\h\w*::' - let g:neocomplete#sources#omni#input_patterns.cpp = l:cpp_patterns + let g:neocomplete#sources#omni#input_patterns.cpp = l:cpp_patterns set completeopt+=longest,menuone endfunction autocmd FileType cpp,c call SetupNeocompleteForCppWithRtags() - ``` -Such config provides automatic calls, of omnicompletion on c and cpp entity accessors. +Such config provides automatic calls of omnicompletion on c and cpp entity accessors. ### Current limitations * There is no support for overridden functions and methods @@ -123,6 +131,8 @@ Such config provides automatic calls, of omnicompletion on c and cpp entity acce # Development Unit tests for some plugin functions can be found in ```tests``` directory. -To run tests, execute: - +To run tests, execute (note `nose` is required for python tests): +``` $ vim tests/test_rtags.vim +UnitTest + $ nosetests tests +``` From 878a216c070778660a1649bb086463080bc2ad79 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 01:28:12 +0000 Subject: [PATCH 33/39] Remove a spammy useless log --- plugin/vimrtags.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 64827bb2..5c8bc4e6 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -237,10 +237,9 @@ def _clean_cache_periodically(): def _clean_cache(): """ Clean closed buffers """ - logger.debug("Cleaning invalid buffers") for id_old in list(Buffer._cache.keys()): if not Buffer._cache[id_old]._vimbuffer.valid: - logger.debug("Cleaning invalid buffer %s" % id_old) + logger.debug("Cleaning invalid buffer: %s" % id_old) del Buffer._cache[id_old] Buffer._cache_last_cleaned = time() From 6ba7607b2ae7ed38685ae7a9a34702eef24eab47 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 01:46:22 +0000 Subject: [PATCH 34/39] Dont bother repeating completion word in menu description column --- plugin/vimrtags.py | 1 - tests/test_vimrtags.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 5c8bc4e6..d813550b 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -133,7 +133,6 @@ def parse_completion_result(data): def _completion_description(completion, fields): fields += ['brief_comment'] fields = [field for field in fields if completion[field] != completion['completion']] - fields = ['completion'] + fields return " -- ".join(filter(None, [completion[field] for field in fields])) diff --git a/tests/test_vimrtags.py b/tests/test_vimrtags.py index e7c66d1a..3083c6cb 100644 --- a/tests/test_vimrtags.py +++ b/tests/test_vimrtags.py @@ -92,30 +92,30 @@ def _completion(self, kind): def _with_parent_sig(self, kind): return { - 'menu': "mock completion -- mock parent -- mock signature -- mock comment", + 'menu': "mock parent -- mock signature -- mock comment", 'word': "mock completion", 'kind': kind } def _with_sig(self, kind): return { - 'menu': "mock completion -- mock signature -- mock comment", 'word': "mock completion", + 'menu': "mock signature -- mock comment", 'word': "mock completion", 'kind': kind } def _with_parent(self, kind): return { - 'menu': "mock completion -- mock parent -- mock comment", 'word': "mock completion", + 'menu': "mock parent -- mock comment", 'word': "mock completion", 'kind': kind } def _with_comment(self, kind): return { - 'menu': "mock completion -- mock comment", 'word': "mock completion", 'kind': kind + 'menu': "mock comment", 'word': "mock completion", 'kind': kind } def _simple(self, kind): return { - 'menu': "mock completion", 'word': "mock completion", 'kind': kind + 'menu': "", 'word': "mock completion", 'kind': kind } From a13fd12c39f77817eea22a5df292e4d27dc58dbd Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 21:10:08 +0000 Subject: [PATCH 35/39] Set omnifunc for c as well cpp --- plugin/rtags.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 84df1240..88d503aa 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -1104,7 +1104,7 @@ endfunction " Prefer omnifunc, if enabled. if g:rtagsCppOmnifunc == 1 - autocmd Filetype cpp setlocal omnifunc=RtagsCompleteFunc + autocmd Filetype cpp,c setlocal omnifunc=RtagsCompleteFunc " Override completefunc if it's available to be used. elseif &completefunc == "" set completefunc=RtagsCompleteFunc From 147db47bc14deeeeeb747e231417ffaf1d7cd351 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 4 Mar 2018 21:11:30 +0000 Subject: [PATCH 36/39] Only poll for changes if current buffer is of cpp or c filetype --- plugin/rtags.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index 88d503aa..e9dfa7c0 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -922,7 +922,9 @@ function! rtags#NotifyCursorMoved() endfunction function! rtags#Poll(timer) - call s:Pyeval("vimrtags.Buffer.current().on_poll()") + if &filetype == "cpp" || &filetype == "c" + call s:Pyeval("vimrtags.Buffer.current().on_poll()") + endif call timer_start(g:rtagsDiagnosticsPollingInterval, "rtags#Poll") endfunction From cc5cb99ea57b3546aa63311774bb7e459243025e Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 13 May 2018 01:56:57 +0100 Subject: [PATCH 37/39] Add `--current-file` to `--diagnose-all` to ensure correct project --- plugin/vimrtags.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index d813550b..17653bdd 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -247,6 +247,10 @@ def show_all_diagnostics(): """ Get all diagnostics for all files and show in quickfix list. """ # Get the diagnostics from rtags. + content = run_rc_command([ + '--diagnose-all', '--current-file', vim.current.buffer.name, + '--synchronous-diagnostics', '--json' + ]) content = run_rc_command(['--diagnose-all', '--synchronous-diagnostics', '--json']) if content is None: return error('Failed to get diagnostics') From a02ba21e9de7458027bf17900dde34c4cc1c1cc3 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 13 May 2018 00:58:28 +0100 Subject: [PATCH 38/39] Add log when starting rdm --- plugin/rtags.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/rtags.vim b/plugin/rtags.vim index d1285ead..d80a0663 100644 --- a/plugin/rtags.vim +++ b/plugin/rtags.vim @@ -1152,7 +1152,9 @@ command! RtagsResetCaches call s:Pyeval('vimrtags.reset_caches()') if g:rtagsAutoLaunchRdm call system(g:rtagsRcCmd." -w") if v:shell_error != 0 - call system(g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".rtagsRdmLog) + let s:cmd = g:rtagsRdmCmd." --daemon --log-timestamp --log-flush --log-file ".g:rtagsRdmLog + call rtags#Log("Starting RDM: ".s:cmd) + call system(s:cmd) end end From 9ae5a10dd98cf20486e1b169c94e4a7ecc89efe7 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Sun, 13 May 2018 01:28:48 +0100 Subject: [PATCH 39/39] Better check of project update + reset when it fails * The rtags "project" file doesn't always update when files are re-indexed. * But some files in the project's directory do. * So find the most recently updated file/directory and use that file's modification time as the project's last updated time. * If the project has been deleted/moved then we're in a bad state, so reset internal caches of diagnostics etc in that case. --- plugin/vimrtags.py | 15 +++++++++++++-- tests/test_vimrtags.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/plugin/vimrtags.py b/plugin/vimrtags.py index 17653bdd..05bf657c 100644 --- a/plugin/vimrtags.py +++ b/plugin/vimrtags.py @@ -636,14 +636,25 @@ def __init__(self, project_root): self._project_root = project_root # Calculate the path of project database in the RTags data directory. self._db_path = os.path.join( - Project._rtags_data_dir, project_root.replace("/", "_"), "project" + Project._rtags_data_dir, project_root.replace("/", "_") # , "project" ) logger.debug("Project %s db path set to %s" % (self._project_root, self._db_path)) def last_updated_time(self): """ Unix timestamp when the rtags database was last updated. """ - return os.path.getmtime(self._db_path) + try: + return max(( + os.path.getmtime(os.path.join(self._db_path, f)) for f in os.listdir(self._db_path) + )) + except IOError as e: + logger.warning( + "Failed to get modification time of '%s', so resetting caches: %s" + % (self._db_path, e) + ) + # Project was deleted, reset everything! + reset_caches() + return 0 class Diagnostic(object): diff --git a/tests/test_vimrtags.py b/tests/test_vimrtags.py index 3083c6cb..9c33ef7f 100644 --- a/tests/test_vimrtags.py +++ b/tests/test_vimrtags.py @@ -1081,20 +1081,42 @@ def test(self): project = vimrtags.Project("/project/root/") self.assertEqual(project._project_root, "/project/root/") - self.assertEqual(project._db_path, "/data/dir/_project_root_/project") + self.assertEqual(project._db_path, "/data/dir/_project_root_") @patch("plugin.vimrtags.logger", Mock(spec=logging.Logger)) @patch("plugin.vimrtags.Project._rtags_data_dir", "/data/dir") @patch("plugin.vimrtags.os.path.getmtime", autospec=True) +@patch("plugin.vimrtags.os.listdir", autospec=True) class Test_Project_last_updated_time(VimRtagsTest): - def test(self, getmtime): + def test_ok(self, listdir, getmtime): + # setup + listdir.return_value = ["file1", "file2", "file3"] + getmtime.side_effect = [1, 3, 2] + project = vimrtags.Project("/file/path/") + + # action + mtime = project.last_updated_time() + + # confirm + listdir.assert_called_once_with("/data/dir/_file_path_") + self.assertListEqual( + getmtime.call_args_list, [ + call("/data/dir/_file_path_/file1"), call("/data/dir/_file_path_/file2"), + call("/data/dir/_file_path_/file3") + ] + ) + self.assertEqual(mtime, 3) + + @patch("plugin.vimrtags.reset_caches", autospec=True) + def test_ioerror(self, reset_caches, listdir, getmtime): + listdir.side_effect = IOError("Boom") project = vimrtags.Project("/file/path/") mtime = project.last_updated_time() - getmtime.assert_called_once_with("/data/dir/_file_path_/project") - self.assertIs(mtime, getmtime.return_value) + reset_caches.assert_called_once_with() + self.assertEqual(mtime, 0) class Test_Diagnostic_from_rtags_errors(VimRtagsTest):