Skip to content

Commit 9be8852

Browse files
committed
Allow completion sections to have a dynamic rank
FEATURE: Completion sections may now set their rank to `dynamic` to indicate their order should be determined by the matching score of their best-matching option. See https://discuss.codemirror.net/t/autocompletion-rank-sections-based-on-filter-matches/9502
1 parent 0e49b08 commit 9be8852

File tree

2 files changed

+18
-4
lines changed

2 files changed

+18
-4
lines changed

src/completion.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ export interface CompletionSection {
7676
/// By default, sections are ordered alphabetically by name. To
7777
/// specify an explicit order, `rank` can be used. Sections with a
7878
/// lower rank will be shown above sections with a higher rank.
79-
rank?: number
79+
///
80+
/// When set to `"dynamic"`, the section's position compared to
81+
/// other dynamic sections depends on the matching score of the
82+
/// best-matching option in the sections.
83+
rank?: number | "dynamic"
8084
}
8185

8286
/// An instance of this is passed to completion source functions.

src/state.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function score(option: Completion) {
1717

1818
function sortOptions(active: readonly ActiveSource[], state: EditorState) {
1919
let options: Option[] = []
20-
let sections: null | CompletionSection[] = null
20+
let sections: null | CompletionSection[] = null, dynamicSectionScore: Record<string, number> | null = null
2121
let addOption = (option: Option) => {
2222
options.push(option)
2323
let {section} = option.completion
@@ -40,14 +40,24 @@ function sortOptions(active: readonly ActiveSource[], state: EditorState) {
4040
let matcher = conf.filterStrict ? new StrictMatcher(pattern) : new FuzzyMatcher(pattern)
4141
for (let option of a.result.options) if (match = matcher.match(option.label)) {
4242
let matched = !option.displayLabel ? match.matched : getMatch ? getMatch(option, match.matched) : []
43-
addOption(new Option(option, a.source, matched, match.score + (option.boost || 0)))
43+
let score = match.score + (option.boost || 0)
44+
addOption(new Option(option, a.source, matched, score))
45+
if (typeof option.section == "object" && option.section.rank === "dynamic") {
46+
let {name} = option.section
47+
if (!dynamicSectionScore) dynamicSectionScore = Object.create(null) as Record<string, number>
48+
dynamicSectionScore[name] = Math.max(score, dynamicSectionScore[name] || -1e9)
49+
}
4450
}
4551
}
4652
}
4753

4854
if (sections) {
4955
let sectionOrder: {[name: string]: number} = Object.create(null), pos = 0
50-
let cmp = (a: CompletionSection, b: CompletionSection) => (a.rank ?? 1e9) - (b.rank ?? 1e9) || (a.name < b.name ? -1 : 1)
56+
let cmp = (a: CompletionSection, b: CompletionSection) => {
57+
return (a.rank === "dynamic" && b.rank === "dynamic" ? dynamicSectionScore![b.name] - dynamicSectionScore![a.name] : 0) ||
58+
(typeof a.rank == "number" ? a.rank : 1e9) - (typeof b.rank == "number" ? b.rank : 1e9) ||
59+
(a.name < b.name ? -1 : 1)
60+
}
5161
for (let s of (sections as CompletionSection[]).sort(cmp)) {
5262
pos -= 1e5
5363
sectionOrder[s.name] = pos

0 commit comments

Comments
 (0)