说道契机, 我想是因为看到微软最近开源了新的静态分析工具pyright, 是一个 Python的静态分析工具, 这类工具主要用来语法检查和补全. 正好我一直 在用YouCompleteMe, 花点时间看看它的工作原理还是比较值得的, 本文会经常 更新, 我会将自己调试过程中的一些想法分享出来.

基本原理

使用YouCompleteMe的时候, 后台会自启动一个ycmd进程, 这个就是server端, 补全的时候, 就会发送相关的代码片段给server端, 由它返回可能的补全内容

在插件目录(~/.vim/plugged/YouCompleteMe)下:

  1. server端位于third_party/ycmd
  2. client端位于python/ycm

两者是通过HTTP协议进行通信.

几个patch

既然是由YouCompleteMe启动ycmd, 我就直接把这部分的代码修改掉, 我们手动启动ycmd 对这个server端进行调试.

client端的patch

这部分的patch主要做了4件事:

  1. hmac_secret固定, 调试时不需要每次重新生成;
  2. 将本地的HTTP端口固定下来(本文中是30827);
  3. 把下面的utils.SafePopen注释掉, 启动vim时不直接启动ycmd;
  4. 不去生成临时的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py
index 530d7d13..ee8cf77c 100644
--- a/python/ycm/youcompleteme.py
+++ b/python/ycm/youcompleteme.py
@@ -139,7 +139,8 @@ class YouCompleteMe( object ):

self._SetLogLevel()

- hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
+ # hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
+ hmac_secret = b"hello world"
options_dict = dict( self._user_options )
options_dict[ 'hmac_secret' ] = utils.ToUnicode(
base64.b64encode( hmac_secret ) )
@@ -147,10 +148,13 @@ class YouCompleteMe( object ):
'keep_logfiles' ]

# The temp options file is deleted by ycmd during startup.
- with NamedTemporaryFile( delete = False, mode = 'w+' ) as options_file:
- json.dump( options_dict, options_file )
+ # with NamedTemporaryFile( delete = False, mode = 'w+' ) as options_file:
+ # json.dump( options_dict, options_file )

- server_port = utils.GetUnusedLocalhostPort()
+ # print(options_dict)
+ # server_port = utils.GetUnusedLocalhostPort()
+ server_port = 30827
+ # print(options_file.name)

BaseRequest.server_location = 'http://127.0.0.1:' + str( server_port )
BaseRequest.hmac_secret = hmac_secret
@@ -169,7 +173,7 @@ class YouCompleteMe( object ):
args = [ python_interpreter,
paths.PathToServerScript(),
'--port={0}'.format( server_port ),
- '--options_file={0}'.format( options_file.name ),
+ # '--options_file={0}'.format( options_file.name ),
'--log={0}'.format( self._user_options[ 'log_level' ] ),
'--idle_suicide_seconds={0}'.format(
SERVER_IDLE_SUICIDE_SECONDS ) ]
@@ -183,9 +187,9 @@ class YouCompleteMe( object ):

if self._user_options[ 'keep_logfiles' ]:
args.append( '--keep_logfiles' )
-
- self._server_popen = utils.SafePopen( args, stdin_windows = PIPE,
- stdout = PIPE, stderr = PIPE )
+ print(args)
+ # self._server_popen = utils.SafePopen( args, stdin_windows = PIPE,
+ # stdout = PIPE, stderr = PIPE )


def _SetUpLogging( self ):

server端的patch

这部分的patch做了2件事:

  1. 阻止ycmd在退出时删掉options_file, 使用我们自己的文件, 一定是不能删的.
  2. 在收到退出信号时, 不进行关闭, 防止我们一直去重新开启服务端.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
