YouCompleteMe/python/ycm.py
Strahinja Val Markovic 06d653c60d Adding the cursor identifier on InsertLeave
This should help catch cases where the user jumps to an identifier and then
edits it in place; we want to add that new ident to the db ASAP because the user
may want to have it completed soon.

Still, we're not perfect. If the user just deletes chars with 'x' or 'd' in Vim
and therefore never even enters insert mode we are obviously not going to pick
up that identifier until the next full file sweep.
2012-07-31 19:01:22 -07:00

401 lines
12 KiB
Python

#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 Strahinja Val Markovic <val@markovic.io>
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# YouCompleteMe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import vim
import indexer
import abc
MIN_NUM_CHARS = int( vim.eval( "g:ycm_min_num_of_chars_for_completion" ) )
CLANG_COMPLETION_ENABLED = int( vim.eval( "g:ycm_clang_completion_enabled" ) )
CLANG_FILETYPES = set( [ 'c', 'cpp', 'objc', 'objcpp' ] )
MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10
class Completer( object ):
__metaclass__ = abc.ABCMeta
def __init__( self ):
self.future = None
def AsyncCandidateRequestReady( self ):
if not self.future:
# We return True so that the caller can extract the default value from the
# future
return True
return self.future.ResultsReady()
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
return self.future.GetResults()
@abc.abstractmethod
def OnFileReadyToParse( self ):
pass
class IdentifierCompleter( Completer ):
def __init__( self ):
self.completer = indexer.IdentifierCompleter()
self.completer.EnableThreading()
def CandidatesForQueryAsync( self, query ):
filetype = vim.eval( "&filetype" )
self.future = self.completer.CandidatesForQueryAndTypeAsync(
SanitizeQuery( query ),
filetype )
def AddIdentifier( self, identifier ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath or not identifier:
return
vector = indexer.StringVec()
vector.append( identifier )
self.completer.AddCandidatesToDatabase( vector,
filetype,
filepath )
def AddPreviousIdentifier( self ):
self.AddIdentifier( PreviousIdentifier() )
def AddIdentifierUnderCursor( self ):
cursor_identifier = vim.eval( 'expand("<cword>")' )
if not cursor_identifier:
return
stripped_cursor_identifier = ''.join( ( x for x in
cursor_identifier if
IsIdentifierChar( x ) ) )
if not stripped_cursor_identifier:
return
self.AddIdentifier( stripped_cursor_identifier )
def AddBufferIdentifiers( self ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath:
return
text = "\n".join( vim.current.buffer )
self.completer.AddCandidatesToDatabaseFromBufferAsync( text,
filetype,
filepath )
def OnFileReadyToParse( self ):
self.AddBufferIdentifiers()
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
completions = self.future.GetResults()[
: MAX_IDENTIFIER_COMPLETIONS_RETURNED ]
# We will never have duplicates in completions so with 'dup':1 we tell Vim
# to add this candidate even if it's a duplicate of an existing one (which
# will never happen). This saves us some expensive string matching
# operations in Vim.
return [ { 'word': x, 'dup': 1 } for x in completions ]
class ClangCompleter( Completer ):
def __init__( self ):
self.completer = indexer.ClangCompleter()
self.completer.EnableThreading()
self.contents_holder = []
self.filename_holder = []
self.last_diagnostics = []
self.possibly_new_diagnostics = False
def GetUnsavedFilesVector( self ):
files = indexer.UnsavedFileVec()
self.contents_holder = []
self.filename_holder = []
for buffer in GetUnsavedBuffers():
if not ClangAvailableForBuffer( buffer ):
continue
contents = '\n'.join( buffer )
name = buffer.name
if not contents or not name:
continue
self.contents_holder.append( contents )
self.filename_holder.append( name )
unsaved_file = indexer.UnsavedFile()
unsaved_file.contents_ = self.contents_holder[ -1 ]
unsaved_file.length_ = len( self.contents_holder[ -1 ] )
unsaved_file.filename_ = self.filename_holder[ -1 ]
files.append( unsaved_file )
return files
def CandidatesForQueryAsync( self, query ):
if self.completer.UpdatingTranslationUnit():
PostVimMessage( 'Still parsing file, no completions yet.' )
self.future = None
return
# TODO: sanitize query
# CAREFUL HERE! For UnsavedFile filename and contents we are referring
# directly to Python-allocated and -managed memory since we are accepting
# pointers to data members of python objects. We need to ensure that those
# objects outlive our UnsavedFile objects. This is why we need the
# contents_holder and filename_holder lists, to make sure the string objects
# are still around when we call CandidatesForQueryAndLocationInFile. We do
# this to avoid an extra copy of the entire file contents.
files = indexer.UnsavedFileVec()
if not query:
files = self.GetUnsavedFilesVector()
line, _ = vim.current.window.cursor
column = int( vim.eval( "s:completion_start_column" ) ) + 1
current_buffer = vim.current.buffer
self.future = self.completer.CandidatesForQueryAndLocationInFileAsync(
query,
current_buffer.name,
line,
column,
files )
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
results = [ CompletionDataToDict( x ) for x in self.future.GetResults() ]
if not results:
PostVimMessage( 'No completions found; errors in the file?' )
return results
def OnFileReadyToParse( self ):
if NumLinesInBuffer( vim.current.buffer ) < 5:
return
self.possibly_new_diagnostics = True
self.completer.UpdateTranslationUnitAsync( vim.current.buffer.name,
self.GetUnsavedFilesVector() )
def DiagnosticsForCurrentFileReady( self ):
return ( self.possibly_new_diagnostics and not
self.completer.UpdatingTranslationUnit() )
def GetDiagnosticsForCurrentFile( self ):
if self.DiagnosticsForCurrentFileReady():
self.last_diagnostics = [ DiagnosticToDict( x ) for x in
self.completer.DiagnosticsForFile(
vim.current.buffer.name ) ]
self.possibly_new_diagnostics = False
return self.last_diagnostics
def NumLinesInBuffer( buffer ):
# This is actually less than obvious, that's why it's wrapped in a function
return len( buffer )
def PostVimMessage( message ):
# TODO: escape the message string before formating it
vim.command( 'echohl WarningMsg | echomsg "{0}" | echohl None'
.format( message ) )
def GetUnsavedBuffers():
def BufferModified( buffer_number ):
to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number )
return bool( int( vim.eval( to_eval ) ) )
return ( x for x in vim.buffers if BufferModified( x.number ) )
def CompletionDataToDict( completion_data ):
# see :h complete-items for a description of the dictionary fields
return {
'word' : completion_data.TextToInsertInBuffer(),
'abbr' : completion_data.original_string_,
'menu' : completion_data.extra_menu_info_,
'kind' : completion_data.kind_,
'dup' : 1,
# TODO: add detailed_info_ as 'info'
}
def DiagnosticToDict( diagnostic ):
# see :h getqflist for a description of the dictionary fields
return {
'bufnr' : int( vim.eval( "bufnr('{0}', 1)".format(
diagnostic.filename_ ) ) ),
'lnum' : diagnostic.line_number_,
'col' : diagnostic.column_number_,
'text' : diagnostic.text_,
'type' : diagnostic.kind_,
'valid' : 1
}
def CurrentColumn():
"""Do NOT access the CurrentColumn in vim.current.line. It doesn't exist yet.
Only the chars before the current column exist in vim.current.line."""
# vim's columns are 1-based while vim.current.line columns are 0-based
# ... but vim.current.window.cursor (which returns a (line, column) tuple)
# columns are 0-based, while the line from that same tuple is 1-based.
# vim.buffers buffer objects OTOH have 0-based lines and columns.
# Pigs have wings and I'm a loopy purple duck. Everything makes sense now.
return vim.current.window.cursor[ 1 ]
def CurrentLineAndColumn():
# See the comment in CurrentColumn about the calculation for the line and
# column number
line, column = vim.current.window.cursor
line -= 1
return line, column
def ClangAvailableForBuffer( buffer_object ):
filetype = vim.eval( 'getbufvar({0}, "&ft")'.format( buffer_object.number ) )
return filetype in CLANG_FILETYPES
def ClangAvailableForFile():
filetype = vim.eval( "&filetype" )
return filetype in CLANG_FILETYPES
def ShouldUseClang( start_column ):
if not CLANG_COMPLETION_ENABLED or not ClangAvailableForFile():
return False
line = vim.current.line
previous_char_index = start_column - 1
if ( not len( line ) or
previous_char_index < 0 or
previous_char_index >= len( line ) ):
return False
if line[ previous_char_index ] == '.':
return True
if previous_char_index - 1 < 0:
return False
two_previous_chars = line[ previous_char_index - 1 : start_column ]
if ( two_previous_chars == '->' or two_previous_chars == '::' ):
return True
return False
def IsIdentifierChar( char ):
return char.isalnum() or char == '_'
def CompletionStartColumn():
"""Returns the 0-based index where the completion string should start. So if
the user enters:
foo.bar^
with the cursor being at the location of the caret, then the starting column
would be the index of the letter 'b'.
"""
line = vim.current.line
start_column = CurrentColumn()
while start_column > 0 and IsIdentifierChar( line[ start_column - 1 ] ):
start_column -= 1
return start_column
def EscapeForVim( text ):
return text.replace( "'", "''" )
def PreviousIdentifier():
line_num, column_num = CurrentLineAndColumn()
buffer = vim.current.buffer
line = buffer[ line_num ]
end_column = column_num
while end_column > 0 and not IsIdentifierChar( line[ end_column - 1 ] ):
end_column -= 1
# Look at the previous line if we reached the end of the current one
if end_column == 0:
try:
line = buffer[ line_num - 1]
except:
return ""
end_column = len( line )
while end_column > 0 and not IsIdentifierChar( line[ end_column - 1 ] ):
end_column -= 1
print end_column, line
start_column = end_column
while start_column > 0 and IsIdentifierChar( line[ start_column - 1 ] ):
start_column -= 1
if end_column - start_column < MIN_NUM_CHARS:
return ""
return line[ start_column : end_column ]
def CurrentIdentifierFinished():
current_column = CurrentColumn()
previous_char_index = current_column - 1
if previous_char_index < 0:
return True
line = vim.current.line
try:
previous_char = line[ previous_char_index ]
except IndexError:
return False
if IsIdentifierChar( previous_char ):
return False
if ( not IsIdentifierChar( previous_char ) and
previous_char_index > 0 and
IsIdentifierChar( line[ previous_char_index - 1 ] ) ):
return True
else:
return line[ : current_column ].isspace()
def SanitizeQuery( query ):
return query.strip()