Skip to content

Commit 0e811a2

Browse files
thomasfaingnaertprabirshrestha
authored andcommitted
Filter completions (#391)
* Filter completed items based on typed word * Remove logs * Only calculate typed_word once * Revert "Only calculate typed_word once" This reverts commit 161c7f4. * Revert "Remove logs" This reverts commit 861c70f. * Revert "Filter completed items based on typed word" This reverts commit c450e83. * Reimplement filtering completion items * Remove s:remove_typed_part * Fix for NeoVim * Use 'ignorecase' to filter items * Allow configuration of filter * Add documentation * Add example for typed_pattern * Change surrounding of `/$` to indicate it is a tag * Add g:lsp_ignorecase * Use filterText for filtering * Add 'contains' filter * Fix tests * Avoid using regex * Fix bug in 'contains' filter * Use completion trigger chars for typed text * Move copy * Update documentation * Use textEdit starting position * Change default typed_pattern * Update documentation * Fix documentation * Add note about narrowing down completions * Fix tests * Make filtering note clearer * Remove duplicate text
1 parent a1860ae commit 0e811a2

File tree

4 files changed

+172
-49
lines changed

4 files changed

+172
-49
lines changed

autoload/lsp/omni.vim

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ let s:completion_status_pending = 'pending'
3737
let s:is_user_data_support = has('patch-8.0.1493')
3838
let s:user_data_key = 'vim-lsp/textEdit'
3939
let s:user_data_additional_edits_key = 'vim-lsp/additionalTextEdits'
40+
let s:user_data_insert_start_key = 'vim-lsp/insertStart'
41+
let s:user_data_filtertext_key = 'vim-lsp/filterText'
4042

4143
" }}}
4244

@@ -65,26 +67,79 @@ function! lsp#omni#complete(findstart, base) abort
6567
while s:completion['status'] is# s:completion_status_pending && !complete_check()
6668
sleep 10m
6769
endwhile
68-
let Is_prefix_match = s:create_prefix_matcher(a:base)
69-
let s:completion['matches'] = filter(s:completion['matches'], {_, match -> Is_prefix_match(match['word'])})
70+
71+
" TODO: Allow multiple servers
72+
let l:server_name = l:info['server_names'][0]
73+
let l:server_info = lsp#get_server_info(l:server_name)
74+
75+
let l:typed_pattern = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'typed_pattern') ? l:server_info['config']['typed_pattern'] : '\k*$'
76+
let l:current_line = strpart(getline('.'), 0, col('.') - 1)
77+
78+
let s:start_pos = min(map(copy(s:completion['matches']), {_, item -> s:get_insertion_point(item, l:current_line, l:typed_pattern) }))
79+
80+
let l:filter = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'filter') ? l:server_info['config']['filter'] : { 'name': 'none' }
81+
let l:last_typed_word = strpart(l:current_line, s:start_pos)
82+
83+
if l:filter['name'] ==? 'prefix'
84+
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:prefix_filter(item, l:last_typed_word)})
85+
elseif l:filter['name'] ==? 'contains'
86+
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:contains_filter(item, l:last_typed_word)})
87+
endif
88+
7089
let s:completion['status'] = ''
71-
return s:completion['matches']
90+
91+
call timer_start(0, function('s:display_completions'))
92+
93+
return exists('v:none') ? v:none : []
7294
endif
7395
endif
7496
endfunction
7597

76-
function! s:normalize_word(word) abort
77-
if &g:ignorecase
78-
return tolower(a:word)
98+
function! s:get_insertion_point(item, current_line, typed_pattern) abort
99+
if !has_key(a:item, 'user_data')
100+
let l:insert_start = -1
101+
else
102+
let l:insert_start = get(json_decode(a:item['user_data']), s:user_data_insert_start_key, -1)
103+
endif
104+
105+
if l:insert_start >= 0
106+
return l:insert_start
79107
else
80-
return a:word
108+
return match(a:current_line, a:typed_pattern)
81109
endif
82110
endfunction
83111

84-
function! s:create_prefix_matcher(prefix) abort
85-
let l:prefix = s:normalize_word(a:prefix)
112+
function! s:get_filter_label(item) abort
113+
if !has_key(a:item, 'user_data')
114+
return trim(a:item['word'])
115+
endif
86116

