From c07350181f58c5c9cba4ceaa3556521b181fe5cb Mon Sep 17 00:00:00 2001 From: hrsh7th Date: Sun, 13 Feb 2022 16:44:45 +0900 Subject: [PATCH] Implement matching config. Fix #796 --- doc/cmp.txt | 18 ++++++++++++++++-- lua/cmp/config/default.lua | 6 ++++++ lua/cmp/core.lua | 14 +++++++++++++- lua/cmp/entry.lua | 27 +++++++++++++++++++++------ lua/cmp/matcher.lua | 30 +++++++++++++++++++++++------- lua/cmp/matcher_spec.lua | 31 ++++++++++++++++++++++++------- lua/cmp/source.lua | 22 ++++++++++++++-------- lua/cmp/types/cmp.lua | 6 ++++++ lua/cmp/utils/async.lua | 2 +- lua/cmp/view.lua | 4 ++-- 10 files changed, 126 insertions(+), 34 deletions(-) diff --git a/doc/cmp.txt b/doc/cmp.txt index 90a4d3c..1f4acc7 100644 --- a/doc/cmp.txt +++ b/doc/cmp.txt @@ -209,12 +209,11 @@ NOTE: You can call these functions in mapping via `lua require('cmp').compl > cmp.setup { mapping = { - [''] = cmp.mapping(function(fallback) + [''] = cmp.mapping(function(fallback) if cmp.visible() then if cmp.complete_common_string() then return end - return cmp.select_next_item() end fallback() end, { 'i', 'c' }), @@ -422,6 +421,21 @@ formatting.format~ NOTE: The `vim.CompletedItem` can have special properties `abbr_hl_group`, `kind_hl_group` and `menu_hl_group`. + *cmp-config.matching.disallow_fuzzy_matching* +matching.disallow_fuzzy_matching~ + `boolean` + Specify disallow or allow fuzzy matching. + + *cmp-config.matching.disallow_partial_matching* +matching.disallow_partial_matching~ + `boolean` + Specify disallow or allow partial matching. + + *cmp-config.matching.disallow_prefix_unmatching* +matching.disallow_prefix_unmatching~ + `boolean` + Specify disallow or allow prefix unmatching. + *cmp-config.sorting.priority_weight* sorting.priority_weight~ `number` diff --git a/lua/cmp/config/default.lua b/lua/cmp/config/default.lua index 63dae2b..ff0311b 100644 --- a/lua/cmp/config/default.lua +++ b/lua/cmp/config/default.lua @@ -88,6 +88,12 @@ return function() end, }, + matching = { + disallow_fuzzy_matching = false, + disallow_partial_matching = false, + disallow_prefix_unmatching = false, + }, + sorting = { priority_weight = 2, comparators = { diff --git a/lua/cmp/core.lua b/lua/cmp/core.lua index 8df922d..58d9b15 100644 --- a/lua/cmp/core.lua +++ b/lua/cmp/core.lua @@ -220,12 +220,24 @@ end ---Complete common string for current completed entries. core.complete_common_string = function(self) - if not self.view:visible() then + if not self.view:visible() or self.view:get_active_entry() then return false end + config.set_onetime({ + sources = config.get().sources, + matching = { + disallow_prefix_unmatching = true, + disallow_partial_matching = true, + disallow_fuzzy_matching = true, + } + }) + + self:filter() self.filter:sync(1000) + config.set_onetime({}) + local cursor = api.get_cursor() local offset = self.view:get_offset() local common_string diff --git a/lua/cmp/entry.lua b/lua/cmp/entry.lua index aa8fda4..8d324d5 100644 --- a/lua/cmp/entry.lua +++ b/lua/cmp/entry.lua @@ -337,13 +337,28 @@ end ---Match line. ---@param input string +---@param matching_config cmp.MatchingConfig ---@return { score: number, matches: table[] } -entry.match = function(self, input) - return self.match_cache:ensure({ input, self.resolved_completion_item and 1 or 0 }, function() - local filter_text = self:get_filter_text() +entry.match = function(self, input, matching_config) + return self.match_cache:ensure({ + input, + self.resolved_completion_item and 1 or 0, + matching_config.disallow_fuzzy_matching and 1 or 0, + matching_config.disallow_partial_matching and 1 or 0, + matching_config.disallow_prefix_unmatching and 1 or 0, + }, function() + local option = { + disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, + disallow_partial_matching = matching_config.disallow_partial_matching, + disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching, + synonyms = { + self:get_word(), + self:get_completion_item().label + } + } local score, matches, _ - score, matches = matcher.match(input, filter_text, { self:get_word(), self:get_completion_item().label }) + score, matches = matcher.match(input, self:get_filter_text(), option) -- Support the language server that doesn't respect VSCode's behaviors. if score == 0 then @@ -355,13 +370,13 @@ entry.match = function(self, input) accept = accept or string.match(prefix, '^[^%a]+$') accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true) if accept then - score, matches = matcher.match(input, prefix .. filter_text, { self:get_word(), self:get_completion_item().label }) + score, matches = matcher.match(input, prefix .. self:get_filter_text(), option) end end end end - if filter_text ~= self:get_completion_item().label then + if self:get_filter_text() ~= self:get_completion_item().label then _, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() }) end diff --git a/lua/cmp/matcher.lua b/lua/cmp/matcher.lua index a68d665..7649f04 100644 --- a/lua/cmp/matcher.lua +++ b/lua/cmp/matcher.lua @@ -72,9 +72,11 @@ end ---Match entry ---@param input string ---@param word string ----@param words string[] +---@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean } ---@return number -matcher.match = function(input, word, words) +matcher.match = function(input, word, option) + option = option or {} + -- Empty input if #input == 0 then return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {} @@ -85,7 +87,14 @@ matcher.match = function(input, word, words) return 0, {} end - --- Gather matched regions + -- Check prefix matching. + if option.disallow_prefix_unmatching then + if not char.match(string.byte(input, 1), string.byte(word, 1)) then + return 0, {} + end + end + + -- Gather matched regions local matches = {} local input_start_index = 1 local input_end_index = 1 @@ -105,6 +114,11 @@ matcher.match = function(input, word, words) word_bound_index = word_bound_index + 1 end + -- Check partial matching. + if option.disallow_partial_matching and #matches > 1 then + return 0, {} + end + if #matches == 0 then return 0, {} end @@ -116,11 +130,11 @@ matcher.match = function(input, word, words) if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then prefix = true else - for _, w in ipairs(words or {}) do + for _, synonym in ipairs(option.synonyms or {}) do prefix = true local o = 1 for i = matches[1].input_match_start, matches[1].input_match_end do - if not char.match(string.byte(w, o), string.byte(input, i)) then + if not char.match(string.byte(synonym, o), string.byte(input, i)) then prefix = false break end @@ -152,8 +166,10 @@ matcher.match = function(input, word, words) -- Check remaining input as fuzzy if matches[#matches].input_match_end < #input then - if prefix and matcher.fuzzy(input, word, matches) then - return score, matches + if not option.disallow_fuzzy_matching then + if prefix and matcher.fuzzy(input, word, matches) then + return score, matches + end end return 0, {} end diff --git a/lua/cmp/matcher_spec.lua b/lua/cmp/matcher_spec.lua index 1f8b0d1..768988e 100644 --- a/lua/cmp/matcher_spec.lua +++ b/lua/cmp/matcher_spec.lua @@ -16,20 +16,37 @@ describe('matcher', function() assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1) assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all')) assert.is.truthy(matcher.match('Buffer', 'Buffer') > matcher.match('Buffer', 'buffer')) + assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext')) assert.is.truthy(matcher.match('fmodify', 'fnamemodify') >= 1) assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single') >= 1) - assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode')) - assert.is.truthy(matcher.match('var_', 'var_dump') >= 1) - assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) - assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext')) - assert.is.truthy(matcher.match('call', 'calc') == 0) assert.is.truthy(matcher.match('vi', 'void#') >= 1) assert.is.truthy(matcher.match('vo', 'void#') >= 1) + assert.is.truthy(matcher.match('var_', 'var_dump') >= 1) + assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode')) assert.is.truthy(matcher.match('usela', 'useLayoutEffect') > matcher.match('usela', 'useDataLayer')) - assert.is.truthy(matcher.match('true', 'v:true', { 'true' }) == matcher.match('true', 'true')) - assert.is.truthy(matcher.match('g', 'get', { 'get' }) > matcher.match('g', 'dein#get', { 'dein#get' })) + assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) assert.is.truthy(matcher.match('2', '[[2021') >= 1) + + assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }) == matcher.match('true', 'true')) + assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }) > matcher.match('g', 'dein#get', { 'dein#get' })) + end) + + it('disallow_fuzzy_matching', function() + assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0) + assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) + end) + + it('disallow_partial_matching', function() + assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0) + assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) + assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1) + assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1) + end) + + it('disallow_prefix_unmatching', function() + assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0) + assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1) end) it('debug', function() diff --git a/lua/cmp/source.lua b/lua/cmp/source.lua index 0e171c0..b73d43e 100644 --- a/lua/cmp/source.lua +++ b/lua/cmp/source.lua @@ -61,12 +61,18 @@ source.reset = function(self) self.complete_dedup(function() end) end ----Return source option +---Return source config ---@return cmp.SourceConfig -source.get_config = function(self) +source.get_source_config = function(self) return config.get_source_config(self.name) or {} end +---Return matching config +---@return cmp.MatchingConfig +source.get_matching_config = function() + return config.get().matching +end + ---Get fetching time source.get_fetching_time = function(self) if self.status == source.SourceStatus.FETCHING then @@ -103,7 +109,7 @@ source.get_entries = function(self, ctx) inputs[o] = string.sub(ctx.cursor_before_line, o) end - local match = e:match(inputs[o]) + local match = e:match(inputs[o], self:get_matching_config()) e.score = match.score e.exact = false if e.score >= 1 then @@ -114,7 +120,7 @@ source.get_entries = function(self, ctx) end self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries) - local max_item_count = self:get_config().max_item_count or 200 + local max_item_count = self:get_source_config().max_item_count or 200 local limited_entries = {} for _, e in ipairs(entries) do table.insert(limited_entries, e) @@ -188,7 +194,7 @@ end ---Get trigger_characters ---@return string[] source.get_trigger_characters = function(self) - local c = self:get_config() + local c = self:get_source_config() if c.trigger_characters then return c.trigger_characters end @@ -206,7 +212,7 @@ end ---Get keyword_pattern ---@return string source.get_keyword_pattern = function(self) - local c = self:get_config() + local c = self:get_source_config() if c.keyword_pattern then return c.keyword_pattern end @@ -219,7 +225,7 @@ end ---Get keyword_length ---@return number source.get_keyword_length = function(self) - local c = self:get_config() + local c = self:get_source_config() if c.keyword_length then return c.keyword_length end @@ -288,7 +294,7 @@ source.complete = function(self, ctx, callback) self.context = ctx self.completion_context = completion_context self.source:complete( - vim.tbl_extend('keep', misc.copy(self:get_config()), { + vim.tbl_extend('keep', misc.copy(self:get_source_config()), { offset = self.offset, context = ctx, completion_context = completion_context, diff --git a/lua/cmp/types/cmp.lua b/lua/cmp/types/cmp.lua index c54037c..3524f22 100644 --- a/lua/cmp/types/cmp.lua +++ b/lua/cmp/types/cmp.lua @@ -77,6 +77,7 @@ cmp.ItemField.Menu = 'menu' ---@field public completion cmp.CompletionConfig ---@field public documentation cmp.DocumentationConfig|"false" ---@field public confirmation cmp.ConfirmationConfig +---@field public matching cmp.MatchingConfig ---@field public sorting cmp.SortingConfig ---@field public formatting cmp.FormattingConfig ---@field public snippet cmp.SnippetConfig @@ -103,6 +104,11 @@ cmp.ItemField.Menu = 'menu' ---@field public default_behavior cmp.ConfirmBehavior ---@field public get_commit_characters fun(commit_characters: string[]): string[] +---@class cmp.MatchingConfig +---@field public disallow_fuzzy_matching boolean +---@field public disallow_partial_matching boolean +---@field public disallow_prefix_unmatching boolean + ---@class cmp.SortingConfig ---@field public priority_weight number ---@field public comparators function[] diff --git a/lua/cmp/utils/async.lua b/lua/cmp/utils/async.lua index fae18da..8c698e7 100644 --- a/lua/cmp/utils/async.lua +++ b/lua/cmp/utils/async.lua @@ -38,8 +38,8 @@ async.throttle = function(fn, timeout) timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() vim.schedule(function() time = nil - self.running = false fn(unpack(args)) + self.running = false end) end) end, diff --git a/lua/cmp/view.lua b/lua/cmp/view.lua index 4c2d03f..2885cb1 100644 --- a/lua/cmp/view.lua +++ b/lua/cmp/view.lua @@ -50,7 +50,7 @@ end view.open = function(self, ctx, sources) local source_group_map = {} for _, s in ipairs(sources) do - local group_index = s:get_config().group_index or 0 + local group_index = s:get_source_config().group_index or 0 if not source_group_map[group_index] then source_group_map[group_index] = {} end @@ -83,7 +83,7 @@ view.open = function(self, ctx, sources) if s.offset <= offset then if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then -- source order priority bonus. - local priority = s:get_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) + local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) for _, e in ipairs(s:get_entries(ctx)) do e.score = e.score + priority