@@ -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
95169export 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