diff --git a/ycmd/__main__.py b/ycmd/__main__.py
index 39e14f72..33ce635c 100644
--- a/ycmd/__main__.py
+++ b/ycmd/__main__.py
@@ -135,7 +135,7 @@ def SetupOptions( options_file ):
options = user_options_store.DefaultOptions()
user_options = json.loads( ReadFile( options_file ) )
options.update( user_options )
- utils.RemoveIfExists( options_file )
+ # utils.RemoveIfExists( options_file )
hmac_secret = ToBytes( base64.b64decode( options[ 'hmac_secret' ] ) )
del options[ 'hmac_secret' ]
user_options_store.SetAll( options )
diff --git a/ycmd/handlers.py b/ycmd/handlers.py
index 38ee87e2..18fcdcb3 100644
--- a/ycmd/handlers.py
+++ b/ycmd/handlers.py
@@ -241,7 +241,7 @@ def DebugInfo():
@app.post( '/shutdown' )
def Shutdown():
LOGGER.info( 'Received shutdown request' )
- ServerShutdown()
+ # ServerShutdown()

return _JsonResponse( True )

手动启动ycmd

启动时需要指定options_file, 以及我们预先指定的端口:

1
2
3
4
> /usr/bin/python  \
~/.vim/plugged/YouCompleteMe/python/ycm/../../third_party/ycmd/ycmd \
--port=30827 \
--options_file=ycmd.json

ycmd.json, 此文件是由ycm生成的, 我这里单独保存了一份, 可直接使用

TL; DR 点击打开折叠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
{
"filepath_blacklist": {
"jsx": "1",
"xml": "1",
"html": "1"
},
"clangd_uses_ycmd_caching": 1,
"autoclose_preview_window_after_insertion": 0,
"collect_identifiers_from_tags_files": 0,
"autoclose_preview_window_after_completion": 1,
"filetype_blacklist": {
"unite": "1"
},
"key_list_previous_completion": [
"<S-TAB>",
"<Up>"
],
"seed_identifiers_with_syntax": 0,
"auto_stop_csharp_server": 1,
"show_diagnostics_ui": 1,
"disable_for_files_larger_than_kb": 1000,
"clangd_args": [],
"collect_identifiers_from_comments_and_strings": 0,
"max_diagnostics_to_display": 30,
"complete_in_strings": 1,
"filetype_specific_completion_to_disable": {
"gitcommit": "1"
},
"max_num_identifier_candidates": 10,
"open_loclist_on_ycm_diags": 1,
"extra_conf_vim_data": [],
"keep_logfiles": 0,
"use_ultisnips_completer": 1,
"min_num_of_chars_for_completion": 1,
"enable_diagnostic_highlighting": 1,
"key_detailed_diagnostics": "<leader>d",
"warning_symbol": ">>",
"auto_trigger": 1,
"goto_buffer_command": "same-buffer",
"echo_current_diagnostic": 1,
"confirm_extra_conf": 1,
"gocode_binary_path": "",
"racerd_binary_path": "",
"filter_diagnostics": {},
"rust_src_path": "",
"python_binary_path": "/usr/bin/python3",
"java_jdtls_use_clean_workspace": 1,
"clangd_binary_path": "",
"server_python_interpreter": "",
"max_num_candidates": 50,
"min_identifier_candidate_chars": 0,
"always_populate_location_list": 0,
"add_preview_to_completeopt": 0,
"global_ycm_extra_conf": "~/.vim/ycm_extra_conf.py",
"enable_diagnostic_signs": 1,
"use_clangd": 1,
"key_list_select_completion": [
"<TAB>",
"<Down>"
],
"extra_conf_globlist": [],
"log_level": "info",
"complete_in_comments": 0,
"key_invoke_completion": "<C-Space>",
"filepath_completion_use_working_dir": 0,
"auto_start_csharp_server": 1,
"cache_omnifunc": 1,
"filetype_whitelist": {
"*": "1"
},
"csharp_server_port": 0,
"key_list_stop_completion": [
"<C-y>"
],
"error_symbol": ">>",
"godef_binary_path": "",
"semantic_triggers": {
"c": [
"->",
"."
],
"perl": [
"->"
],
"cpp,objcpp": [
"->",
".",
"::"
],
"lua": [
".",
":"
],
"php": [
"->",
"::"
],
"objc": [
"->",
"."
],
"cs,java,javascript,d,vim,ruby,python,perl6,scala,vb,elixir,go": [
"."
],
"erlang": [
":"
]
},
"hmac_secret": "aGVsbG8gd29ybGQ=",
"server_keep_logfiles": 0
}

使用Tornado调试

ycmd使用的是bottle框架, 也是可以tornado进行调试的, 欢迎大家自己尝试.