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)
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.
Background
Have you been aware of the great comparators in nvim-cmp?
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.
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.
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
snippet
keyword, and there are many cool features to let you config/add these Snippets.- But if you've already used LuaSnip (or snippets edit User UI nvim-scissors), too much for Snippets kind!
- You want some kinds to be higher priorities: CompletionItemKind treats Variables and Fields lower then Methods,
then you can do nothing but typing more to wait the desired one pops up.
- Typing more is not a big problem: when you have large candidates, no matter for what sorting, you must type more characters or arrow keys :)
- The bigger problem is kinds like Variables/Fields are closer to use for you compared to other kinds.
- You want features sepecific to Rust. Like
- Items in scope are prior to that needing to import.
- You may rarely want a non-imported method appears as the first candidate when other in-scope methods exist.
- You may want local modules prior to external modules.
- Inherent methods are prior to trait methods.
- Items in scope are prior to that needing to import.
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 even though I've been using it for two years in (almost) daily coding.
- 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 neovim and nvim-cmp power us.
- I don't want to extend the sorting functions to other LSP/languanges. So the background hopefully can inspire people starting out.
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'cmp_lsp_rs'.combo()<cr>",
desc = "(nvim-cmp) switch comparators"
},
},
dependencies = {
{
"zjp-CN/nvim-cmp-lsp-rs",
---@type cmp_lsp_rs.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.
alphabetic_label_but_underscore_last = function()
local comparators = require("cmp_lsp_rs").comparators
return { comparators.sort_by_label_but_underscore_last }
end,
recentlyUsed_sortText = function()
local compare = require("cmp").config.compare
local comparators = require("cmp_lsp_rs").comparators
-- Mix cmp sorting function with cmp_lsp_rs.
return {
compare.recently_used,
compare.sort_text,
comparators.sort_by_label_but_underscore_last
}
end,
},
},
},
},
--@param opts cmp.ConfigSchema
opts = function(_, opts)
local cmp_lsp_rs = require("cmp_lsp_rs")
local comparators = cmp_lsp_rs.comparators
local compare = require("cmp").config.compare
opts.sorting.comparators = {
compare.exact,
compare.score,
-- comparators.inherent_import_inscope,
comparators.inscope_inherent_import,
comparators.sort_by_label_but_underscore_last,
}
for _, source in ipairs(opts.sources) do
cmp_lsp_rs.filter_out.entry_filter(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 entry_filter for nvim_lsp 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. inscope_inherent_import
or
inherent_import_inscope
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.
sort_by_label_but_underscore_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 sort_by_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.
The entry_filter will only apply to nvim_lsp source and rust filetype. Currently, it filters out import methods with unwanted_prefix.
Two sorting functions are provided.
local cmp_rs = require("cmp_lsp_rs")
local comparators = cmp_rs.comparators
opts.sorting.comparators = {
comparators.inscope_inherent_import,
comparators.sort_by_label_but_underscore_last,
}
Sorting Behaviors:
- in-scope items
- kind order: Field -> Method -> rest
- alphabetic sort on item names separately in the same kind
- method order: inherent -> trait
- alphabetic sort on method names in inherent
- alphabetic sort on trait names in trait methods
- alphabetic sort on method names in the same trait
- kind order: Field -> Method -> rest
- import items
- kind order
- alphabetic sort on item names separately in the same kind
- trait method order
- alphabetic sort on trait names in trait methods
- alphabetic sort on method names in the same trait
- kind order
[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 color_eyre::owo_colors::OwoColorize)
... methods from color_eyre::owo_colors::OwoColorize trait
[entry 79] yellow() (use color_eyre::owo_colors::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
local cmp_rs = require("cmp_lsp_rs")
local comparators = cmp_rs.comparators
opts.sorting.comparators = {
comparators.inherent_import_inscope,
comparators.sort_by_label_but_underscore_last,
}
Sorting Behaviors:
- kind order: Field -> Method -> rest
- alphabetic sort on item names separately in the same kind
- method order
- inherent methods
- alphabetic sort on method names
- trait methods
- in-scope methods
- alphabetic sort on trait names in trait methods
- alphabetic sort on method names in the same trait
- import methods
- alphabetic sort on trait names in trait methods
- alphabetic sort on method names in the same trait
- in-scope methods
- inherent methods
[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 color_eyre::owo_colors::OwoColorize)
... (use color_eyre::owo_colors::OwoColorize)
[entry 63] yellow() (use color_eyre::owo_colors::OwoColorize)
[entry 64] type_id() (use std::any::Any)
[entry 65] borrow() (use std::borrow::Borrow)
[entry 66] borrow_mut() (use std::borrow::BorrowMut)
...
See the configuration example in usage above.
You can bind the function to a key to switch between defined combinations.
The default is like
{
["inherent_import_inscope + sort_by_label_but_underscore_last"] = {
M.comparators.inherent_import_inscope,
M.comparators.sort_by_label_but_underscore_last
},
["inscope_inherent_import + sort_by_label_but_underscore_last"] = {
M.comparators.inscope_inherent_import,
M.comparators.sort_by_label_but_underscore_last
},
}
You can call require("cmp_lsp_rs").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.
The format shouldn't be relied on. e.g.
[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)
filter_text: type_id
kind: Method
data: {
full_import_path = "std::any::Any",
imported_name = "Any"
}
[entry 84] arc (use std::sync::Arc)
filter_text: arc
kind: Snippet
data: {
full_import_path = "std::sync::Arc",
imported_name = "Arc"
}
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.
:lua rs = require'cmp_lsp_rs'
: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.
:lua rs = require'cmp_lsp_rs'
:lua rs.kind.get() -- query
:lua rs.kind.set(...) -- set kind ordering with most priorities
The argument ...
for set
can be one of these
string
name for kindstring[]
names for kindinteger
integer for kindinteger[]
integers for kindfunction(k) -> integer[]
where k is of kind type and you can easily write the kinds likek.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
If you want to override comparators nvim-cmp calls when experimenting them, you can run these commands.
:lua rs = require'cmp_lsp_rs'
:lua cmp = require'cmp'
:lua cmp.get_config().sorting.comparators = { rs.comparators.sort_by_label }