Refine nvim-cmp completion behavior by applying useful filtering and sorting for candidates from Rust Analyzer. (Won't cause side effects on other cmp sources)
nvim-cmp-lsp-rs
Refine completion behavior by applying useful filtering and sorting for candidates, but only specific to Rust filetype (or rather Rust-Analyzer).
Before (click the picture and jump to #1 to see details) [][#1]
[#1]: https://github.com/zjp-CN/nvim-cmp-lsp-rs/issues/1
and after (both are improved, which is better depends on your usecase!)
One of the improvements is alphabetic sorting separately on inherent methods, in-scope trait methods and to-be-imported trait methods.
For more usage, jump to Usage section by skipping mutters in Background.
Have you been aware of the great [comparators][cmp-comparators] in [nvim-cmp]?
[nvim-cmp]: https://github.com/hrsh7th/nvim-cmp/tree/main [cmp-comparators]: https://github.com/hrsh7th/nvim-cmp/blob/97dc716fc914c46577a4f254035ebef1aa72558a/lua/cmp/config/compare.lua
The default sorting is defined as below, which means if you use [LazyVim], you'll see the weird completion item list exactly as the first picture shows.
[LazyVim]: https://www.lazyvim.org/
sorting = {
priority_weight = 2,
comparators = {
compare.offset,
compare.exact,
-- compare.scopes,
compare.score,
compare.recently_used,
compare.locality,
compare.kind,
-- compare.sort_text,
compare.length,
compare.order,
},
}
The problem is not about each sorting, but about the combination of sortings.
compare.kind is very close to the tail, meaning it'll be used only if all the sortings before it return nil.
A comparator is a sorting function [used] in table.sort to compare two arguments passed in.
A comparator in the form of fn(a, b) returns
- true to indicate a is prior to b
- false to indicate b is prior to a
- nil to indicate comparison result is the same or uncertain: like for the same lsp.CompletionItemKind
So if you want a simplist and general solution, putting require("cmp").config.compare.kind first might be good. It sort the completion items in [completionItemKind] order, but with Text kind always lowest priority and Snippet kind a bit higher in some cases.
[completionItemKind]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
You may notice sometimes the ordering is not good for Rust codebases!
- You don't want Snippets have higher priorities: search in RA's [manual] with
snippetkeyword, and there are
- You want some kinds to be higher priorities: [CompletionItemKind] treats Variables and Fields lower then Methods,
- You want features sepecific to Rust. Like
[manual]: https://rust-analyzer.github.io/manual.html [LuaSnip]: https://github.com/L3MON4D3/LuaSnip [nvim-scissors]: https://github.com/chrisgrieser/nvim-scissors
Why are you telling me this story or details?
- Share what I found lately. I didn't realize nvim-cmp could change the sorting behavior so much easily
- Encourage you to check out the comparators, tweak it a bit so that feel comfortable when seeing completion popup.
- Knowing more details helps you use or write related code to enjoy the wonderful completion experience
- I don't want to extend the sorting functions to other LSP/languanges. So the background hopefully can
Usage
This plugin should be a plugin of nvim-cmp, which means the completion behavior is affacted by specifying sorting.comparators and entry_filter in nvim-cmp.
Here's how to do in lazy.nvim (NOT default setting)
{ "hrsh7th/nvim-cmp", keys = { -- See opts.combo from nvim-cmp-lsp-rs below { "<leader>bc", "<cmd>lua require'cmplsprs'.combo()<cr>", desc = "(nvim-cmp) switch comparators" }, }, dependencies = { { "zjp-CN/nvim-cmp-lsp-rs", ---@type cmplsprs.Opts opts = { -- Filter out import items starting with one of these prefixes. -- A prefix can be crate name, module name or anything an import -- path starts with, no matter it's complete or incomplete. -- Only literals are recognized: no regex matching. unwanted_prefix = { "color", "ratatui::style::Styled" }, -- make these kinds prior to others -- e.g. make Module kind first, and then Function second, -- the rest ordering is merged from a default kind list kind = function(k) -- The argument in callback is type-aware with opts annotated, -- so you can type the CompletionKind easily. return { k.Module, k.Function } end, -- Override the default comparator list provided by this plugin. -- Mainly used with key binding to switch between these Comparators. combo = { -- The key is the name for combination of comparators and used -- in notification in swiching. -- The value is a list of comparators functions or a function -- to generate the list. alphabeticlabelbutunderscorelast = function() local comparators = require("cmplsprs").comparators return { comparators.sortbylabelbutunderscore_last } end, recentlyUsed_sortText = function() local compare = require("cmp").config.compare local comparators = require("cmplsprs").comparators -- Mix cmp sorting function with cmplsprs. return { compare.recently_used, compare.sort_text, comparators.sortbylabelbutunderscore_last } end, }, }, }, }, --@param opts cmp.ConfigSchema opts = function(_, opts) local cmplsprs = require("cmplsprs") local comparators = cmplsprs.comparators local compare = require("cmp").config.compare
opts.sorting.comparators = { compare.exact, compare.score, -- comparators.inherentimportinscope, comparators.inscopeinherentimport, comparators.sortbylabelbutunderscore_last, }
for _, source in ipairs(opts.sources) do cmplsprs.filterout.entryfilter(source) end
return opts end, }
unwanted_prefix only applies to import items, with items in scope unaffacted.
When specifying the kind list, you can directly pass in a list of integer that lsp.CompletionItemKind represents. So kind = { 9, 3 } behaves the same way.
It's totally fine to omit opts on nvim-cmp-lsp-rs, and dynamically change them in runtime when you already open a rust file and RA starts.
The way to inject into nvim-cmp's config is by overriding comparators list and entryfilter for nvimlsp source.
NOTE: we use a callback to modify opts on nvim-cmp, because opts table form can't make this plugin work. Maybe this is a nuance from lazy.nvim. Therefore, you should tweak your original opts to this way.
The order in comparators list matters. inscopeinherentimport or inherentimportinscope is used with kind. They will sort Rust entries by kind, and then group for inherent vs trait methods and in-scope vs import items. They will also affact non-Rust entries, but only sort them by kind.
sortbylabelbutunderscore_last will sort the entries the first comparator emits nil on. The sort is alphabetic, but _ will be put to the last. This is most desired because it means low priority in most cases. If you don't want to be last, use sortby_label instead.
You may notice there are two comparators built in nvim-cmp as the first and second. It provides better typed characters matching across entry kinds. See [here] for demonstration of lacking them.
[here]: https://github.com/zjp-CN/nvim-cmp-lsp-rs/issues/4
The entryfilter will only apply to nvimlsp source and rust filetype. Currently, it filters out import methods with unwanted_prefix.
cmplsprs.comparators
Two sorting functions are provided.
inscopeinherentimport
local cmprs = require("cmplsp_rs")
local comparators = cmp_rs.comparators
opts.sorting.comparators = {
comparators.inscopeinherentimport,
comparators.sortbylabelbutunderscore_last,
}
Sorting Behaviors:
- in-scope items
- import items
[entry 1] s (this is a Field)
[entry 2] render(โฆ)
[entry 3] zzzz()
[entry 4] f() (as AAA)
[entry 5] z() (as AAA)
[entry 6] into() (as Into)
[entry 7] try_into() (as TryInto)
... other kinds
[entry 24] bg() (use coloreyre::owocolors::OwoColorize)
... methods from coloreyre::owocolors::OwoColorize trait
[entry 79] yellow() (use coloreyre::owocolors::OwoColorize)
[entry 80] type_id() (use std::any::Any)
[entry 81] borrow() (use std::borrow::Borrow)
[entry 82] borrow_mut() (use std::borrow::BorrowMut)
... other kinds
inherentimportinscope
local cmprs = require("cmplsp_rs")
local comparators = cmp_rs.comparators
opts.sorting.comparators = {
comparators.inherentimportinscope,
comparators.sortbylabelbutunderscore_last,
}
Sorting Behaviors:
- kind order: Field -> Method -> rest
[entry 1] s (this is a Field)
[entry 2] render(โฆ)
[entry 3] zzzz()
[entry 4] f() (as AAA)
[entry 5] z() (as AAA)
[entry 6] into() (as Into)
[entry 7] try_into() (as TryInto)
[entry 8] bg() (use coloreyre::owocolors::OwoColorize)
... (use coloreyre::owocolors::OwoColorize)
[entry 63] yellow() (use coloreyre::owocolors::OwoColorize)
[entry 64] type_id() (use std::any::Any)
[entry 65] borrow() (use std::borrow::Borrow)
[entry 66] borrow_mut() (use std::borrow::BorrowMut)
...
cmplsprs.combo
See the configuration example in usage above.
You can bind the function to a key to switch between defined combinations.
The default is like
{ ["inherentimportinscope + sortbylabelbutunderscore_last"] = { M.comparators.inherentimportinscope, M.comparators.sortbylabelbutunderscore_last }, ["inscopeinherentimport + sortbylabelbutunderscore_last"] = { M.comparators.inscopeinherentimport, M.comparators.sortbylabelbutunderscore_last }, }
cmplsprs.log
You can call require("cmplsprs").log.register() to listen on menu_opened event emitted by nvim-cmp to obtain the last and sorted completion result displayed to you.
The log file is entries.log right under the current folder.
This is mainly used in debuging.
[entry 1] s
filter_text: s
kind: Field
[entry 2] render(โฆ)
filter_text: render
kind: Method
[entry 3] zzzz()
filter_text: zzzz
kind: Method
[entry 4] f() (as AAA)
filter_text: f
kind: Method
[entry 5] z() (as AAA)
filter_text: z
kind: Method
[entry 6] into() (as Into)
filter_text: into
kind: Method
[entry 8] box
filter_text: box
kind: Snippet
[entry 80] type_id() (use std::any::Any)
filtertext: typeid
kind: Method
data: {
fullimportpath = "std::any::Any",
imported_name = "Any"
}
[entry 84] arc (use std::sync::Arc)
filter_text: arc
kind: Snippet
data: {
fullimportpath = "std::sync::Arc",
imported_name = "Arc"
}
Dynamic Setting in Runtime
The filtering and sorting in nvim-cmp are pretty dynamic and straightforward.
Each entry from various sources will be passed into entry_filter, a function that accepts an entry and returns the entry if the function returns true. Then a table of entries will be sorted by a list of comparator.
Dynamic Unwanted Prefix
:lua rs = require'cmplsprs'
:lua rs.unwanted_prefix.get() -- query
:lua rs.unwanted_prefix.set(...) -- override
:lua rs.unwanted_prefix.add(...) -- append
:lua rs.unwanted_prefix.remove(...) -- delete
The argument ... for them can be a string or string[].
unwanted_prefix is default to empty.
Dynamic Kind Sorting
:lua rs = require'cmplsprs'
:lua rs.kind.get() -- query
:lua rs.kind.set(...) -- set kind ordering with most priorities
The argument ... for set can be one of these
stringname for kindstring[]names for kindintegerinteger for kindinteger[]integers for kindfunction(k) -> integer[]where k is of kind type and you can easily write
k.Module etc with lsp help.
e.g. for the last case, you can write
rs.kind.set(function(k) return { k.Module, k.Function })
The current default ordering is as follows:
Variable Value Field EnumMember Property TypeParameter Method Module Function Constructor Interface Class Struct Enum Constant Unit Keyword Snippet Color File Folder Event Operator Reference Text
Dynamic Comparators
If you want to override comparators nvim-cmp calls when experimenting them, you can run these commands.
:lua rs = require'cmplsprs' :lua cmp = require'cmp' :lua cmp.getconfig().sorting.comparators = { rs.comparators.sortby_label }