aboutsummaryrefslogtreecommitdiff
path: root/vim/bundle/YouCompleteMe/python/ycm/vimsupport.py
diff options
context:
space:
mode:
Diffstat (limited to 'vim/bundle/YouCompleteMe/python/ycm/vimsupport.py')
-rw-r--r--vim/bundle/YouCompleteMe/python/ycm/vimsupport.py978
1 files changed, 978 insertions, 0 deletions
diff --git a/vim/bundle/YouCompleteMe/python/ycm/vimsupport.py b/vim/bundle/YouCompleteMe/python/ycm/vimsupport.py
new file mode 100644
index 0000000..c4a600c
--- /dev/null
+++ b/vim/bundle/YouCompleteMe/python/ycm/vimsupport.py
@@ -0,0 +1,978 @@
+# Copyright (C) 2011, 2012 Google Inc.
+#
+# 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 __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import * # noqa
+
+from future.utils import iterkeys
+import vim
+import os
+import tempfile
+import json
+import re
+from collections import defaultdict
+from ycmd.utils import ToUnicode, ToBytes
+from ycmd import user_options_store
+
+BUFFER_COMMAND_MAP = { 'same-buffer' : 'edit',
+ 'horizontal-split' : 'split',
+ 'vertical-split' : 'vsplit',
+ 'new-tab' : 'tabedit' }
+
+FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT = (
+ 'The requested operation will apply changes to {0} files which are not '
+ 'currently open. This will therefore open {0} new files in the hidden '
+ 'buffers. The quickfix list can then be used to review the changes. No '
+ 'files will be written to disk. Do you wish to continue?' )
+
+
+def CurrentLineAndColumn():
+ """Returns the 0-based current line and 0-based current column."""
+ # 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 CurrentColumn():
+ """Returns the 0-based current column. Do NOT access the CurrentColumn in
+ vim.current.line. It doesn't exist yet when the cursor is at the end of the
+ line. 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 CurrentLineContents():
+ return ToUnicode( vim.current.line )
+
+
+def TextAfterCursor():
+ """Returns the text after CurrentColumn."""
+ return ToUnicode( vim.current.line[ CurrentColumn(): ] )
+
+
+def TextBeforeCursor():
+ """Returns the text before CurrentColumn."""
+ return ToUnicode( vim.current.line[ :CurrentColumn() ] )
+
+
+# Expects version_string in 'MAJOR.MINOR.PATCH' format, e.g. '7.4.301'
+def VimVersionAtLeast( version_string ):
+ major, minor, patch = [ int( x ) for x in version_string.split( '.' ) ]
+
+ # For Vim 7.4.301, v:version is '704'
+ actual_major_and_minor = GetIntValue( 'v:version' )
+ matching_major_and_minor = major * 100 + minor
+ if actual_major_and_minor != matching_major_and_minor:
+ return actual_major_and_minor > matching_major_and_minor
+
+ return GetBoolValue( 'has("patch{0}")'.format( patch ) )
+
+
+# Note the difference between buffer OPTIONS and VARIABLES; the two are not
+# the same.
+def GetBufferOption( buffer_object, option ):
+ # NOTE: We used to check for the 'options' property on the buffer_object which
+ # is available in recent versions of Vim and would then use:
+ #
+ # buffer_object.options[ option ]
+ #
+ # to read the value, BUT this caused annoying flickering when the
+ # buffer_object was a hidden buffer (with option = 'ft'). This was all due to
+ # a Vim bug. Until this is fixed, we won't use it.
+
+ to_eval = 'getbufvar({0}, "&{1}")'.format( buffer_object.number, option )
+ return GetVariableValue( to_eval )
+
+
+def BufferModified( buffer_object ):
+ return bool( int( GetBufferOption( buffer_object, 'mod' ) ) )
+
+
+def GetUnsavedAndCurrentBufferData():
+ buffers_data = {}
+ for buffer_object in vim.buffers:
+ if not ( BufferModified( buffer_object ) or
+ buffer_object == vim.current.buffer ):
+ continue
+
+ buffers_data[ GetBufferFilepath( buffer_object ) ] = {
+ # Add a newline to match what gets saved to disk. See #1455 for details.
+ 'contents': '\n'.join( ToUnicode( x ) for x in buffer_object ) + '\n',
+ 'filetypes': FiletypesForBuffer( buffer_object )
+ }
+
+ return buffers_data
+
+
+def GetBufferNumberForFilename( filename, open_file_if_needed = True ):
+ return GetIntValue( u"bufnr('{0}', {1})".format(
+ EscapeForVim( os.path.realpath( filename ) ),
+ int( open_file_if_needed ) ) )
+
+
+def GetCurrentBufferFilepath():
+ return GetBufferFilepath( vim.current.buffer )
+
+
+def BufferIsVisible( buffer_number ):
+ if buffer_number < 0:
+ return False
+ window_number = GetIntValue( "bufwinnr({0})".format( buffer_number ) )
+ return window_number != -1
+
+
+def GetBufferFilepath( buffer_object ):
+ if buffer_object.name:
+ return buffer_object.name
+ # Buffers that have just been created by a command like :enew don't have any
+ # buffer name so we use the buffer number for that. Also, os.getcwd() throws
+ # an exception when the CWD has been deleted so we handle that.
+ try:
+ folder_path = os.getcwd()
+ except OSError:
+ folder_path = tempfile.gettempdir()
+ return os.path.join( folder_path, str( buffer_object.number ) )
+
+
+def UnplaceSignInBuffer( buffer_number, sign_id ):
+ if buffer_number < 0:
+ return
+ vim.command(
+ 'try | exec "sign unplace {0} buffer={1}" | catch /E158/ | endtry'.format(
+ sign_id, buffer_number ) )
+
+
+def PlaceSign( sign_id, line_num, buffer_num, is_error = True ):
+ # libclang can give us diagnostics that point "outside" the file; Vim borks
+ # on these.
+ if line_num < 1:
+ line_num = 1
+
+ sign_name = 'YcmError' if is_error else 'YcmWarning'
+ vim.command( 'sign place {0} line={1} name={2} buffer={3}'.format(
+ sign_id, line_num, sign_name, buffer_num ) )
+
+
+def PlaceDummySign( sign_id, buffer_num, line_num ):
+ if buffer_num < 0 or line_num < 0:
+ return
+ vim.command( 'sign define ycm_dummy_sign' )
+ vim.command(
+ 'sign place {0} name=ycm_dummy_sign line={1} buffer={2}'.format(
+ sign_id,
+ line_num,
+ buffer_num,
+ )
+ )
+
+
+def UnPlaceDummySign( sign_id, buffer_num ):
+ if buffer_num < 0:
+ return
+ vim.command( 'sign undefine ycm_dummy_sign' )
+ vim.command( 'sign unplace {0} buffer={1}'.format( sign_id, buffer_num ) )
+
+
+def ClearYcmSyntaxMatches():
+ matches = VimExpressionToPythonType( 'getmatches()' )
+ for match in matches:
+ if match[ 'group' ].startswith( 'Ycm' ):
+ vim.eval( 'matchdelete({0})'.format( match[ 'id' ] ) )
+
+
+# Returns the ID of the newly added match
+# Both line and column numbers are 1-based
+def AddDiagnosticSyntaxMatch( line_num,
+ column_num,
+ line_end_num = None,
+ column_end_num = None,
+ is_error = True ):
+ group = 'YcmErrorSection' if is_error else 'YcmWarningSection'
+
+ if not line_end_num:
+ line_end_num = line_num
+
+ line_num, column_num = LineAndColumnNumbersClamped( line_num, column_num )
+ line_end_num, column_end_num = LineAndColumnNumbersClamped( line_end_num,
+ column_end_num )
+
+ if not column_end_num:
+ return GetIntValue(
+ "matchadd('{0}', '\%{1}l\%{2}c')".format( group, line_num, column_num ) )
+ else:
+ return GetIntValue(
+ "matchadd('{0}', '\%{1}l\%{2}c\_.\\{{-}}\%{3}l\%{4}c')".format(
+ group, line_num, column_num, line_end_num, column_end_num ) )
+
+
+# Clamps the line and column numbers so that they are not past the contents of
+# the buffer. Numbers are 1-based byte offsets.
+def LineAndColumnNumbersClamped( line_num, column_num ):
+ new_line_num = line_num
+ new_column_num = column_num
+
+ max_line = len( vim.current.buffer )
+ if line_num and line_num > max_line:
+ new_line_num = max_line
+
+ max_column = len( vim.current.buffer[ new_line_num - 1 ] )
+ if column_num and column_num > max_column:
+ new_column_num = max_column
+
+ return new_line_num, new_column_num
+
+
+def SetLocationList( diagnostics ):
+ """Diagnostics should be in qflist format; see ":h setqflist" for details."""
+ vim.eval( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
+
+
+def SetQuickFixList( quickfix_list, focus = False, autoclose = False ):
+ """Populate the quickfix list and open it. List should be in qflist format:
+ see ":h setqflist" for details. When focus is set to True, the quickfix
+ window becomes the active window. When autoclose is set to True, the quickfix
+ window is automatically closed after an entry is selected."""
+ vim.eval( 'setqflist( {0} )'.format( json.dumps( quickfix_list ) ) )
+ OpenQuickFixList( focus, autoclose )
+
+
+def OpenQuickFixList( focus = False, autoclose = False ):
+ """Open the quickfix list to full width at the bottom of the screen with its
+ height automatically set to fit all entries. This behavior can be overridden
+ by using the YcmQuickFixOpened autocommand.
+ See the SetQuickFixList function for the focus and autoclose options."""
+ vim.command( 'botright copen' )
+
+ SetFittingHeightForCurrentWindow()
+
+ if autoclose:
+ # This autocommand is automatically removed when the quickfix window is
+ # closed.
+ vim.command( 'au WinLeave <buffer> q' )
+
+ if VariableExists( '#User#YcmQuickFixOpened' ):
+ vim.command( 'doautocmd User YcmQuickFixOpened' )
+
+ if not focus:
+ JumpToPreviousWindow()
+
+
+def SetFittingHeightForCurrentWindow():
+ window_width = GetIntValue( 'winwidth( 0 )' )
+ fitting_height = 0
+ for line in vim.current.buffer:
+ fitting_height += len( line ) // window_width + 1
+ vim.command( '{0}wincmd _'.format( fitting_height ) )
+
+
+def ConvertDiagnosticsToQfList( diagnostics ):
+ def ConvertDiagnosticToQfFormat( diagnostic ):
+ # See :h getqflist for a description of the dictionary fields.
+ # Note that, as usual, Vim is completely inconsistent about whether
+ # line/column numbers are 1 or 0 based in its various APIs. Here, it wants
+ # them to be 1-based. The documentation states quite clearly that it
+ # expects a byte offset, by which it means "1-based column number" as
+ # described in :h getqflist ("the first column is 1").
+ location = diagnostic[ 'location' ]
+ line_num = location[ 'line_num' ]
+
+ # libclang can give us diagnostics that point "outside" the file; Vim borks
+ # on these.
+ if line_num < 1:
+ line_num = 1
+
+ text = diagnostic[ 'text' ]
+ if diagnostic.get( 'fixit_available', False ):
+ text += ' (FixIt available)'
+
+ return {
+ 'bufnr' : GetBufferNumberForFilename( location[ 'filepath' ] ),
+ 'lnum' : line_num,
+ 'col' : location[ 'column_num' ],
+ 'text' : text,
+ 'type' : diagnostic[ 'kind' ][ 0 ],
+ 'valid' : 1
+ }
+
+ return [ ConvertDiagnosticToQfFormat( x ) for x in diagnostics ]
+
+
+def GetVimGlobalsKeys():
+ return vim.eval( 'keys( g: )' )
+
+
+def VimExpressionToPythonType( vim_expression ):
+ """Returns a Python type from the return value of the supplied Vim expression.
+ If the expression returns a list, dict or other non-string type, then it is
+ returned unmodified. If the string return can be converted to an
+ integer, returns an integer, otherwise returns the result converted to a
+ Unicode string."""
+
+ result = vim.eval( vim_expression )
+ if not ( isinstance( result, str ) or isinstance( result, bytes ) ):
+ return result
+
+ try:
+ return int( result )
+ except ValueError:
+ return ToUnicode( result )
+
+
+def HiddenEnabled( buffer_object ):
+ return bool( int( GetBufferOption( buffer_object, 'hid' ) ) )
+
+
+def BufferIsUsable( buffer_object ):
+ return not BufferModified( buffer_object ) or HiddenEnabled( buffer_object )
+
+
+def EscapedFilepath( filepath ):
+ return filepath.replace( ' ' , r'\ ' )
+
+
+# Both |line| and |column| need to be 1-based
+def TryJumpLocationInOpenedTab( filename, line, column ):
+ filepath = os.path.realpath( filename )
+
+ for tab in vim.tabpages:
+ for win in tab.windows:
+ if win.buffer.name == filepath:
+ vim.current.tabpage = tab
+ vim.current.window = win
+ vim.current.window.cursor = ( line, column - 1 )
+
+ # Center the screen on the jumped-to location
+ vim.command( 'normal! zz' )
+ return True
+ # 'filename' is not opened in any tab pages
+ return False
+
+
+# Maps User command to vim command
+def GetVimCommand( user_command, default = 'edit' ):
+ vim_command = BUFFER_COMMAND_MAP.get( user_command, default )
+ if vim_command == 'edit' and not BufferIsUsable( vim.current.buffer ):
+ vim_command = 'split'
+ return vim_command
+
+
+# Both |line| and |column| need to be 1-based
+def JumpToLocation( filename, line, column ):
+ # Add an entry to the jumplist
+ vim.command( "normal! m'" )
+
+ if filename != GetCurrentBufferFilepath():
+ # We prefix the command with 'keepjumps' so that opening the file is not
+ # recorded in the jumplist. So when we open the file and move the cursor to
+ # a location in it, the user can use CTRL-O to jump back to the original
+ # location, not to the start of the newly opened file.
+ # Sadly this fails on random occasions and the undesired jump remains in the
+ # jumplist.
+ user_command = user_options_store.Value( 'goto_buffer_command' )
+
+ if user_command == 'new-or-existing-tab':
+ if TryJumpLocationInOpenedTab( filename, line, column ):
+ return
+ user_command = 'new-tab'
+
+ vim_command = GetVimCommand( user_command )
+ try:
+ vim.command( 'keepjumps {0} {1}'.format( vim_command,
+ EscapedFilepath( filename ) ) )
+ # When the file we are trying to jump to has a swap file
+ # Vim opens swap-exists-choices dialog and throws vim.error with E325 error,
+ # or KeyboardInterrupt after user selects one of the options.
+ except vim.error as e:
+ if 'E325' not in str( e ):
+ raise
+ # Do nothing if the target file is still not opened (user chose (Q)uit)
+ if filename != GetCurrentBufferFilepath():
+ return
+ # Thrown when user chooses (A)bort in .swp message box
+ except KeyboardInterrupt:
+ return
+ vim.current.window.cursor = ( line, column - 1 )
+
+ # Center the screen on the jumped-to location
+ vim.command( 'normal! zz' )
+
+
+def NumLinesInBuffer( buffer_object ):
+ # This is actually less than obvious, that's why it's wrapped in a function
+ return len( buffer_object )
+
+
+# Calling this function from the non-GUI thread will sometimes crash Vim. At
+# the time of writing, YCM only uses the GUI thread inside Vim (this used to
+# not be the case).
+# We redraw the screen before displaying the message to avoid the "Press ENTER
+# or type command to continue" prompt when editing a new C-family file.
+def PostVimMessage( message ):
+ vim.command( "redraw | echohl WarningMsg | echom '{0}' | echohl None"
+ .format( EscapeForVim( ToUnicode( message ) ) ) )
+
+
+# Unlike PostVimMesasge, this supports messages with newlines in them because it
+# uses 'echo' instead of 'echomsg'. This also means that the message will NOT
+# appear in Vim's message log.
+def PostMultiLineNotice( message ):
+ vim.command( "echohl WarningMsg | echo '{0}' | echohl None"
+ .format( EscapeForVim( ToUnicode( message ) ) ) )
+
+
+def PresentDialog( message, choices, default_choice_index = 0 ):
+ """Presents the user with a dialog where a choice can be made.
+ This will be a dialog for gvim users or a question in the message buffer
+ for vim users or if `set guioptions+=c` was used.
+
+ choices is list of alternatives.
+ default_choice_index is the 0-based index of the default element
+ that will get choosen if the user hits <CR>. Use -1 for no default.
+
+ PresentDialog will return a 0-based index into the list
+ or -1 if the dialog was dismissed by using <Esc>, Ctrl-C, etc.
+
+ See also:
+ :help confirm() in vim (Note that vim uses 1-based indexes)
+
+ Example call:
+ PresentDialog("Is this a nice example?", ["Yes", "No", "May&be"])
+ Is this a nice example?
+ [Y]es, (N)o, May(b)e:"""
+ to_eval = "confirm('{0}', '{1}', {2})".format(
+ EscapeForVim( ToUnicode( message ) ),
+ EscapeForVim( ToUnicode( "\n" .join( choices ) ) ),
+ default_choice_index + 1 )
+ return int( vim.eval( to_eval ) ) - 1
+
+
+def Confirm( message ):
+ """Display |message| with Ok/Cancel operations. Returns True if the user
+ selects Ok"""
+ return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 )
+
+
+def EchoText( text, log_as_message = True ):
+ def EchoLine( text ):
+ command = 'echom' if log_as_message else 'echo'
+ vim.command( "{0} '{1}'".format( command,
+ EscapeForVim( text ) ) )
+
+ for line in ToUnicode( text ).split( '\n' ):
+ EchoLine( line )
+
+
+# Echos text but truncates it so that it all fits on one line
+def EchoTextVimWidth( text ):
+ vim_width = GetIntValue( '&columns' )
+ truncated_text = ToUnicode( text )[ : int( vim_width * 0.9 ) ]
+ truncated_text.replace( '\n', ' ' )
+
+ old_ruler = GetIntValue( '&ruler' )
+ old_showcmd = GetIntValue( '&showcmd' )
+ vim.command( 'set noruler noshowcmd' )
+ vim.command( 'redraw' )
+
+ EchoText( truncated_text, False )
+
+ SetVariableValue( '&ruler', old_ruler )
+ SetVariableValue( '&showcmd', old_showcmd )
+
+
+def EscapeForVim( text ):
+ return ToUnicode( text.replace( "'", "''" ) )
+
+
+def CurrentFiletypes():
+ return VimExpressionToPythonType( "&filetype" ).split( '.' )
+
+
+def FiletypesForBuffer( buffer_object ):
+ # NOTE: Getting &ft for other buffers only works when the buffer has been
+ # visited by the user at least once, which is true for modified buffers
+ return GetBufferOption( buffer_object, 'ft' ).split( '.' )
+
+
+def VariableExists( variable ):
+ return GetBoolValue( "exists( '{0}' )".format( EscapeForVim( variable ) ) )
+
+
+def SetVariableValue( variable, value ):
+ vim.command( "let {0} = {1}".format( variable, json.dumps( value ) ) )
+
+
+def GetVariableValue( variable ):
+ return vim.eval( variable )
+
+
+def GetBoolValue( variable ):
+ return bool( int( vim.eval( variable ) ) )
+
+
+def GetIntValue( variable ):
+ return int( vim.eval( variable ) )
+
+
+def _SortChunksByFile( chunks ):
+ """Sort the members of the list |chunks| (which must be a list of dictionaries
+ conforming to ycmd.responses.FixItChunk) by their filepath. Returns a new
+ list in arbitrary order."""
+
+ chunks_by_file = defaultdict( list )
+
+ for chunk in chunks:
+ filepath = chunk[ 'range' ][ 'start' ][ 'filepath' ]
+ chunks_by_file[ filepath ].append( chunk )
+
+ return chunks_by_file
+
+
+def _GetNumNonVisibleFiles( file_list ):
+ """Returns the number of file in the iterable list of files |file_list| which
+ are not curerntly open in visible windows"""
+ return len(
+ [ f for f in file_list
+ if not BufferIsVisible( GetBufferNumberForFilename( f, False ) ) ] )
+
+
+def _OpenFileInSplitIfNeeded( filepath ):
+ """Ensure that the supplied filepath is open in a visible window, opening a
+ new split if required. Returns the buffer number of the file and an indication
+ of whether or not a new split was opened.
+
+ If the supplied filename is already open in a visible window, return just
+ return its buffer number. If the supplied file is not visible in a window
+ in the current tab, opens it in a new vertical split.
+
+ Returns a tuple of ( buffer_num, split_was_opened ) indicating the buffer
+ number and whether or not this method created a new split. If the user opts
+ not to open a file, or if opening fails, this method raises RuntimeError,
+ otherwise, guarantees to return a visible buffer number in buffer_num."""
+
+ buffer_num = GetBufferNumberForFilename( filepath, False )
+
+ # We only apply changes in the current tab page (i.e. "visible" windows).
+ # Applying changes in tabs does not lead to a better user experience, as the
+ # quickfix list no longer works as you might expect (doesn't jump into other
+ # tabs), and the complexity of choosing where to apply edits is significant.
+ if BufferIsVisible( buffer_num ):
+ # file is already open and visible, just return that buffer number (and an
+ # idicator that we *didn't* open a split)
+ return ( buffer_num, False )
+
+ # The file is not open in a visible window, so we open it in a split.
+ # We open the file with a small, fixed height. This means that we don't
+ # make the current buffer the smallest after a series of splits.
+ OpenFilename( filepath, {
+ 'focus': True,
+ 'fix': True,
+ 'size': GetIntValue( '&previewheight' ),
+ } )
+
+ # OpenFilename returns us to the original cursor location. This is what we
+ # want, because we don't want to disorientate the user, but we do need to
+ # know the (now open) buffer number for the filename
+ buffer_num = GetBufferNumberForFilename( filepath, False )
+ if not BufferIsVisible( buffer_num ):
+ # This happens, for example, if there is a swap file and the user
+ # selects the "Quit" or "Abort" options. We just raise an exception to
+ # make it clear to the user that the abort has left potentially
+ # partially-applied changes.
+ raise RuntimeError(
+ 'Unable to open file: {0}\nFixIt/Refactor operation '
+ 'aborted prior to completion. Your files have not been '
+ 'fully updated. Please use undo commands to revert the '
+ 'applied changes.'.format( filepath ) )
+
+ # We opened this file in a split
+ return ( buffer_num, True )
+
+
+def ReplaceChunks( chunks ):
+ """Apply the source file deltas supplied in |chunks| to arbitrary files.
+ |chunks| is a list of changes defined by ycmd.responses.FixItChunk,
+ which may apply arbitrary modifications to arbitrary files.
+
+ If a file specified in a particular chunk is not currently open in a visible
+ buffer (i.e., one in a window visible in the current tab), we:
+ - issue a warning to the user that we're going to open new files (and offer
+ her the option to abort cleanly)
+ - open the file in a new split, make the changes, then hide the buffer.
+
+ If for some reason a file could not be opened or changed, raises RuntimeError.
+ Otherwise, returns no meaningful value."""
+
+ # We apply the edits file-wise for efficiency, and because we must track the
+ # file-wise offset deltas (caused by the modifications to the text).
+ chunks_by_file = _SortChunksByFile( chunks )
+
+ # We sort the file list simply to enable repeatable testing
+ sorted_file_list = sorted( iterkeys( chunks_by_file ) )
+
+ # Make sure the user is prepared to have her screen mutilated by the new
+ # buffers
+ num_files_to_open = _GetNumNonVisibleFiles( sorted_file_list )
+
+ if num_files_to_open > 0:
+ if not Confirm(
+ FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( num_files_to_open ) ):
+ return
+
+ # Store the list of locations where we applied changes. We use this to display
+ # the quickfix window showing the user where we applied changes.
+ locations = []
+
+ for filepath in sorted_file_list:
+ ( buffer_num, close_window ) = _OpenFileInSplitIfNeeded( filepath )
+
+ ReplaceChunksInBuffer( chunks_by_file[ filepath ],
+ vim.buffers[ buffer_num ],
+ locations )
+
+ # When opening tons of files, we don't want to have a split for each new
+ # file, as this simply does not scale, so we open the window, make the
+ # edits, then hide the window.
+ if close_window:
+ # Some plugins (I'm looking at you, syntastic) might open a location list
+ # for the window we just opened. We don't want that location list hanging
+ # around, so we close it. lclose is a no-op if there is no location list.
+ vim.command( 'lclose' )
+
+ # Note that this doesn't lose our changes. It simply "hides" the buffer,
+ # which can later be re-accessed via the quickfix list or `:ls`
+ vim.command( 'hide' )
+
+ # Open the quickfix list, populated with entries for each location we changed.
+ if locations:
+ SetQuickFixList( locations )
+
+ EchoTextVimWidth( "Applied " + str( len( chunks ) ) + " changes" )
+
+
+def ReplaceChunksInBuffer( chunks, vim_buffer, locations ):
+ """Apply changes in |chunks| to the buffer-like object |buffer|. Append each
+ chunk's start to the list |locations|"""
+
+ # We need to track the difference in length, but ensuring we apply fixes
+ # in ascending order of insertion point.
+ chunks.sort( key = lambda chunk: (
+ chunk[ 'range' ][ 'start' ][ 'line_num' ],
+ chunk[ 'range' ][ 'start' ][ 'column_num' ]
+ ) )
+
+ # Remember the line number we're processing. Negative line number means we
+ # haven't processed any lines yet (by nature of being not equal to any
+ # real line number).
+ last_line = -1
+
+ line_delta = 0
+ for chunk in chunks:
+ if chunk[ 'range' ][ 'start' ][ 'line_num' ] != last_line:
+ # If this chunk is on a different line than the previous chunk,
+ # then ignore previous deltas (as offsets won't have changed).
+ last_line = chunk[ 'range' ][ 'end' ][ 'line_num' ]
+ char_delta = 0
+
+ ( new_line_delta, new_char_delta ) = ReplaceChunk(
+ chunk[ 'range' ][ 'start' ],
+ chunk[ 'range' ][ 'end' ],
+ chunk[ 'replacement_text' ],
+ line_delta, char_delta,
+ vim_buffer,
+ locations )
+ line_delta += new_line_delta
+ char_delta += new_char_delta
+
+
+# Replace the chunk of text specified by a contiguous range with the supplied
+# text.
+# * start and end are objects with line_num and column_num properties
+# * the range is inclusive
+# * indices are all 1-based
+# * the returned character delta is the delta for the last line
+#
+# returns the delta (in lines and characters) that any position after the end
+# needs to be adjusted by.
+#
+# NOTE: Works exclusively with bytes() instances and byte offsets as returned
+# by ycmd and used within the Vim buffers
+def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
+ vim_buffer, locations = None ):
+ # ycmd's results are all 1-based, but vim's/python's are all 0-based
+ # (so we do -1 on all of the values)
+ start_line = start[ 'line_num' ] - 1 + line_delta
+ end_line = end[ 'line_num' ] - 1 + line_delta
+
+ source_lines_count = end_line - start_line + 1
+ start_column = start[ 'column_num' ] - 1 + char_delta
+ end_column = end[ 'column_num' ] - 1
+ if source_lines_count == 1:
+ end_column += char_delta
+
+ # NOTE: replacement_text is unicode, but all our offsets are byte offsets,
+ # so we convert to bytes
+ replacement_lines = ToBytes( replacement_text ).splitlines( False )
+ if not replacement_lines:
+ replacement_lines = [ bytes( b'' ) ]
+ replacement_lines_count = len( replacement_lines )
+
+ end_existing_text = vim_buffer[ end_line ][ end_column : ]
+ start_existing_text = vim_buffer[ start_line ][ : start_column ]
+
+ new_char_delta = ( len( replacement_lines[ -1 ] )
+ - ( end_column - start_column ) )
+ if replacement_lines_count > 1:
+ new_char_delta -= start_column
+
+ replacement_lines[ 0 ] = start_existing_text + replacement_lines[ 0 ]
+ replacement_lines[ -1 ] = replacement_lines[ -1 ] + end_existing_text
+
+ vim_buffer[ start_line : end_line + 1 ] = replacement_lines[:]
+
+ if locations is not None:
+ locations.append( {
+ 'bufnr': vim_buffer.number,
+ 'filename': vim_buffer.name,
+ # line and column numbers are 1-based in qflist
+ 'lnum': start_line + 1,
+ 'col': start_column + 1,
+ 'text': replacement_text,
+ 'type': 'F',
+ } )
+
+ new_line_delta = replacement_lines_count - source_lines_count
+ return ( new_line_delta, new_char_delta )
+
+
+def InsertNamespace( namespace ):
+ if VariableExists( 'g:ycm_csharp_insert_namespace_expr' ):
+ expr = GetVariableValue( 'g:ycm_csharp_insert_namespace_expr' )
+ if expr:
+ SetVariableValue( "g:ycm_namespace_to_insert", namespace )
+ vim.eval( expr )
+ return
+
+ pattern = '^\s*using\(\s\+[a-zA-Z0-9]\+\s\+=\)\?\s\+[a-zA-Z0-9.]\+\s*;\s*'
+ line = SearchInCurrentBuffer( pattern )
+ existing_line = LineTextInCurrentBuffer( line )
+ existing_indent = re.sub( r"\S.*", "", existing_line )
+ new_line = "{0}using {1};\n\n".format( existing_indent, namespace )
+ replace_pos = { 'line_num': line + 1, 'column_num': 1 }
+ ReplaceChunk( replace_pos, replace_pos, new_line, 0, 0 )
+ PostVimMessage( "Add namespace: {0}".format( namespace ) )
+
+
+def SearchInCurrentBuffer( pattern ):
+ return GetIntValue( "search('{0}', 'Wcnb')".format( EscapeForVim( pattern )))
+
+
+def LineTextInCurrentBuffer( line ):
+ return vim.current.buffer[ line ]
+
+
+def ClosePreviewWindow():
+ """ Close the preview window if it is present, otherwise do nothing """
+ vim.command( 'silent! pclose!' )
+
+
+def JumpToPreviewWindow():
+ """ Jump the vim cursor to the preview window, which must be active. Returns
+ boolean indicating if the cursor ended up in the preview window """
+ vim.command( 'silent! wincmd P' )
+ return vim.current.window.options[ 'previewwindow' ]
+
+
+def JumpToPreviousWindow():
+ """ Jump the vim cursor to its previous window position """
+ vim.command( 'silent! wincmd p' )
+
+
+def JumpToTab( tab_number ):
+ """Jump to Vim tab with corresponding number """
+ vim.command( 'silent! tabn {0}'.format( tab_number ) )
+
+
+def OpenFileInPreviewWindow( filename ):
+ """ Open the supplied filename in the preview window """
+ vim.command( 'silent! pedit! ' + filename )
+
+
+def WriteToPreviewWindow( message ):
+ """ Display the supplied message in the preview window """
+
+ # This isn't something that comes naturally to Vim. Vim only wants to show
+ # tags and/or actual files in the preview window, so we have to hack it a
+ # little bit. We generate a temporary file name and "open" that, then write
+ # the data to it. We make sure the buffer can't be edited or saved. Other
+ # approaches include simply opening a split, but we want to take advantage of
+ # the existing Vim options for preview window height, etc.
+
+ ClosePreviewWindow()
+
+ OpenFileInPreviewWindow( vim.eval( 'tempname()' ) )
+
+ if JumpToPreviewWindow():
+ # We actually got to the preview window. By default the preview window can't
+ # be changed, so we make it writable, write to it, then make it read only
+ # again.
+ vim.current.buffer.options[ 'modifiable' ] = True
+ vim.current.buffer.options[ 'readonly' ] = False
+
+ vim.current.buffer[:] = message.splitlines()
+
+ vim.current.buffer.options[ 'buftype' ] = 'nofile'
+ vim.current.buffer.options[ 'swapfile' ] = False
+ vim.current.buffer.options[ 'modifiable' ] = False
+ vim.current.buffer.options[ 'readonly' ] = True
+
+ # We need to prevent closing the window causing a warning about unsaved
+ # file, so we pretend to Vim that the buffer has not been changed.
+ vim.current.buffer.options[ 'modified' ] = False
+
+ JumpToPreviousWindow()
+ else:
+ # We couldn't get to the preview window, but we still want to give the user
+ # the information we have. The only remaining option is to echo to the
+ # status area.
+ EchoText( message )
+
+
+def CheckFilename( filename ):
+ """Check if filename is openable."""
+ try:
+ # We don't want to check for encoding issues when trying to open the file
+ # so we open it in binary mode.
+ open( filename, mode = 'rb' ).close()
+ except TypeError:
+ raise RuntimeError( "'{0}' is not a valid filename".format( filename ) )
+ except IOError as error:
+ raise RuntimeError(
+ "filename '{0}' cannot be opened. {1}.".format( filename,
+ error.strerror ) )
+
+
+def BufferIsVisibleForFilename( filename ):
+ """Check if a buffer exists for a specific file."""
+ buffer_number = GetBufferNumberForFilename( filename, False )
+ return BufferIsVisible( buffer_number )
+
+
+def CloseBuffersForFilename( filename ):
+ """Close all buffers for a specific file."""
+ buffer_number = GetBufferNumberForFilename( filename, False )
+ while buffer_number != -1:
+ vim.command( 'silent! bwipeout! {0}'.format( buffer_number ) )
+ new_buffer_number = GetBufferNumberForFilename( filename, False )
+ if buffer_number == new_buffer_number:
+ raise RuntimeError( "Buffer {0} for filename '{1}' should already be "
+ "wiped out.".format( buffer_number, filename ) )
+ buffer_number = new_buffer_number
+
+
+def OpenFilename( filename, options = {} ):
+ """Open a file in Vim. Following options are available:
+ - command: specify which Vim command is used to open the file. Choices
+ are same-buffer, horizontal-split, vertical-split, and new-tab (default:
+ horizontal-split);
+ - size: set the height of the window for a horizontal split or the width for
+ a vertical one (default: '');
+ - fix: set the winfixheight option for a horizontal split or winfixwidth for
+ a vertical one (default: False). See :h winfix for details;
+ - focus: focus the opened file (default: False);
+ - watch: automatically watch for changes (default: False). This is useful
+ for logs;
+ - position: set the position where the file is opened (default: start).
+ Choices are start and end."""
+
+ # Set the options.
+ command = GetVimCommand( options.get( 'command', 'horizontal-split' ),
+ 'horizontal-split' )
+ size = ( options.get( 'size', '' ) if command in [ 'split', 'vsplit' ] else
+ '' )
+ focus = options.get( 'focus', False )
+
+ # There is no command in Vim to return to the previous tab so we need to
+ # remember the current tab if needed.
+ if not focus and command == 'tabedit':
+ previous_tab = GetIntValue( 'tabpagenr()' )
+ else:
+ previous_tab = None
+
+ # Open the file
+ CheckFilename( filename )
+ try:
+ vim.command( '{0}{1} {2}'.format( size, command, filename ) )
+ # When the file we are trying to jump to has a swap file,
+ # Vim opens swap-exists-choices dialog and throws vim.error with E325 error,
+ # or KeyboardInterrupt after user selects one of the options which actually
+ # opens the file (Open read-only/Edit anyway).
+ except vim.error as e:
+ if 'E325' not in str( e ):
+ raise
+
+ # Otherwise, the user might have chosen Quit. This is detectable by the
+ # current file not being the target file
+ if filename != GetCurrentBufferFilepath():
+ return
+ except KeyboardInterrupt:
+ # Raised when the user selects "Abort" after swap-exists-choices
+ return
+
+ _SetUpLoadedBuffer( command,
+ filename,
+ options.get( 'fix', False ),
+ options.get( 'position', 'start' ),
+ options.get( 'watch', False ) )
+
+ # Vim automatically set the focus to the opened file so we need to get the
+ # focus back (if the focus option is disabled) when opening a new tab or
+ # window.
+ if not focus:
+ if command == 'tabedit':
+ JumpToTab( previous_tab )
+ if command in [ 'split', 'vsplit' ]:
+ JumpToPreviousWindow()
+
+
+def _SetUpLoadedBuffer( command, filename, fix, position, watch ):
+ """After opening a buffer, configure it according to the supplied options,
+ which are as defined by the OpenFilename method."""
+
+ if command == 'split':
+ vim.current.window.options[ 'winfixheight' ] = fix
+ if command == 'vsplit':
+ vim.current.window.options[ 'winfixwidth' ] = fix
+
+ if watch:
+ vim.current.buffer.options[ 'autoread' ] = True
+ vim.command( "exec 'au BufEnter <buffer> :silent! checktime {0}'"
+ .format( filename ) )
+
+ if position == 'end':
+ vim.command( 'silent! normal G zz' )