Better completion in the middle of a word

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 commit resolves 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.

Fixes #374.
This commit is contained in:
Strahinja Val Markovic 2013-06-09 19:00:49 -07:00
parent 8fe41c7c73
commit e740bac1f6
6 changed files with 156 additions and 2 deletions

View File

@ -484,7 +484,7 @@ function! s:CompletionsForQuery( query, use_filetype_completer,
endif endif
endwhile 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 let s:searched_and_results_found = len( l:results ) != 0
return { 'words' : l:results, 'refresh' : 'always' } return { 'words' : l:results, 'refresh' : 'always' }
endfunction endfunction

View File

@ -18,6 +18,7 @@
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import os import os
import re
import vim import vim
from ycm import vimsupport from ycm import vimsupport
from ycm import utils from ycm import utils
@ -70,6 +71,55 @@ def CurrentIdentifierFinished():
return line[ : current_column ].isspace() 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 COMPATIBLE_WITH_CORE_VERSION = 4
def CompatibleWithYcmCore(): def CompatibleWithYcmCore():

View File

@ -45,6 +45,7 @@ class OmniCompleter( Completer ):
return super( OmniCompleter, self ).ShouldUseNow( start_column ) return super( OmniCompleter, self ).ShouldUseNow( start_column )
return self.ShouldUseNowInner( start_column ) return self.ShouldUseNowInner( start_column )
def ShouldUseNowInner( self, start_column ): def ShouldUseNowInner( self, start_column ):
if not self.omnifunc: if not self.omnifunc:
return False return False
@ -58,6 +59,7 @@ class OmniCompleter( Completer ):
else: else:
return self.CandidatesForQueryAsyncInner( query, unused_start_column ) return self.CandidatesForQueryAsyncInner( query, unused_start_column )
def CandidatesForQueryAsyncInner( self, query, unused_start_column ): def CandidatesForQueryAsyncInner( self, query, unused_start_column ):
if not self.omnifunc: if not self.omnifunc:
self.stored_candidates = None self.stored_candidates = None
@ -104,6 +106,7 @@ class OmniCompleter( Completer ):
else: else:
return self.CandidatesFromStoredRequestInner() return self.CandidatesFromStoredRequestInner()
def CandidatesFromStoredRequestInner( self ): def CandidatesFromStoredRequestInner( self ):
return self.stored_candidates if self.stored_candidates else [] return self.stored_candidates if self.stored_candidates else []

View File

@ -24,7 +24,13 @@ def MockVimModule():
"""The 'vim' module is something that is only present when running inside the """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. """ 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 = MagicMock()
vim_mock.eval = MagicMock( return_value = '' ) vim_mock.eval = MagicMock( side_effect = VimEval )
sys.modules[ 'vim' ] = vim_mock sys.modules[ 'vim' ] = vim_mock
return vim_mock return vim_mock

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python
#
# Copyright (C) 2013 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/>.
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' } ] ) )

View File

@ -41,6 +41,11 @@ def CurrentColumn():
return vim.current.window.cursor[ 1 ] return vim.current.window.cursor[ 1 ]
def TextAfterCursor():
"""Returns the text after CurrentColumn."""
return vim.current.line[ CurrentColumn(): ]
def GetUnsavedBuffers(): def GetUnsavedBuffers():
def BufferModified( buffer_number ): def BufferModified( buffer_number ):
to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number ) to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number )