Skip to content

Commit 902afcf

Browse files
committed
feat: expand autocomplete result count
1 parent 90c16e6 commit 902afcf

1 file changed

Lines changed: 97 additions & 24 deletions

File tree

src/hooks/useComposerAutocomplete.ts

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,39 +64,113 @@ function resolveAutocompleteState(
6464
};
6565
}
6666

67-
function filterItems(items: AutocompleteItem[], query: string) {
68-
const normalized = query.trim().toLowerCase();
69-
if (!normalized) {
70-
return items.slice();
67+
function isFileLabel(label: string) {
68+
return label.includes("/") || label.includes("\\");
69+
}
70+
71+
function basename(label: string) {
72+
const normalized = label.replace(/\\/g, "/");
73+
const parts = normalized.split("/").filter(Boolean);
74+
return parts.length ? parts[parts.length - 1] : label;
75+
}
76+
77+
function fileParts(label: string) {
78+
const normalized = label.replace(/\\/g, "/").toLowerCase();
79+
const base = basename(normalized);
80+
const dotIndex = base.lastIndexOf(".");
81+
const name =
82+
dotIndex > 0 && dotIndex < base.length - 1 ? base.slice(0, dotIndex) : base;
83+
const ext =
84+
dotIndex > 0 && dotIndex < base.length - 1 ? base.slice(dotIndex + 1) : "";
85+
return { normalized, base, name, ext };
86+
}
87+
88+
function isSubsequence(query: string, target: string) {
89+
let q = 0;
90+
let t = 0;
91+
while (q < query.length && t < target.length) {
92+
if (query[q] === target[t]) {
93+
q += 1;
94+
}
95+
t += 1;
7196
}
72-
return items.filter((item) => {
73-
const label = item.label.toLowerCase();
74-
return label.includes(normalized);
75-
});
97+
return q === query.length;
7698
}
7799

78-
function sortItems(items: AutocompleteItem[], query: string) {
100+
function scoreMatch(query: string, label: string) {
101+
if (!query) {
102+
return 0;
103+
}
104+
const normalizedQuery = query.toLowerCase();
105+
const { normalized, base, name, ext } = fileParts(label);
106+
const queryParts = normalizedQuery.split(".");
107+
const queryName = queryParts[0] ?? "";
108+
const queryExt = queryParts.length > 1 ? queryParts.slice(1).join(".") : "";
109+
const matchExt =
110+
!queryExt || ext.startsWith(queryExt) || ext.includes(queryExt);
111+
if (!matchExt) {
112+
return 0;
113+
}
114+
115+
if (!queryName) {
116+
if (queryExt && ext === queryExt) {
117+
return 60;
118+
}
119+
if (queryExt) {
120+
return 40;
121+
}
122+
return 0;
123+
}
124+
125+
if (normalized === normalizedQuery || name === queryName) {
126+
return 110;
127+
}
128+
if (name.startsWith(queryName)) {
129+
return 95 + (queryExt ? 10 : 0);
130+
}
131+
if (base.startsWith(queryName)) {
132+
return 90 + (queryExt ? 10 : 0);
133+
}
134+
if (normalized.startsWith(queryName)) {
135+
return 80 + (queryExt ? 5 : 0);
136+
}
137+
if (name.includes(queryName)) {
138+
return 70 + (queryExt ? 5 : 0);
139+
}
140+
if (normalized.includes(queryName)) {
141+
return 60 + (queryExt ? 5 : 0);
142+
}
143+
if (isSubsequence(queryName, name)) {
144+
return 50 + (queryExt ? 5 : 0);
145+
}
146+
return 0;
147+
}
148+
149+
function rankItems(items: AutocompleteItem[], query: string) {
79150
const normalized = query.trim().toLowerCase();
80151
if (!normalized) {
81-
return items;
152+
return items.slice();
82153
}
83-
return items.slice().sort((a, b) => {
84-
const aLabel = a.label.toLowerCase();
85-
const bLabel = b.label.toLowerCase();
86-
const aStarts = aLabel.startsWith(normalized);
87-
const bStarts = bLabel.startsWith(normalized);
88-
if (aStarts !== bStarts) {
89-
return aStarts ? -1 : 1;
90-
}
91-
return aLabel.localeCompare(bLabel);
92-
});
154+
const ranked = items
155+
.map((item) => ({
156+
item,
157+
score: scoreMatch(normalized, item.label),
158+
}))
159+
.filter((entry) => entry.score > 0)
160+
.sort((a, b) => {
161+
if (a.score !== b.score) {
162+
return b.score - a.score;
163+
}
164+
return a.item.label.localeCompare(b.item.label);
165+
});
166+
return ranked.map((entry) => entry.item);
93167
}
94168

95169
export function useComposerAutocomplete({
96170
text,
97171
selectionStart,
98172
triggers,
99-
maxResults = 8,
173+
maxResults = 50,
100174
}: UseComposerAutocompleteArgs) {
101175
const [highlightIndex, setHighlightIndex] = useState(0);
102176
const [dismissed, setDismissed] = useState(false);
@@ -116,9 +190,8 @@ export function useComposerAutocomplete({
116190
if (!source) {
117191
return [];
118192
}
119-
const filtered = filterItems(source.items, state.query);
120-
const sorted = sortItems(filtered, state.query);
121-
return sorted.slice(0, Math.max(0, maxResults));
193+
const ranked = rankItems(source.items, state.query);
194+
return ranked.slice(0, Math.max(0, maxResults));
122195
}, [state.active, state.query, state.trigger, triggers, maxResults]);
123196

124197
useEffect(() => {

0 commit comments

Comments
 (0)