• YouCompleteMe插件的一些实现


    一、vim对python脚本的支持

    vim作为一个开发环境,不仅支持原生的vim脚本,还支持其它的动态脚本语言,例如lua、ruby、perl、python等。这些脚本语言在vim的源代码中都是通过if_XXX型文件实现。具体对于python的支持来说,实现在if_python.h、if_python3.c中。
    vim一个流行的自动补全插件YouCompleteMe就是使用了python扩展功能。所以,如果要使用YouCompleteMe这个插件的话,vim在构建的时候需要支持python。可以通过--version命令行选项列出vim支持的特性、构建时使用的参数等信息。

    二、YouCompleteMe脚本对python的使用

    1、YouCompleteMe使用的自动加载模块

    根据vim的规定,自动运行的脚本放在autoload文件夹中,YouCompleteMe使用的自动运行脚本一些相关的代码在下面摘录出来。
    可以看到,python代码通过import vim来导入vim模块,并通过该模块和vim实时通讯。
    YouCompleteMe/autoload/youcompleteme.vim
    22 " This needs to be called outside of a function
    23 let s:script_folder_path = escape( expand( '<sfile>:p:h' ), '\' )
    ……
    53 function! s:UsingPython3()
    54 if has('python3')
    55 return 1
    56 endif
    57 return 0
    58 endfunction
    59
    60
    61 let s:using_python3 = s:UsingPython3()
    62 let s:python_until_eof = s:using_python3 ? "python3 << EOF" : "python << EOF"
    63 let s:python_command = s:using_python3 ? "py3 " : "py "
    ……
    151 function! s:SetUpPython() abort
    152 exec s:python_until_eof
    153 from __future__ import unicode_literals
    154 from __future__ import print_function
    155 from __future__ import division
    156 from __future__ import absolute_import
    157
    158 import os
    159 import sys
    160 import traceback
    161 import vim
    162
    163 # Add python sources folder to the system path.
    164 script_folder = vim.eval( 's:script_folder_path' )
    165 sys.path.insert( 0, os.path.join( script_folder, '..', 'python' ) )
    166
    167 from ycm.setup import SetUpSystemPaths, SetUpYCM
    168
    169 # We enclose this code in a try/except block to avoid backtraces in Vim.
    170 try:
    171 SetUpSystemPaths()
    172
    173 # Import the modules used in this file.
    174 from ycm import base, vimsupport
    175
    176 ycm_state = SetUpYCM()
    177 except Exception as error:
    178 # We don't use PostVimMessage or EchoText from the vimsupport module because
    179 # importing this module may fail.
    180 vim.command( 'redraw | echohl WarningMsg' )
    181 for line in traceback.format_exc().splitlines():
    182 vim.command( "echom '{0}'".format( line.replace( "'", "''" ) ) )
    183
    184 vim.command( "echo 'YouCompleteMe unavailable: {0}'"
    185 .format( str( error ).replace( "'", "''" ) ) )
    186 vim.command( 'echohl None' )
    187 vim.command( 'return 0' )
    188 else:
    189 vim.command( 'return 1' )
    190 EOF
    191 endfunction
    ……
    831 function! s:CompleterCommand(...)
    832 " CompleterCommand will call the OnUserCommand function of a completer.
    833 " If the first arguments is of the form "ft=..." it can be used to specify the
    834 " completer to use (for example "ft=cpp"). Else the native filetype completer
    835 " of the current buffer is used. If no native filetype completer is found and
    836 " no completer was specified this throws an error. You can use
    837 " "ft=ycm:ident" to select the identifier completer.
    838 " The remaining arguments will be passed to the completer.
    839 let arguments = copy(a:000)
    840 let completer = ''
    841
    842 if a:0 > 0 && strpart(a:1, 0, 3) == 'ft='
    843 if a:1 == 'ft=ycm:ident'
    844 let completer = 'identifier'
    845 endif
    846 let arguments = arguments[1:]
    847 endif
    848
    849 exec s:python_command "ycm_state.SendCommandRequest(" .
    850 \ "vim.eval( 'l:arguments' ), vim.eval( 'l:completer' ) )"
    851 endfunction

    2、如何获得插件使用的python文件目录

    按照python的规定,如果希望一个文件夹作为python模块的搜索路径,需要将它追加到sys.path列表中。
    在autoload脚本中,将该vim脚本本身(YouCompleteMe/autoload/youcompleteme.vim)所在路径的"../python"文件夹添加到sys.path中,从而该文件夹下的所有python脚本(模块)都可以在python代码中导入和使用。
    163 # Add python sources folder to the system path.
    164 script_folder = vim.eval( 's:script_folder_path' )
    165 sys.path.insert( 0, os.path.join( script_folder, '..', 'python' ) )
    其实也就是YouCompleteMe/python/文件夹,下面是插件文件夹结构
    tsecer@harry: tree -L 2 YouCompleteMe
    YouCompleteMe
    ├── appveyor.yml
    ├── autoload
    │   └── youcompleteme.vim
    ├── ci
    │   ├── appveyor
    │   └── travis
    ├── codecov.yml
    ├── CODE_OF_CONDUCT.md
    ├── CONTRIBUTING.md
    ├── COPYING.txt
    ├── doc
    │   └── youcompleteme.txt
    ├── install.py
    ├── install.sh
    ├── plugin
    │   └── youcompleteme.vim
    ├── print_todos.sh
    ├── python
    │   ├── test_requirements.txt
    │   └── ycm
    ├── README.md
    ├── run_tests.py
    ├── third_party
    │   ├── pythonfutures
    │   ├── requests-futures
    │   └── ycmd
    └── tox.ini

    12 directories, 15 files
    tsecer@harry:

    3、使用python脚本

    将插件的python文件夹添加到sys.path之后,就可以让python从这些文件夹下搜索模块(module)。例如,
    167 from ycm.setup import SetUpSystemPaths, SetUpYCM
    就是从YouCompleteMe/python/ycm/setup.py文件中导入SetUpSystemPaths和SetUpYCM函数。
    而SetUpYCM函数返回的对象(名字为ycm_state)
    176 ycm_state = SetUpYCM()
    是之后vim脚本操作的“句柄”变量,也就是之后的大部分操作都是通过该对象的方法来完成。
    167 from ycm.setup import SetUpSystemPaths, SetUpYCM
    168
    169 # We enclose this code in a try/except block to avoid backtraces in Vim.
    170 try:
    171 SetUpSystemPaths()
    172
    173 # Import the modules used in this file.
    174 from ycm import base, vimsupport
    175
    176 ycm_state = SetUpYCM()

    三、ycm_state的由来

    从前面可以看到,ycm_state主要由setup.py中的SetUpYCM函数返回。
    在YouCompleteMe/python/ycm/setup.py文件中,可以看到函数实现为
    44 def SetUpYCM():
    45 from ycm import base
    46 from ycmd import user_options_store
    47 from ycm.youcompleteme import YouCompleteMe
    48
    49 base.LoadJsonDefaultsIntoVim()
    50
    51 user_options_store.SetAll( base.BuildServerConf() )
    52
    53 return YouCompleteMe( user_options_store.GetAll() )
    也即是返回的是一个YouCompleteMe对象。

    四、server相关

    1、server的启动

    在执行YouCompleteMe类的构造函数时,会通过_SetupServer函数启动server
    YouCompleteMe/python/ycm/youcompleteme.py
    135 def _SetupServer( self ):
    136 self._available_completers = {}
    137 self._user_notified_about_crash = False
    138 self._filetypes_with_keywords_loaded = set()
    139 self._server_is_ready_with_cache = False
    140
    141 hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
    142 options_dict = dict( self._user_options )
    143 options_dict[ 'hmac_secret' ] = utils.ToUnicode(
    144 base64.b64encode( hmac_secret ) )
    145 options_dict[ 'server_keep_logfiles' ] = self._user_options[
    146 'keep_logfiles' ]
    147
    148 # The temp options file is deleted by ycmd during startup.
    149 with NamedTemporaryFile( delete = False, mode = 'w+' ) as options_file:
    150 json.dump( options_dict, options_file )
    151
    152 server_port = utils.GetUnusedLocalhostPort()
    153
    154 BaseRequest.server_location = 'http://127.0.0.1:' + str( server_port )
    155 BaseRequest.hmac_secret = hmac_secret
    156
    157 try:
    158 python_interpreter = paths.PathToPythonInterpreter()
    159 except RuntimeError as error:
    160 error_message = (
    161 "Unable to start the ycmd server. {0}. "
    162 "Correct the error then restart the server "
    163 "with ':YcmRestartServer'.".format( str( error ).rstrip( '.' ) ) )
    164 self._logger.exception( error_message )
    165 vimsupport.PostVimMessage( error_message )
    166 return
    167
    168 args = [ python_interpreter,
    169 paths.PathToServerScript(),
    170 '--port={0}'.format( server_port ),
    171 '--options_file={0}'.format( options_file.name ),
    172 '--log={0}'.format( self._user_options[ 'log_level' ] ),
    173 '--idle_suicide_seconds={0}'.format(
    174 SERVER_IDLE_SUICIDE_SECONDS ) ]
    175
    176 self._server_stdout = utils.CreateLogfile(
    177 SERVER_LOGFILE_FORMAT.format( port = server_port, std = 'stdout' ) )
    178 self._server_stderr = utils.CreateLogfile(
    179 SERVER_LOGFILE_FORMAT.format( port = server_port, std = 'stderr' ) )
    180 args.append( '--stdout={0}'.format( self._server_stdout ) )
    181 args.append( '--stderr={0}'.format( self._server_stderr ) )
    182
    183 if self._user_options[ 'keep_logfiles' ]:
    184 args.append( '--keep_logfiles' )
    185
    186 self._server_popen = utils.SafePopen( args, stdin_windows = PIPE,
    187 stdout = PIPE, stderr = PIPE )

    2、server端口的选择

    从实现上看,操作系统比较清楚哪些端口没有使用。所以这里其实是让操作系统自动选择一个没有使用的端口地址。
    YouCompleteMe/third_party/ycmd/ycmd/utils.py
    193 def GetUnusedLocalhostPort():
    194 sock = socket.socket()
    195 # This tells the OS to give us any free port in the range [1024 - 65535]
    196 sock.bind( ( '', 0 ) )
    197 port = sock.getsockname()[ 1 ]
    198 sock.close()
    199 return port

    3、server根据文件类型确定completer的逻辑

    主要就是到ycmd/${filetype}/hook.py创建文件,其中的${filetype}替换为具体的、运行时文件名。当然,一些特殊文件夹不需要,例如general文件夹下的completer,它们作为基本的、常驻的completer,是静态访问的。
    third_party/ycmd/ycmd/completers/completer_utils.py
    162 def PathToFiletypeCompleterPluginLoader( filetype ):
    163 return os.path.join( _PathToCompletersFolder(), filetype, 'hook.py' )
    164
    165
    166 def FiletypeCompleterExistsForFiletype( filetype ):
    167 return os.path.exists( PathToFiletypeCompleterPluginLoader( filetype ) )
    例如,ycmd中支持的文件夹包括了常见的文件类型,例如cpp类型文件就是用cpp文件夹下的hook.py创建completer对象。
    tsecer@harry: find . -type d
    .
    ./general
    ./cs
    ./objcpp
    ./typescript
    ./all
    ./go
    ./python
    ./rust
    ./objc
    ./cpp
    ./javascript
    ./__pycache__
    ./c

    五、vim请求的发送

    1、发送请求的基本内容

    可以看到,主要包括当前文件路径,输入位置的行号和列号,当前编辑buffer的内容(包括没有写回文件的内容)。
    YouCompleteMe/python/ycm/client/base_request.py
    155 def BuildRequestData( filepath = None ):
    156 """Build request for the current buffer or the buffer corresponding to
    157 |filepath| if specified."""
    158 current_filepath = vimsupport.GetCurrentBufferFilepath()
    159
    160 if filepath and current_filepath != filepath:
    161 # Cursor position is irrelevant when filepath is not the current buffer.
    162 return {
    163 'filepath': filepath,
    164 'line_num': 1,
    165 'column_num': 1,
    166 'file_data': vimsupport.GetUnsavedAndSpecifiedBufferData( filepath )
    167 }
    168
    169 line, column = vimsupport.CurrentLineAndColumn()
    170
    171 return {
    172 'filepath': current_filepath,
    173 'line_num': line + 1,
    174 'column_num': column + 1,
    175 'file_data': vimsupport.GetUnsavedAndSpecifiedBufferData( current_filepath )
    176 }

    2、文件类型的同步

    类型是客户端通过vim的ft变量获得,然后传递ycmd进程的。
    YouCompleteMe/python/ycm/vimsupport.py
    123 def GetUnsavedAndSpecifiedBufferData( including_filepath ):
    124 """Build part of the request containing the contents and filetypes of all
    125 dirty buffers as well as the buffer with filepath |including_filepath|."""
    126 buffers_data = {}
    127 for buffer_object in vim.buffers:
    128 buffer_filepath = GetBufferFilepath( buffer_object )
    129 if not ( BufferModified( buffer_object ) or
    130 buffer_filepath == including_filepath ):
    131 continue
    132
    133 buffers_data[ buffer_filepath ] = {
    134 # Add a newline to match what gets saved to disk. See #1455 for details.
    135 'contents': JoinLinesAsUnicode( buffer_object ) + '\n',
    136 'filetypes': FiletypesForBuffer( buffer_object )
    137 }
    138
    139 return buffers_data
    ……
    599 def FiletypesForBuffer( buffer_object ):
    600 # NOTE: Getting &ft for other buffers only works when the buffer has been
    601 # visited by the user at least once, which is true for modified buffers
    602 return GetBufferOption( buffer_object, 'ft' ).split( '.' )

    六、ycmd的内置completer

    1、identifier completer

    对于常见的C++,使用clang作为语义分析并完成智能提示。但是如果没有clang支持,ycmd也有内置的identifier提示。这个提示功能相对比较朴素,就是对文件的内容进行基于“正则表达式”的拆分,从文件中拆分出不同的单词(可能还有其它的特殊结构,例如C语言中的注释等)。
    identifier completer需要ycm_core.so文件的支持。
    YouCompleteMe\third_party\ycmd\ycmd\identifier_utils.py
    30 C_STYLE_COMMENT = "/\*(?:\n|.)*?\*/"
    31 CPP_STYLE_COMMENT = "//.*?$"
    32 PYTHON_STYLE_COMMENT = "#.*?$"
    ……
    101 # At least c++ and javascript support unicode identifiers, and identifiers may
    102 # start with unicode character, e.g. ålpha. So we need to accept any identifier
    103 # starting with an 'alpha' character or underscore. i.e. not starting with a
    104 # 'digit'. The following regex will match:
    105 # - A character which is alpha or _. That is a character which is NOT:
    106 # - a digit (\d)
    107 # - non-alphanumeric
    108 # - not an underscore
    109 # (The latter two come from \W which is the negation of \w)
    110 # - Followed by any alphanumeric or _ characters
    111 DEFAULT_IDENTIFIER_REGEX = re.compile( r"[^\W\d]\w*", re.UNICODE )
    ……
    183 def ExtractIdentifiersFromText( text, filetype = None ):
    184 return re.findall( IdentifierRegexForFiletype( filetype ), text )
    如果指定文件类型的completer没有返回任何完成选项(completion是completer返回的可选集合结果:if not completions),则使用默认的completer(_server_state.GetGeneralCompleter().ComputeCandidates( request_data ) )。
    88 @app.post( '/completions' )
    89 def GetCompletions():
    90 _logger.info( 'Received completion request' )
    91 request_data = RequestWrap( request.json )
    92 ( do_filetype_completion, forced_filetype_completion ) = (
    93 _server_state.ShouldUseFiletypeCompleter( request_data ) )
    94 _logger.debug( 'Using filetype completion: %s', do_filetype_completion )
    95
    96 errors = None
    97 completions = None
    98
    99 if do_filetype_completion:
    100 try:
    101 completions = ( _server_state.GetFiletypeCompleter(
    102 request_data[ 'filetypes' ] )
    103 .ComputeCandidates( request_data ) )
    104
    105 except Exception as exception:
    106 if forced_filetype_completion:
    107 # user explicitly asked for semantic completion, so just pass the error
    108 # back
    109 raise
    110 else:
    111 # store the error to be returned with results from the identifier
    112 # completer
    113 stack = traceback.format_exc()
    114 _logger.error( 'Exception from semantic completer (using general): ' +
    115 "".join( stack ) )
    116 errors = [ BuildExceptionResponse( exception, stack ) ]
    117
    118 if not completions and not forced_filetype_completion:
    119 completions = ( _server_state.GetGeneralCompleter()
    120 .ComputeCandidates( request_data ) )
    121
    122 return _JsonResponse(
    123 BuildCompletionResponse( completions if completions else [],
    124 request_data[ 'start_column' ],
    125 errors = errors ) )
    而对于通用completer来说,它们之间没有互斥关系,一旦触发,所有的completer都会被执行。可以看到流程是循环追加的模式。
    另外也可以看到,它会将编辑过程中输入的标识符(OnInsertLeave)也加入到选项集合中,并且没有删除。这也意味着只要输入过某个标识符,即使之后被删除,它依然会存在于可选集中。
    YouCompleteMe/third_party/ycmd/ycmd/completers/general/general_completer_store.py
    82 def ComputeCandidates( self, request_data ):
    83 if not self.ShouldUseNow( request_data ):
    84 return []
    85
    86 candidates = []
    87 for completer in self._current_query_completers:
    88 candidates += completer.ComputeCandidates( request_data )
    89
    90 return candidates
    ……
    172 def OnInsertLeave( self, request_data ):
    173 self._AddIdentifierUnderCursor( request_data )
    174
    175
    176 def OnCurrentIdentifierFinished( self, request_data ):
    177 self._AddPreviousIdentifier( request_data )

    2、如何识别一个identifier输入完成

    这个在ycm的客户端侧做的判断。当输入一个字符时,通过CurrentIdentifierFinished函数判断当前identifier是否输入完成。如何判断是一个identifier这个还是跟文件类型有关,并且和ycmd使用的文件内标识符识别正则表达式模式相同(前一个小段中的DEFAULT_IDENTIFIER_REGEX变量)。
    YouCompleteMe/python/ycm/base.py
    28 from ycmd import identifier_utils
    ……
    59 def CurrentIdentifierFinished():
    60 line, current_column = vimsupport.CurrentLineContentsAndCodepointColumn()
    61 previous_char_index = current_column - 1
    62 if previous_char_index < 0:
    63 return True
    64 filetype = vimsupport.CurrentFiletypes()[ 0 ]
    65 regex = identifier_utils.IdentifierRegexForFiletype( filetype )
    66
    67 for match in regex.finditer( line ):
    68 if match.end() == previous_char_index:
    69 return True
    70 # If the whole line is whitespace, that means the user probably finished an
    71 # identifier on the previous line.
    72 return line[ : current_column ].isspace()

    3、C语言的提示

    completer下只有CPP的文件夹而没有C文件夹,对于C文件是如何处理的呢?
    这个其实同样是有cpp文件夹下的clang支持
    YouCompleteMe/third_party/ycmd/ycmd/completers/cpp/clang_completer.py
    46 CLANG_FILETYPES = set( [ 'c', 'cpp', 'objc', 'objcpp' ] )
    ……
    70 def SupportedFiletypes( self ):
    71 return CLANG_FILETYPES

    4、文件名的提示

    在使用YouCompleteMe的时候,还可以发现ycm还是支持文件名自动匹配的。查看ycm的实现,可以看到completer中有一个filename的完成器,顾名思义,它应该就是对文件名进行自动完成的工具了。
    可以看到,其中是通过路径分隔符来触发自动完成的,所以,即使在C++中代码中输入了除法符号(/),也会触发当做根目录下的文件名匹配。
    YouCompleteMe/third_party/ycmd/ycmd/completers/general/filename_completer.py
    42 def __init__( self, user_options ):
    43 super( FilenameCompleter, self ).__init__( user_options )
    44
    45 # On Windows, backslashes are also valid path separators.
    46 self._triggers = [ '/', '\\' ] if OnWindows() else [ '/' ]
    ……
    75 def ShouldUseNowInner( self, request_data ):
    76 current_line = request_data[ 'line_value' ]
    77 start_codepoint = request_data[ 'start_codepoint' ]
    78
    79 # inspect the previous 'character' from the start column to find the trigger
    80 # note: 1-based still. we subtract 1 when indexing into current_line
    81 trigger_codepoint = start_codepoint - 1
    82
    83 return ( trigger_codepoint > 0 and
    84 current_line[ trigger_codepoint - 1 ] in self._triggers )
    但是使用ycm的时候,在输入
    #include ""
    的双引号内部,也会触发文件名的自动完成,但是这个是clang完成的。验证的方法是把completer文件夹下的cpp重命名,之后发现#include内部的文件名完成失效,但是使用"./"还是会触发文件名匹配。

    七、vim内置的completer

    runtime\autoload\README.txt
    Omni completion files:
    ccomplete.vim C
    csscomplete.vim HTML / CSS
    htmlcomplete.vim HTML
    javascriptcomplete.vim Javascript
    phpcomplete.vim PHP
    pythoncomplete.vim Python
    rubycomplete.vim Ruby
    syntaxcomplete.vim from syntax highlighting
    xmlcomplete.vim XML (uses files in the xml directory)
    在文件类型插件中,如果识别使用的C语言,则设置默认的omnifunction为内置的ccomplete#Complete,也就是ccomplete.vim中的Complete函数。其它一些常用的,例如python也有类似的机制。当使用这种机制的时候,通过ctrl-x ctrl-o就可以打开并使用内置的自动完成功能。
    runtime\ftplugin\c.vim
    " Set completion with CTRL-X CTRL-O to autoloaded function.
    if exists('&ofu')
    setlocal ofu=ccomplete#Complete
    endif
    关于vim内置的omnifunc及自动完成功能,可以通过vim内置的
    :h ins-completion
    :h omnifunc
    查看相关帮助手册。

    八、一些总结

    关键的是文件类型,也就是当前文件的ft信息,如果ft为空,通常所有的智能提示都会失效。
    例如,在cmd中执行
    :set ft=
    之后,几乎所有的只能提示都会失效。

  • 相关阅读:
    ETL讲解(很详细!!!)
    必须掌握的30种SQL语句优化
    亿级Web系统搭建——单机到分布式集群
    运行第一个容器
    Docker 架构详解
    容器 What, Why, How
    Docker 组件如何协作?
    部署 DevStack
    通过例子学习 Keystone
    创建 Image
  • 原文地址:https://www.cnblogs.com/tsecer/p/15995662.html
Copyright © 2020-2023  润新知