@@ -102,135 +102,160 @@ export function completionBehavior(behaviorContext: BehaviorContext): Behavior {
102102  } ; 
103103} 
104104
105- async  function  getCompletions ( 
106-   context : CompletionContext , 
107-   cvContext : CodeViewCompletionContext , 
108-   behaviorContext : BehaviorContext 
109- ) : Promise < CompletionResult  |  null >  { 
110- 
111-   // get completions 
112-   const  completions  =  await  behaviorContext . pmContext . ui . codeview ?. codeViewCompletions ( cvContext ) ; 
113-   if  ( context . aborted  ||  ! completions  ||  completions . items . length  ==  0 )  { 
114-     return  null ; 
105+ const  compareBySortText  =  ( a : CompletionItem ,  b : CompletionItem )  =>  { 
106+   if  ( a . sortText  &&  b . sortText )  { 
107+     return  a . sortText . localeCompare ( b . sortText ) ; 
108+   }  else  { 
109+     return  0 ; 
115110  } 
111+ } ; 
116112
117-   // order completions 
118-   const  haveOrder  =  ! ! completions . items ?. [ 0 ] . sortText ; 
119-   if  ( haveOrder )  { 
120-     completions . items  =  completions . items . sort ( ( a ,  b )  =>  { 
121-       if  ( a . sortText  &&  b . sortText )  { 
122-         return  a . sortText . localeCompare ( b . sortText ) ; 
123-       }  else  { 
124-         return  0 ; 
113+ // compute from 
114+ const  itemFrom  =  ( item : CompletionItem ,  contextPos : number )  =>  { 
115+   // compute from 
116+   return  item . textEdit 
117+     ? InsertReplaceEdit . is ( item . textEdit ) 
118+       ? contextPos  -  ( item . textEdit . insert . end . character  -  item . textEdit . insert . start . character ) 
119+       : TextEdit . is ( item . textEdit ) 
120+         ? contextPos  -  ( item . textEdit . range . end . character  -  item . textEdit . range . start . character ) 
121+         : contextPos 
122+     : contextPos ; 
123+ } ; 
124+ 
125+ /** 
126+  * replaceText for a given CompletionItem is the text that is already in the document 
127+  * that that CompletionItem will replace. 
128+  * 
129+  * Example 1: if you are typing `lib` and get the completion `library`, then this function 
130+  *   will give `lib`. 
131+  * Example 2: if you are typing `os.a` and get the completion `abc`, then this function 
132+  *   will give `a`. 
133+  */ 
134+ const  getReplaceText  =  ( context : CompletionContext ,  item : CompletionItem )  => 
135+   context . state . sliceDoc ( itemFrom ( item ,  context . pos ) ,  context . pos ) ; 
136+ 
137+ const  makeCompletionItemApplier  =  ( item : CompletionItem ,  context : CompletionContext )  => 
138+   ( view : EditorView ,  completion : Completion )  =>  { 
139+     // compute from 
140+     const  from  =  itemFrom ( item ,  context . pos ) ; 
141+ 
142+     // handle snippets 
143+     const  insertText  =  item . textEdit ?. newText  ??  ( item . insertText  ||  item . label ) ; 
144+     if  ( item . insertTextFormat  ===  InsertTextFormat . Snippet )  { 
145+       const  insertSnippet  =  snippet ( insertText . replace ( / \$ ( \d + ) / g,  "$${$1}" ) ) ; 
146+       insertSnippet ( view ,  completion ,  from ,  context . pos ) ; 
147+       // normal completions 
148+     }  else  { 
149+       view . dispatch ( { 
150+         ...insertCompletionText ( view . state ,  insertText ,  from ,  context . pos ) , 
151+         annotations : pickedCompletion . of ( completion ) 
152+       } ) ; 
153+       if  ( item . command ?. command  ===  "editor.action.triggerSuggest" )  { 
154+         startCompletion ( view ) ; 
125155      } 
126-     } ) ; 
156+     } 
157+   } ; 
158+ 
159+ const  sortTextItemsBoostScore  =  ( context : CompletionContext ,  items : CompletionItem [ ] ,  index : number )  =>  { 
160+   const  total  =  items . length ; 
161+   const  item  =  items [ index ] ; 
162+   // compute replaceText 
163+   const  replaceText  =  getReplaceText ( context ,  item ) ; 
164+ 
165+   // if the replaceText doesn't start with "." then bury items that do 
166+   if  ( ! replaceText . startsWith ( "." )  &&  item . label . startsWith ( "." ) )  { 
167+     return  - 99 ; 
127168  } 
128169
129-   // compute token 
130-   const  token  =  context . matchBefore ( / \S + / ) ?. text ; 
170+   // only boost things that have a prefix match 
171+   if  ( item . label . toLowerCase ( ) . startsWith ( replaceText )  || 
172+     ( item . textEdit  &&  item . textEdit . newText . toLowerCase ( ) . startsWith ( replaceText ) )  || 
173+     ( item . insertText  &&  item . insertText . toLowerCase ( ) . startsWith ( replaceText ) ) )  { 
174+     return  - 99  +  Math . round ( ( ( total  -  index )  /  total )  *  198 ) ; ; 
175+   }  else  { 
176+     return  - 99 ; 
177+   } 
178+ } ; 
131179
132-   // compute from 
133-   const  itemFrom  =  ( item : CompletionItem )  =>  { 
134-     // compute from 
135-     return  item . textEdit 
136-       ? InsertReplaceEdit . is ( item . textEdit ) 
137-         ? context . pos  -  ( item . textEdit . insert . end . character  -  item . textEdit . insert . start . character ) 
138-         : TextEdit . is ( item . textEdit ) 
139-           ? context . pos  -  ( item . textEdit . range . end . character  -  item . textEdit . range . start . character ) 
140-           : context . pos 
141-       : context . pos ; 
142-   } ; 
180+ const  defaultBoostScore  =  ( context : CompletionContext ,  items : CompletionItem [ ] ,  index : number )  =>  { 
181+   const  item  =  items [ index ] ; 
143182
144-   // use order to create boost 
145-   const  total  =  completions . items . length ; 
146-   const  boostScore  =  ( index : number )  =>  { 
183+   const  replaceText  =  getReplaceText ( context ,  item ) ; 
147184
148-      // compute replaceText  
149-      const   item   =   completions . items [ index ] ; 
150-      const   replaceText   =   context . state . sliceDoc ( itemFrom ( item ) ,   context . pos ) . toLowerCase ( ) ; 
185+   // if you haven't typed into the completions yet (for example after a `.`) then  
186+   // score items starting with non-alphabetic characters -1, everything else 0. 
187+   if   ( replaceText . length   ===   0 )   return   isLetter ( item . label [ 0 ] )  ?  0  :  - 1 ; 
151188
152-     if  ( haveOrder )  { 
189+   // We filter items by replaceText inclusion before scoring, 
190+   // so i is garaunteed to be an index into `item.label`... 
191+   const  i  =  item . label . toLowerCase ( ) . indexOf ( replaceText . toLowerCase ( ) ) ; 
192+   // and `replaceTextInItermLabel` should be the same as `replaceText` up to upper/lowercase 
193+   // differences. 
194+   const  replaceTextInItemLabel  =  item . label . slice ( i ,  replaceText . length ) ; 
153195
154-       // if the replaceText doesn't start with "." then bury items that do 
155-       if  ( ! replaceText . startsWith ( "." )  &&  item . label . startsWith ( "." ) )  { 
156-         return  - 99 ; 
157-       } 
196+   // mostly counts how many upper/lowercase differences there are 
197+   let  diff  =  simpleStringDiff ( replaceTextInItemLabel ,  replaceText ) ; 
158198
159-       // only boost things that have a prefix match 
160-       if  ( item . label . toLowerCase ( ) . startsWith ( replaceText )  || 
161-         ( item . textEdit  &&  item . textEdit . newText . toLowerCase ( ) . startsWith ( replaceText ) )  || 
162-         ( item . insertText  &&  item . insertText . toLowerCase ( ) . startsWith ( replaceText ) ) )  { 
163-         return  - 99  +  Math . round ( ( ( total  -  index )  /  total )  *  198 ) ; ; 
164-       }  else  { 
165-         return  - 99 ; 
166-       } 
199+   // `-i` scores completions better if what you typed is earlier in the completion 
200+   // `-diff/10` mostly tie breaks that score by capitalization differences. 
201+   return  - i  -  diff  /  10 ;  // 10 is a magic number 
202+ } ; 
167203
168-     }  else  { 
169-       return  undefined ; 
170-     } 
171-   } ; 
204+ async  function  getCompletions ( 
205+   context : CompletionContext , 
206+   cvContext : CodeViewCompletionContext , 
207+   behaviorContext : BehaviorContext 
208+ ) : Promise < CompletionResult  |  null >  { 
209+   if  ( context . aborted )  return  null ; 
172210
173-   // return completions 
174-   return  { 
175-     from : context . pos , 
211+   // get completions 
212+   const  completions  =  await  behaviorContext . pmContext . ui . codeview ?. codeViewCompletions ( cvContext ) ; 
213+   if  ( completions  ===  undefined )  return  null ; 
214+   if  ( completions . items . length  ==  0 )  return  null ; 
176215
177-     options : completions . items 
178-       . filter ( item  =>  { 
216+   const  itemsHaveSortText  =  completions . items ?. [ 0 ] . sortText  !==  undefined ; 
179217
180-         // no text completions that aren't snippets 
181-         if  ( item . kind  ===  CompletionItemKind . Text  && 
182-           item . insertTextFormat  !==  InsertTextFormat . Snippet )  { 
183-           return  false ; 
184-         } 
218+   const  items  =  itemsHaveSortText  ?
219+     completions . items . sort ( compareBySortText )  :
220+     completions . items ; 
185221
186-         // compute text to replace 
187-         const  replaceText  =  context . state . sliceDoc ( itemFrom ( item ) ,  context . pos ) . toLowerCase ( ) ; 
222+   // The token is the contents of the line up to your cursor. 
223+   // For example, if you type `os.a` then token will be `os.a`. 
224+   // Note: in contrast, when you type `os.a` replaceText will give `a` for a completion like `abc`. 
225+   const  token  =  context . matchBefore ( / \S + / ) ?. text ; 
188226
189-         // only allow non-text edits if we have no token 
190-         if  ( ! item . textEdit  &&  token )  { 
191-           return  false ; 
192-         } 
227+   const  filteredItems  =  items . filter ( item  =>  { 
228+     // no text completions that aren't snippets 
229+     if  ( item . kind  ===  CompletionItemKind . Text  && 
230+       item . insertTextFormat  !==  InsertTextFormat . Snippet )  return  false ; 
231+ 
232+     // only allow non-text edits if we have no token 
233+     if  ( ! item . textEdit  &&  token )  return  false ; 
234+ 
235+     // require at least inclusion 
236+     const  replaceText  =  getReplaceText ( context ,  item ) . toLowerCase ( ) ; 
237+     return  item . label . toLowerCase ( ) . includes ( replaceText )  || 
238+       item . insertText ?. toLowerCase ( ) . includes ( replaceText ) ; 
239+   } ) ; 
240+ 
241+   const  boostScore  =  itemsHaveSortText  ?
242+     sortTextItemsBoostScore  :
243+     defaultBoostScore ; 
244+ 
245+   const  options  =  filteredItems 
246+     . map ( ( item ,  index ) : Completion  =>  { 
247+       return  { 
248+         label : item . label , 
249+         detail : ! item . documentation  ? item . detail  : undefined , 
250+         type : vsKindToType ( item . kind ) , 
251+         info : ( )  =>  infoNodeForItem ( item ) , 
252+         apply : makeCompletionItemApplier ( item ,  context ) , 
253+         boost : boostScore ( context ,  filteredItems ,  index ) 
254+       } ; 
255+     } ) ; 
193256
194-         // require at least inclusion 
195-         return  item . label . toLowerCase ( ) . includes ( replaceText )  || 
196-           ( item . insertText  &&  item . insertText . toLowerCase ( ) . includes ( replaceText ) ) ; 
197-       } ) 
198-       . map ( ( item ,  index ) : Completion  =>  { 
199-         return  { 
200-           label : item . label , 
201-           detail : item . detail  &&  ! item . documentation  ? item . detail  : undefined , 
202-           type : vsKindToType ( item . kind ) , 
203-           info : ( ) : Node  |  null  =>  { 
204-             if  ( item . documentation )  { 
205-               return  infoNodeForItem ( item ) ; 
206-             }  else  { 
207-               return  null ; 
208-             } 
209-           } , 
210-           apply : ( view : EditorView ,  completion : Completion ,  from : number )  =>  { 
211-             // compute from 
212-             from  =  itemFrom ( item ) ; 
213- 
214-             // handle snippets 
215-             const  insertText  =  item . textEdit ?. newText  ??  ( item . insertText  ||  item . label ) ; 
216-             if  ( item . insertTextFormat  ===  InsertTextFormat . Snippet )  { 
217-               const  insertSnippet  =  snippet ( insertText . replace ( / \$ ( \d + ) / g,  "$${$1}" ) ) ; 
218-               insertSnippet ( view ,  completion ,  from ,  context . pos ) ; 
219-               // normal completions 
220-             }  else  { 
221-               view . dispatch ( { 
222-                 ...insertCompletionText ( view . state ,  insertText ,  from ,  context . pos ) , 
223-                 annotations : pickedCompletion . of ( completion ) 
224-               } ) ; 
225-               if  ( item . command ?. command  ===  "editor.action.triggerSuggest" )  { 
226-                 startCompletion ( view ) ; 
227-               } 
228-             } 
229-           } , 
230-           boost : boostScore ( index ) 
231-         } ; 
232-       } ) 
233-   } ; 
257+   // return completions 
258+   return  {  from : context . pos ,  options } ; 
234259} 
235260
236261
@@ -281,7 +306,6 @@ function vsKindToType(kind?: CompletionItemKind) {
281306
282307
283308function  infoNodeForItem ( item : CompletionItem )  { 
284- 
285309  const  headerEl  =  ( text : string ,  tag : string )  =>  { 
286310    const  header  =  document . createElement ( tag ) ; 
287311    header . classList . add ( "cm-completionInfoHeader" ) ; 
@@ -328,3 +352,15 @@ function infoNodeForItem(item: CompletionItem) {
328352    return  null ; 
329353  } 
330354} 
355+ 
356+ function  simpleStringDiff ( str1 : string ,  str2 : string )  { 
357+   let  diff  =  0 ; 
358+   for  ( let  i  =  0 ;  i  <  Math . min ( str1 . length ,  str2 . length ) ;  i ++ )  { 
359+     if  ( str1 [ i ]  !==  str2 [ i ] )  diff ++ ; 
360+   } 
361+   return  diff ; 
362+ } ; 
363+ 
364+ function  isLetter ( c : string )  { 
365+   return  c . toLowerCase ( )  !=  c . toUpperCase ( ) ; 
366+ } 
0 commit comments