87-
return { word -> stridx(s:normalize_word(word), l:prefix) == 0 }
117+
let l:user_data = json_decode(a:item['user_data'])
118+
return trim(get(l:user_data, s:user_data_filtertext_key, a:item['word']))
119+
endfunction
120+
121+
function! s:prefix_filter(item, last_typed_word) abort
122+
let l:label = s:get_filter_label(a:item)
123+
124+
if g:lsp_ignorecase
125+
return stridx(tolower(l:label), tolower(a:last_typed_word)) == 0
126+
else
127+
return stridx(l:label, a:last_typed_word) == 0
128+
endif
129+
endfunction
130+
131+
function! s:contains_filter(item, last_typed_word) abort
132+
let l:label = s:get_filter_label(a:item)
133+
134+
if g:lsp_ignorecase
135+
return stridx(tolower(l:label), tolower(a:last_typed_word)) >= 0
136+
else
137+
return stridx(l:label, a:last_typed_word) >= 0
138+
endif
139+
endfunction
140+
141+
function! s:display_completions(timer) abort
142+
call complete(s:start_pos + 1, s:completion['matches'])
88143
endfunction
89144

90145
function! s:handle_omnicompletion(server_name, complete_counter, data) abort
@@ -111,7 +166,7 @@ endfunction
111166

112167
function! lsp#omni#get_kind_text(completion_item, ...) abort
113168
let l:server = get(a:, 1, '')
114-
if empty(l:server) " server name
169+
if empty(l:server) " server name
115170
let l:completion_item_kinds = s:default_completion_item_kinds
116171
else
117172
if !has_key(s:completion_item_kinds, l:server)
@@ -125,7 +180,7 @@ function! lsp#omni#get_kind_text(completion_item, ...) abort
125180
let l:completion_item_kinds = s:completion_item_kinds[l:server]
126181
endif
127182

128-
return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind'])
183+
return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind'])
129184
\ ? l:completion_item_kinds[a:completion_item['kind']] : ''
130185
endfunction
131186

@@ -172,36 +227,13 @@ function! s:get_completion_result(server_name, data) abort
172227
let l:incomplete = 0
173228
endif
174229

