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
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

View File

@ -18,6 +18,7 @@
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
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():

View File

@ -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 []

View File

@ -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

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 ]
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 )