diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 642fbbf9..94a83ab8 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -484,7 +484,7 @@ function! s:CompletionsForQuery( query, use_filetype_completer, endif endwhile - let l:results = pyeval( 'completer.CandidatesFromStoredRequest()' ) + let l:results = pyeval( 'base.AdjustCandidateInsertionText( completer.CandidatesFromStoredRequest() )' ) let s:searched_and_results_found = len( l:results ) != 0 return { 'words' : l:results, 'refresh' : 'always' } endfunction diff --git a/python/ycm/base.py b/python/ycm/base.py index 5adb66cf..cecd8a9d 100644 --- a/python/ycm/base.py +++ b/python/ycm/base.py @@ -18,6 +18,7 @@ # along with YouCompleteMe. If not, see . import os +import re import vim from ycm import vimsupport from ycm import utils @@ -70,6 +71,55 @@ def CurrentIdentifierFinished(): return line[ : current_column ].isspace() +def AdjustCandidateInsertionText( candidates ): + """This function adjusts the candidate insertion text to take into account the + text that's currently in front of the cursor. + + For instance ('|' represents the cursor): + 1. Buffer state: 'foo.|bar' + 2. A completion candidate of 'zoobar' is shown and the user selects it. + 3. Buffer state: 'foo.zoobar|bar' instead of 'foo.zoo|bar' which is what the + user wanted. + + This function changes candidates to resolve that issue. + + It could be argued that the user actually wants the final buffer state to be + 'foo.zoobar|' (the cursor at the end), but that would be much more difficult + to implement and is probably not worth doing. + """ + + def NewCandidateInsertionText( to_insert, word_after_cursor ): + if to_insert.endswith( word_after_cursor ): + return to_insert[ : - len( word_after_cursor ) ] + return to_insert + + match = re.search( r'^(\w+)', vimsupport.TextAfterCursor() ) + if not match: + return candidates + + new_candidates = [] + + word_after_cursor = match.group( 1 ) + for candidate in candidates: + if type( candidate ) is dict: + new_candidate = candidate.copy() + + if not 'abbr' in new_candidate: + new_candidate[ 'abbr' ] = new_candidate[ 'word' ] + + new_candidate[ 'word' ] = NewCandidateInsertionText( + new_candidate[ 'word' ], + word_after_cursor ) + + new_candidates.append( new_candidate ) + + elif type( candidate ) is str: + new_candidates.append( + { 'abbr': candidate, + 'word': NewCandidateInsertionText( candidate, word_after_cursor ) } ) + return new_candidates + + COMPATIBLE_WITH_CORE_VERSION = 4 def CompatibleWithYcmCore(): diff --git a/python/ycm/completers/all/omni_completer.py b/python/ycm/completers/all/omni_completer.py index 17052647..66b6fff6 100644 --- a/python/ycm/completers/all/omni_completer.py +++ b/python/ycm/completers/all/omni_completer.py @@ -45,6 +45,7 @@ class OmniCompleter( Completer ): return super( OmniCompleter, self ).ShouldUseNow( start_column ) return self.ShouldUseNowInner( start_column ) + def ShouldUseNowInner( self, start_column ): if not self.omnifunc: return False @@ -58,6 +59,7 @@ class OmniCompleter( Completer ): else: return self.CandidatesForQueryAsyncInner( query, unused_start_column ) + def CandidatesForQueryAsyncInner( self, query, unused_start_column ): if not self.omnifunc: self.stored_candidates = None @@ -104,6 +106,7 @@ class OmniCompleter( Completer ): else: return self.CandidatesFromStoredRequestInner() + def CandidatesFromStoredRequestInner( self ): return self.stored_candidates if self.stored_candidates else [] diff --git a/python/ycm/test_utils.py b/python/ycm/test_utils.py index 4b48ef60..ab07013f 100644 --- a/python/ycm/test_utils.py +++ b/python/ycm/test_utils.py @@ -24,7 +24,13 @@ def MockVimModule(): """The 'vim' module is something that is only present when running inside the Vim Python interpreter, so we replace it with a MagicMock for tests. """ + def VimEval( value ): + if value == "g:ycm_min_num_of_chars_for_completion": + return 0 + return '' + vim_mock = MagicMock() - vim_mock.eval = MagicMock( return_value = '' ) + vim_mock.eval = MagicMock( side_effect = VimEval ) sys.modules[ 'vim' ] = vim_mock return vim_mock + diff --git a/python/ycm/tests/base_test.py b/python/ycm/tests/base_test.py new file mode 100644 index 00000000..24c97c8c --- /dev/null +++ b/python/ycm/tests/base_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +from nose.tools import eq_ +from mock import MagicMock +from ycm.test_utils import MockVimModule +vim_mock = MockVimModule() +from ycm import base +from ycm import vimsupport + + +def AdjustCandidateInsertionText_Basic_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + + +def AdjustCandidateInsertionText_ParenInTextAfterCursor_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + + +def AdjustCandidateInsertionText_PlusInTextAfterCursor_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar+zoo' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + + +def AdjustCandidateInsertionText_WhitespaceInTextAfterCursor_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar zoo' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + + +def AdjustCandidateInsertionText_NotSuffix_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) + eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ], + base.AdjustCandidateInsertionText( [ 'foofoo' ] ) ) + + +def AdjustCandidateInsertionText_NothingAfterCursor_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = '' ) + eq_( [ 'foofoo', + 'zobar' ], + base.AdjustCandidateInsertionText( [ 'foofoo', + 'zobar' ] ) ) + + +def AdjustCandidateInsertionText_MultipleStrings_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' }, + { 'abbr': 'zobar', 'word': 'zo' }, + { 'abbr': 'qbar', 'word': 'q' }, + { 'abbr': 'bar', 'word': '' }, + ], + base.AdjustCandidateInsertionText( [ 'foobar', + 'zobar', + 'qbar', + 'bar' ] ) ) + + +def AdjustCandidateInsertionText_DictInput_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) + eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( + [ { 'word': 'foobar' } ] ) ) + + +def AdjustCandidateInsertionText_DontTouchAbbr_test(): + vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) + eq_( [ { 'abbr': '1234', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( + [ { 'abbr': '1234', 'word': 'foobar' } ] ) ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 15532f7d..40082676 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -41,6 +41,11 @@ def CurrentColumn(): return vim.current.window.cursor[ 1 ] +def TextAfterCursor(): + """Returns the text after CurrentColumn.""" + return vim.current.line[ CurrentColumn(): ] + + def GetUnsavedBuffers(): def BufferModified( buffer_number ): to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number )