175-
let l:matches = type(l:items) == type([]) ? map(l:items, {_, item -> lsp#omni#get_vim_completion_item(item, a:server_name, 1) }) : []
230+
let l:matches = type(l:items) == type([]) ? map(l:items, {_, item -> lsp#omni#get_vim_completion_item(item, a:server_name) }) : []
176231

177232
return {'matches': l:matches, 'incomplete': l:incomplete}
178233
endfunction
179234

180-
181-
function! s:remove_typed_part(word) abort
182-
let l:current_line = strpart(getline('.'), 0, col('.') - 1)
183-
184-
let l:overlap_length = 0
185-
let l:i = 1
186-
let l:max_possible_overlap = min([len(a:word), len(l:current_line)])
187-
188-
while l:i <= l:max_possible_overlap
189-
let l:current_line_suffix = strpart(l:current_line, len(l:current_line) - l:i, l:i)
190-
let l:word_prefix = strpart(a:word, 0, l:i)
191-
192-
if l:current_line_suffix == l:word_prefix
193-
let l:overlap_length = l:i
194-
endif
195-
196-
let l:i += 1
197-
endwhile
198-
199-
return strpart(a:word, l:overlap_length)
200-
endfunction
201-
202235
function! lsp#omni#default_get_vim_completion_item(item, ...) abort
203236
let l:server_name = get(a:, 1, '')
204-
let l:do_remove_typed_part = get(a:, 2, 0)
205237

206238
if g:lsp_insert_text_enabled && has_key(a:item, 'insertText') && !empty(a:item['insertText'])
207239
if has_key(a:item, 'insertTextFormat') && a:item['insertTextFormat'] != 1
@@ -215,10 +247,6 @@ function! lsp#omni#default_get_vim_completion_item(item, ...) abort
215247
let l:abbr = a:item['label']
216248
endif
217249

218-
if l:do_remove_typed_part
219-
let l:word = s:remove_typed_part(l:word)
220-
endif
221-
222250
let l:kind = lsp#omni#get_kind_text(a:item, l:server_name)
223251

224252
let l:completion = {
@@ -240,27 +268,37 @@ function! lsp#omni#default_get_vim_completion_item(item, ...) abort
240268
call lsp#log(l:no_support_error_message)
241269
endif
242270

271+
let l:user_data = {}
272+
273+
" Use '-1' to signal "no specific insertion point" set.
274+
let l:user_data[s:user_data_insert_start_key] = -1
275+
243276
" add user_data in completion item, when
244277
" 1. provided user_data
245278
" 2. provided textEdit or additionalTextEdits
246279
" 3. textEdit value is Dictionary or additionalTextEdits is non-empty list
247280
if g:lsp_text_edit_enabled
248281
let l:text_edit = get(a:item, 'textEdit', v:null)
249282
let l:additional_text_edits = get(a:item, 'additionalTextEdits', v:null)
250-
let l:user_data = {}
251283

252284
" type check
253285
if type(l:text_edit) == type({})
254286
let l:user_data[s:user_data_key] = l:text_edit
287+
let l:user_data[s:user_data_insert_start_key] = l:text_edit['range']['start']['character']
255288
endif
256289

257290
if type(l:additional_text_edits) == type([]) && !empty(l:additional_text_edits)
258291
let l:user_data[s:user_data_additional_edits_key] = l:additional_text_edits
259292
endif
293+
endif
260294

261-
if !empty(l:user_data)
262-
let l:completion['user_data'] = json_encode(l:user_data)
263-
endif
295+
" Store filterText in user_data
296+
if s:is_user_data_support && has_key(a:item, 'filterText')
297+
let l:user_data[s:user_data_filtertext_key] = a:item['filterText']
298+
endif
299+
300+
if !empty(l:user_data)
301+
let l:completion['user_data'] = json_encode(l:user_data)
264302
endif
265303

266304
if has_key(a:item, 'detail') && !empty(a:item['detail'])
@@ -270,7 +308,7 @@ function! lsp#omni#default_get_vim_completion_item(item, ...) abort
270308
if has_key(a:item, 'documentation')
271309
if type(a:item['documentation']) == type('') " field is string
272310
let l:completion['info'] .= a:item['documentation']
273-
elseif type(a:item['documentation']) == type({}) &&
311+
elseif type(a:item['documentation']) == type({}) &&
274312
\ has_key(a:item['documentation'], 'value')
275313
" field is MarkupContent (hopefully 'plaintext')
276314
let l:completion['info'] .= a:item['documentation']['value']

doc/vim-lsp.txt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ CONTENTS *vim-lsp-contents*
3535
g:lsp_signature_help_enabled |g:lsp_signature_help_enabled|
3636
g:lsp_fold_enabled |g:lsp_fold_enabled|
3737
g:lsp_hover_conceal |g:lsp_hover_conceal|
38+
g:lsp_ignorecase |g:lsp_ignorecase|
3839
Functions |vim-lsp-functions|
3940
enable |vim-lsp-enable|
4041
disable |vim-lsp-disable|
@@ -518,6 +519,14 @@ g:lsp_hover_conceal *g:lsp_hover_conceal*
518519

519520
To override this setting per server, see |vim-lsp-hover_conceal|.
520521

522+
g:lsp_ignorecase *g:lsp_ignorecase*
523+
Type: |Boolean|
524+
Default: the value of |'ignorecase'|
525+
526+
Determines whether or not case should be ignored when filtering completion
527+
items. See |vim-lsp-completion-filter|. By default, the value of
528+
|'ignorecase'| is used.
529+
521530
===============================================================================
522531
FUNCTIONS *vim-lsp-functions*
523532

@@ -668,6 +677,10 @@ The vim |dict| containing information about the server.
668677

669678
'cmd': function('s:myserver_cmd')
670679

680+
Using the `config` key, you can also specify a custom 'typed word
681+
pattern', or a custom filter for completion items, see
682+
|vim-lsp-typed_pattern| and |vim-lsp-completion-filter|.
683+
671684
The following per-server configuration options are supported by vim-lsp.
672685

673686
* hover_conceal
@@ -702,6 +715,60 @@ The vim |dict| containing information about the server.
702715
Example:
703716
'config': { 'completion_item_kinds': {'26': 'type' } }
704717

718+
typed_pattern *vim-lsp-typed_pattern*
719+
Type: |String| (|pattern|)
720+
Default: `'\k*$'`
721+
722+
Vim-lsp will automatically detect the text you have typed so far when invoking
723+
completion. It does this by checking the textEdit's range of each completion
724+
item.
725+
726+
If the language server doesn't use textEdits, you can use a |regexp| to
727+
determine what you have typed so far. The pattern is matched against the
728+
current line, from column 0 up until the cursor's position. Thus, |/$| means
729+
"current cursor position" in this context.
730+
731+
For example:
732+
'config': { 'typed_pattern': '\k*$' }
733+
734+
This uses all characters in `'iskeyword'` in front of the cursor as typed
735+
word.
736+
737+
This key is also used to align the completion menu: the completion menu is
738+
placed so its left border is at the column that matches the start of the
739+
`typed_pattern`.
740+
741+
filter *vim-lsp-completion-filter*
742+
743+
You can filter the completion items returned from the server by specifying a
744+
completion filter using the `filter` key in the server info's `config` |dict|.
745+
The value of the `filter` key is itself a |dict| containing at least a key
746+
`name`, which specifies which filter to use.
747+
748+
For the meaning of "already typed text" in the remainder of this section, see
749+
|vim-lsp-typed_pattern|. The case (in)sensitivity of the matching is
750+
determined by |g:lsp_ignorecase|.
751+
752+
Example:
753+
'config': { 'filter': { 'name': 'none' } }
754+
755+
Available filters are:
756+
- `none` (default)
757+
Do not filter completion items, use all items returned from the
758+
language server.
759+
760+
- `prefix`
761+
Only allow completion items that are a prefix of the already typed
762+
word.
763+
764+
- `contains`
765+
Only allow completion items that contain the already typed word.
766+
767+
Note: After triggering completion with |i_CTRL-X_CTRL-O|, further filtering is
768+
only possible by adding to the already typed prefix (even if you're using the
769+
`contains` filter). If you'd like to retrigger the filtering, you will have to
770+
press CTRL-X CTRL-O again.
771+
705772
lsp#stop_server *vim-lsp-stop_server*
706773

707774
Used to stop the server.

plugin/lsp.vim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ let g:lsp_preview_max_height = get(g:, 'lsp_preview_max_height', -1)
3838
let g:lsp_signature_help_enabled = get(g:, 'lsp_signature_help_enabled', 1)
3939
let g:lsp_fold_enabled = get(g:, 'lsp_fold_enabled', 1)
4040
let g:lsp_hover_conceal = get(g:, 'lsp_hover_conceal', 1)
41+
let g:lsp_ignorecase = get(g:, 'lsp_ignorecase', &ignorecase)
4142

4243
let g:lsp_get_vim_completion_item = get(g:, 'lsp_get_vim_completion_item', [function('lsp#omni#default_get_vim_completion_item')])
4344
let g:lsp_get_supported_capabilities = get(g:, 'lsp_get_supported_capabilities', [function('lsp#default_get_supported_capabilities')])

test/lsp/omni.vimspec

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
" Remove keys we're not interested in
2+
function! PreprocessItem(item) abort
3+
let l:item = a:item
4+
5+
if has_key(l:item, 'user_data')
6+
let l:user_data = json_decode(l:item['user_data'])
7+
unlet l:user_data['vim-lsp/insertStart']
8+
let l:item['user_data'] = json_encode(l:user_data)
9+
endif
10+
11+
if l:item['user_data'] == '{}'
12+
unlet l:item['user_data']
13+
endif
14+
15+
return l:item
16+
endfunction
17+
118
Describe lsp#omni
219
Describe lsp#omni#get_vim_completion_item
320
It should return item with proper kind
@@ -20,7 +37,7 @@ Describe lsp#omni
2037
\}
2138
let got = lsp#omni#get_vim_completion_item(item)
2239

23-
Assert Equals(got, want)
40+
Assert Equals(PreprocessItem(got), want)
2441
End
2542

2643
It should return result contained user_data['vim-lsp/textEdit'], if exist textEdit in item
@@ -63,7 +80,7 @@ Describe lsp#omni
6380
\}
6481
let got = lsp#omni#get_vim_completion_item(item)
6582

66-
Assert Equals(got, want)
83+
Assert Equals(PreprocessItem(got), want)
6784
End
6885

6986
It should not add user_data, if provide textEdit property and textEdit value is null
@@ -91,7 +108,7 @@ Describe lsp#omni
91108
\}
92109
let got = lsp#omni#get_vim_completion_item(item)
93110

94-
Assert Equals(got, want)
111+
Assert Equals(PreprocessItem(got), want)
95112
End
96113

97114
It should return item with newlines in 'menu' replaced
@@ -114,7 +131,7 @@ Describe lsp#omni
114131
\}
115132
let got = lsp#omni#get_vim_completion_item(item)
116133

117-
Assert Equals(got, want)
134+
Assert Equals(PreprocessItem(got), want)
118135
End
119136
End
120137
End

0 commit comments

Comments
 (0)