From 6c521ba37a6bb15558155aeb106f2f1d037289e3 Mon Sep 17 00:00:00 2001 From: Xavier Deguillard Date: Sat, 16 Feb 2013 22:29:14 -0800 Subject: [PATCH 1/3] Launch one thread per file. That thread is re-used when re-launching a completion. --- plugin/clang_complete.vim | 1 + plugin/libclang.py | 128 +++++++++++++++++++++++++------------- 2 files changed, 85 insertions(+), 44 deletions(-) diff --git a/plugin/clang_complete.vim b/plugin/clang_complete.vim index b4bdaa48..5f633740 100644 --- a/plugin/clang_complete.vim +++ b/plugin/clang_complete.vim @@ -276,6 +276,7 @@ function! s:initClangCompletePython(user_requested) if l:res == 0 return 0 endif + au VimLeavePre * python FinishClangComplete() let s:libclang_loaded = 1 endif python WarmupCache() diff --git a/plugin/libclang.py b/plugin/libclang.py index 08533a1c..245ca60e 100644 --- a/plugin/libclang.py +++ b/plugin/libclang.py @@ -51,6 +51,7 @@ def initClangComplete(clang_complete_flags, clang_compilation_database, \ library_path, user_requested): global index + global debug debug = int(vim.eval("g:clang_debug")) == 1 printWarnings = (user_requested != "0") or debug @@ -82,6 +83,8 @@ def initClangComplete(clang_complete_flags, clang_compilation_database, \ global translationUnits translationUnits = dict() + global threads + threads = dict() global complete_flags complete_flags = int(clang_complete_flags) global compilation_database @@ -91,6 +94,8 @@ def initClangComplete(clang_complete_flags, clang_compilation_database, \ compilation_database = None global libclangLock libclangLock = threading.Lock() + global threadsShouldTerminate + threadsShouldTerminate = False return 1 # Get a tuple (fileName, fileContent) for the file opened in the current @@ -100,23 +105,23 @@ def getCurrentFile(): return (vim.current.buffer.name, file) class CodeCompleteTimer: - def __init__(self, debug, file, line, column, params): + def __init__(self): self._debug = debug - if not debug: + def start(self, file, line, column, args, cwd): + if not self._debug: return - content = vim.current.line print " " print "libclang code completion" print "========================" - print "Command: clang %s -fsyntax-only " % " ".join(params['args']), + print "Command: clang %s -fsyntax-only " % " ".join(args), print "-Xclang -code-completion-at=%s:%d:%d %s" % (file, line, column, file) - print "cwd: %s" % params['cwd'] + print "cwd: %s" % cwd print "File: %s" % file print "Line: %d, Column: %d" % (line, column) print " " - print "%s" % content + print "%s" % vim.current.buffer[line] print " " @@ -160,11 +165,14 @@ def getCurrentTranslationUnit(args, currentFile, fileName, timer, timer.registerEvent("Reparsing") return tu + if threadsShouldTerminate: + return None + flags = TranslationUnit.PARSE_PRECOMPILED_PREAMBLE tu = index.parse(fileName, args, [currentFile], flags) timer.registerEvent("First parse") - if tu == None: + if tu == None or threadsShouldTerminate: return None translationUnits[fileName] = tu @@ -332,8 +340,9 @@ def updateCurrentDiagnostics(): global debug debug = int(vim.eval("g:clang_debug")) == 1 params = getCompileParams(vim.current.buffer.name) - timer = CodeCompleteTimer(debug, vim.current.buffer.name, -1, -1, params) + timer = CodeCompleteTimer() + timer.start(vim.current.buffer.name, -1, -1, params['args'], params['cwd']) with workingDir(params['cwd']): with libclangLock: getCurrentTranslationUnit(params['args'], getCurrentFile(), @@ -402,64 +411,95 @@ def formatResult(result): class CompleteThread(threading.Thread): - def __init__(self, line, column, currentFile, fileName, params, timer): + def __init__(self, fileName, params): threading.Thread.__init__(self) - self.line = line - self.column = column - self.currentFile = currentFile + self.line = -1 + self.column = -1 + self.fileContent = None self.fileName = fileName self.result = None + self.shouldTerminate = False self.args = params['args'] self.cwd = params['cwd'] - self.timer = timer + self.timer = CodeCompleteTimer() + self.event = threading.Event() + self.doneEvent = threading.Event() def run(self): + global threadsShouldTerminate with workingDir(self.cwd): with libclangLock: - if self.line == -1: - # Warm up the caches. For this it is sufficient to get the - # current translation unit. No need to retrieve completion - # results. This short pause is necessary to allow vim to - # initialize itself. Otherwise we would get: E293: block was - # not locked The user does not see any delay, as we just pause - # a background thread. - time.sleep(0.1) - getCurrentTranslationUnit(self.args, self.currentFile, self.fileName, - self.timer) - else: + # We need to sleep, otherwise vim become crazy and crash. + time.sleep(0.1) + getCurrentTranslationUnit(self.args, self.fileContent, self.fileName, + self.timer) + + while True: + # FIXME: put a timeout and background reparse. + self.event.wait() + if self.shouldTerminate: + return + + self.event.clear() + with libclangLock: self.result = getCurrentCompletionResults(self.line, self.column, - self.args, self.currentFile, + self.args, self.fileContent, self.fileName, self.timer) + self.doneEvent.set() -def WarmupCache(): - params = getCompileParams(vim.current.buffer.name) - timer = CodeCompleteTimer(0, "", -1, -1, params) - t = CompleteThread(-1, -1, getCurrentFile(), vim.current.buffer.name, - params, timer) - t.start() + def getResult(self): + res = self.result + self.doneEvent.clear() + return res + +def getThread(filename): + line, _ = vim.current.window.cursor + column = int(vim.eval("b:col")) + fileContent = getCurrentFile() + + t = threads.get(filename) + if t is None: + params = getCompileParams(vim.current.buffer.name) + t = CompleteThread(vim.current.buffer.name, params) + threads[filename] = t + t.line = line + t.column = column + t.fileContent = fileContent + t.timer.start(filename, line, column, t.args, t.cwd) + + if not t.is_alive(): + t.start() + + return t + +def FinishClangComplete(): + # I've got no idea why threadsShouldTerminate is not enough for the + # threads… + threadsShouldTerminate = True + for t in threads.itervalues(): + t.shouldTerminate = True + t.event.set() + +def WarmupCache(): + getThread(vim.current.buffer.name) def getCurrentCompletions(base): - global debug - debug = int(vim.eval("g:clang_debug")) == 1 sorting = vim.eval("g:clang_sort_algo") - line, _ = vim.current.window.cursor - column = int(vim.eval("b:col")) - params = getCompileParams(vim.current.buffer.name) - timer = CodeCompleteTimer(debug, vim.current.buffer.name, line, column, - params) + t = getThread(vim.current.buffer.name) + timer = t.timer + t.event.set() + + while True: + if t.doneEvent.wait(0.1): + break - t = CompleteThread(line, column, getCurrentFile(), vim.current.buffer.name, - params, timer) - t.start() - while t.isAlive(): - t.join(0.01) cancel = int(vim.eval('complete_check()')) if cancel != 0: return (str([]), timer) - cr = t.result + cr = t.getResult() if cr is None: print "Cannot parse this source file. The following arguments " \ + "are used for clang: " + " ".join(params['args']) From 14150f9dd11e15cb6f667f11884688e31d6d2564 Mon Sep 17 00:00:00 2001 From: Xavier Deguillard Date: Mon, 18 Feb 2013 23:10:22 -0800 Subject: [PATCH 2/3] Push nearly everything into the thread, and make the completion async. --- plugin/clang_complete.vim | 75 ++++++++++++++------- plugin/libclang.py | 134 ++++++++++++++++++++++---------------- 2 files changed, 127 insertions(+), 82 deletions(-) diff --git a/plugin/clang_complete.vim b/plugin/clang_complete.vim index 5f633740..3080b63b 100644 --- a/plugin/clang_complete.vim +++ b/plugin/clang_complete.vim @@ -104,9 +104,9 @@ function! s:ClangCompleteInit() call LoadUserOptions() inoremap LaunchCompletion() - inoremap . CompleteDot() - inoremap > CompleteArrow() - inoremap : CompleteColon() + "inoremap . CompleteDot() + "inoremap > CompleteArrow() + "inoremap : CompleteColon() inoremap HandlePossibleSelectionEnter() if g:clang_snippets == 1 @@ -162,6 +162,9 @@ function! s:ClangCompleteInit() augroup end endif + au CursorMovedI call ReLaunchCompletion() + "au InsertCharPre call ReLaunchCompletion() + if !exists('g:clang_use_library') || g:clang_use_library == 1 " Try to use libclang. On failure, we fall back to the clang executable. let l:initialized = s:initClangCompletePython(exists('g:clang_use_library')) @@ -590,6 +593,9 @@ function! ClangComplete(findstart, base) while l:start > 0 && l:line[l:start - 1] =~ '\i' let l:start -= 1 endwhile + if l:start == 0 + return -1 + endif if l:line[l:start - 2:] =~ '->' || l:line[l:start - 1] == '.' let b:clang_complete_type = 0 endif @@ -605,37 +611,46 @@ function! ClangComplete(findstart, base) endif if g:clang_use_library == 1 - python completions, timer = getCurrentCompletions(vim.eval('a:base')) + python thread = getThread(vim.current.buffer.name) + let l:str = 'tryComplete(thread,' . b:col . ', str(' . a:base . '))' + let l:shouldRetry = pyeval('tryComplete(thread,' . b:col . ', "' . a:base . '")') + if l:shouldRetry + return [] + endif + + python completions, timer = getCurrentCompletions(thread) python vim.command('let l:res = ' + completions) python timer.registerEvent("Load into vimscript") else let l:res = s:ClangCompleteBinary(a:base) endif - for item in l:res - if g:clang_snippets == 1 - let item['word'] = b:AddSnip(item['info'], item['args_pos']) - else - let item['word'] = item['abbr'] - endif - endfor + if l:res != [] + for item in l:res + if g:clang_snippets == 1 + let item['word'] = b:AddSnip(item['info'], item['args_pos']) + else + let item['word'] = item['abbr'] + endif + endfor - inoremap HandlePossibleSelectionCtrlY() - augroup ClangComplete - au CursorMovedI call TriggerSnippet() - augroup end - let b:snippet_chosen = 0 + inoremap HandlePossibleSelectionCtrlY() + augroup ClangComplete + au CursorMovedI call TriggerSnippet() + augroup end + let b:snippet_chosen = 0 - if g:clang_use_library == 1 - python timer.registerEvent("vimscript + snippets") - python timer.finish() - endif + if g:clang_use_library == 1 + python timer.registerEvent("vimscript + snippets") + python timer.finish() + endif - if g:clang_debug == 1 - echom 'clang_complete: completion time (' . (g:clang_use_library == 1 ? 'library' : 'binary') . ') '. split(reltimestr(reltime(l:time_start)))[0] + if g:clang_debug == 1 + echom 'clang_complete: completion time (' . (g:clang_use_library == 1 ? 'library' : 'binary') . ') '. split(reltimestr(reltime(l:time_start)))[0] + endif + endif + return l:res endif - return l:res -endif endfunction function! s:HandlePossibleSelectionEnter() @@ -727,9 +742,19 @@ function! s:CompleteColon() return ':' . s:LaunchCompletion() endfunction +let b:prev_line = "" + +function! s:ReLaunchCompletion() + if s:ShouldComplete() && b:prev_line != getline('.') + let b:prev_line = getline('.') + call feedkeys("\\") + endif + return '' +endfunction + " May be used in a mapping to update the quickfix window. function! g:ClangUpdateQuickFix() - call s:DoPeriodicQuickFix() + "call s:DoPeriodicQuickFix() return '' endfunction diff --git a/plugin/libclang.py b/plugin/libclang.py index 245ca60e..4e8c6e79 100644 --- a/plugin/libclang.py +++ b/plugin/libclang.py @@ -121,7 +121,7 @@ def start(self, file, line, column, args, cwd): print "File: %s" % file print "Line: %d, Column: %d" % (line, column) print " " - print "%s" % vim.current.buffer[line] + print "%s" % vim.current.buffer[line - 1] print " " @@ -415,7 +415,9 @@ def __init__(self, fileName, params): threading.Thread.__init__(self) self.line = -1 self.column = -1 - self.fileContent = None + self.base = "" + self.sorting = "" + self.fileContent = "" self.fileName = fileName self.result = None self.shouldTerminate = False @@ -424,6 +426,7 @@ def __init__(self, fileName, params): self.timer = CodeCompleteTimer() self.event = threading.Event() self.doneEvent = threading.Event() + self.restartEvent = threading.Event() def run(self): global threadsShouldTerminate @@ -437,93 +440,110 @@ def run(self): while True: # FIXME: put a timeout and background reparse. self.event.wait() + self.restartEvent.clear() if self.shouldTerminate: return - self.event.clear() with libclangLock: - self.result = getCurrentCompletionResults(self.line, self.column, - self.args, self.fileContent, - self.fileName, self.timer) + results = getCurrentCompletionResults(self.line, self.column, + self.args, self.fileContent, + self.fileName, self.timer) + if results is not None: + res = results.results + self.timer.registerEvent("Count # Results (%d)" % len(res)) + + if self.base != "": + res = filter(lambda x: getAbbr(x.string).startswith(self.base), res) + + self.timer.registerEvent("Filter") + + if self.sorting == 'priority': + getPriority = lambda x: x.string.priority + res = sorted(res, None, getPriority) + if self.sorting == 'alpha': + getAbbrevation = lambda x: getAbbr(x.string).lower() + res = sorted(res, None, getAbbrevation) + self.timer.registerEvent("Sort") + + self.result = res self.doneEvent.set() + self.event.clear() + self.restartEvent.wait() + if self.shouldTerminate: + return + self.doneEvent.clear() + + STATE_WAITING = 0 + STATE_RUNNING = 1 + STATE_FINISHED = 2 + def getCompletionState(self): + if self.doneEvent.is_set(): + return CompleteThread.STATE_FINISHED + + if self.event.is_set(): + return CompleteThread.STATE_RUNNING + else: + return CompleteThread.STATE_WAITING - def getResult(self): - res = self.result - self.doneEvent.clear() - return res def getThread(filename): - line, _ = vim.current.window.cursor - column = int(vim.eval("b:col")) - fileContent = getCurrentFile() - t = threads.get(filename) if t is None: - params = getCompileParams(vim.current.buffer.name) - t = CompleteThread(vim.current.buffer.name, params) + params = getCompileParams(filename) + t = CompleteThread(filename, params) threads[filename] = t - t.line = line - t.column = column - t.fileContent = fileContent - t.timer.start(filename, line, column, t.args, t.cwd) - - if not t.is_alive(): - t.start() - return t def FinishClangComplete(): # I've got no idea why threadsShouldTerminate is not enough for the - # threads… + # threads. threadsShouldTerminate = True for t in threads.itervalues(): t.shouldTerminate = True t.event.set() + t.restartEvent.set() def WarmupCache(): - getThread(vim.current.buffer.name) + t = getThread(vim.current.buffer.name) + t.fileContent = getCurrentFile() + t.timer.start(vim.current.buffer.name, -1, -1, t.args, t.cwd) + t.start() -def getCurrentCompletions(base): - sorting = vim.eval("g:clang_sort_algo") +def tryComplete(t, column, base): + state = t.getCompletionState() + if state == CompleteThread.STATE_FINISHED and column == t.column: + return False - t = getThread(vim.current.buffer.name) - timer = t.timer + if state == CompleteThread.STATE_RUNNING: + return True + + if not t.is_alive(): + t.start() + + # The thread is waiting, give him some work! + line, _ = vim.current.window.cursor + t.line = line + t.column = column + t.base = base + t.sorting = vim.eval("g:clang_sort_algo") + t.fileContent = getCurrentFile() + t.timer.start(vim.current.buffer.name, line, column, t.args, t.cwd) t.event.set() - while True: - if t.doneEvent.wait(0.1): - break + return True - cancel = int(vim.eval('complete_check()')) - if cancel != 0: - return (str([]), timer) +def getCurrentCompletions(t): + timer = t.timer + cr = t.result + t.restartEvent.set() - cr = t.getResult() if cr is None: print "Cannot parse this source file. The following arguments " \ - + "are used for clang: " + " ".join(params['args']) + + "are used for clang: " + " ".join(t.args) return (str([]), timer) - results = cr.results - - timer.registerEvent("Count # Results (%s)" % str(len(results))) - - if base != "": - results = filter(lambda x: getAbbr(x.string).startswith(base), results) - - timer.registerEvent("Filter") - - if sorting == 'priority': - getPriority = lambda x: x.string.priority - results = sorted(results, None, getPriority) - if sorting == 'alpha': - getAbbrevation = lambda x: getAbbr(x.string).lower() - results = sorted(results, None, getAbbrevation) - - timer.registerEvent("Sort") - - result = map(formatResult, results) + result = map(formatResult, cr) timer.registerEvent("Format") return (str(result), timer) From 5d25e3bc19f26bf65bd601bccd12b637a4998861 Mon Sep 17 00:00:00 2001 From: Xavier Deguillard Date: Wed, 20 Feb 2013 19:26:45 -0800 Subject: [PATCH 3/3] pyeval() is too recent. --- plugin/clang_complete.vim | 5 ++--- plugin/libclang.py | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/clang_complete.vim b/plugin/clang_complete.vim index 3080b63b..5f3ecab9 100644 --- a/plugin/clang_complete.vim +++ b/plugin/clang_complete.vim @@ -612,9 +612,8 @@ function! ClangComplete(findstart, base) if g:clang_use_library == 1 python thread = getThread(vim.current.buffer.name) - let l:str = 'tryComplete(thread,' . b:col . ', str(' . a:base . '))' - let l:shouldRetry = pyeval('tryComplete(thread,' . b:col . ', "' . a:base . '")') - if l:shouldRetry + python vim.command('let l:shouldRetry = ' + str(tryComplete(thread, vim.eval('b:col'), vim.eval("a:base")))) + if l:shouldRetry == 1 return [] endif diff --git a/plugin/libclang.py b/plugin/libclang.py index 4e8c6e79..ee4dddd4 100644 --- a/plugin/libclang.py +++ b/plugin/libclang.py @@ -512,11 +512,12 @@ def WarmupCache(): def tryComplete(t, column, base): state = t.getCompletionState() + column = int(column) if state == CompleteThread.STATE_FINISHED and column == t.column: - return False + return 0 if state == CompleteThread.STATE_RUNNING: - return True + return 1 if not t.is_alive(): t.start() @@ -531,7 +532,7 @@ def tryComplete(t, column, base): t.timer.start(vim.current.buffer.name, line, column, t.args, t.cwd) t.event.set() - return True + return 1 def getCurrentCompletions(t): timer = t.timer