@@ -4,24 +4,108 @@ import {LatexCompletionItemProvider} from './completion'
44
55const RE_LATEX_NAME = / ( \\ \S + ) / g;
66
7- let latexItems : vscode . QuickPickItem [ ] = [ ] ;
8- let pickOptions : vscode . QuickPickOptions = {
9- matchOnDescription : true ,
10- } ;
7+ function hammingDistance ( query : string , latexName : string ) : number {
8+ // Count the number of case-toggling "edits" needed to equate:
9+ // - the query string
10+ // - the substring of the LaTeX name beginning at `indexOfFirstCaseInsensitiveMatch`
11+ //
12+ // Note that the LaTeX name includes its leading "\",
13+ // which might be convenient for users who habitually type "\" directly into the picker as part of the query string,
14+ // especially if the user has not bound the `insertMathSymbol` command to a keybinding that happens to include a backslash.
15+ // (A backslash case-folds to itself, so it contributes neither a mismatch nor a phantom match either way, FWIW.)
16+ const indexOfFirstCaseInsensitiveMatch = latexName . toLowerCase ( ) . indexOf ( query . toLowerCase ( ) ) ;
17+ if ( indexOfFirstCaseInsensitiveMatch < 0 ) {
18+ return Infinity ;
19+ }
20+
21+ let distance = 0 ;
22+ for ( let i = 0 ; i < query . length ; i ++ ) {
23+ if ( query [ i ] !== latexName [ indexOfFirstCaseInsensitiveMatch + i ] ) {
24+ distance ++ ;
25+ }
26+ }
27+ return distance ;
28+ }
1129
1230export function activate ( context : vscode . ExtensionContext ) {
1331
14- latexItems = [ ] ;
15- for ( let name in latexSymbols ) {
16- latexItems . push ( {
17- description : name ,
18- label : latexSymbols [ name ] ,
32+ const insertion = vscode . commands . registerCommand ( 'unicode-latex.insertMathSymbol' , function showQuickPickForLatexSymbols ( ) {
33+ /*
34+ * Similar to `vscode.window.showQuickPick`,
35+ * but in addition to filtering items (based on case-_insensitive_ substring search),
36+ * this can also dynamically sort the results based on how closely they case-_sensitively_ match the search query
37+ * rather than naively preserving the order in which items were provided when first opening the picker.
38+ * (Compared to LaTeX symbol names, other VS Code functionality that uses the picker
39+ * presumably doesn't have as strong a need for case-sensitivity.)
40+ * Note that the various case-independent equivalence classes, themselves,
41+ * are still arranged in the order in which their first entries appeared in `latex.ts`.
42+ */
43+
44+ const quickPick = vscode . window . createQuickPick ( ) ;
45+ quickPick . matchOnDescription = true ;
46+
47+ // Pre-populate the picker with (the first) symbols visible, even before the user starts typing a query,
48+ // thus matching the behavior of VS Code's file opener and command palette
49+ // (rather than starting with an empty dropdown menu).
50+ quickPick . items = Object . entries ( latexSymbols ) . map ( ( [ description , label ] ) => ( { description, label } ) ) ;
51+
52+ quickPick . onDidChangeValue ( ( query : string ) => {
53+ const canonicalQuery = query . toLowerCase ( ) ;
54+
55+ // Contiguous, case-insensitive substring search is what QuickPick itself uses for filtering the provided items that it's been given
56+ // (see https://github.com/microsoft/vscode/blob/0ebc49192dd9e2004396003d7590cd6340daec04/src/vs/base/common/filters.ts#L67-L75).
57+ // Here, we pre-filter by the same criterion while giving ourselves the opportunity to dynamically _reorder_
58+ // those filtered results based on the casing in the user's query, before handing off the items to `QuickPick`.
59+ const latexSymbolsByCanonicalForm = new Map < string , string [ ] > ( ) ;
60+ for ( const latexName in latexSymbols ) {
61+ const canonicalName = latexName . toLowerCase ( ) ;
62+ if ( ! canonicalName . includes ( canonicalQuery ) ) {
63+ continue ;
64+ }
65+
66+ let matchingLatexSymbols = latexSymbolsByCanonicalForm . get ( canonicalName ) ;
67+ if ( ! matchingLatexSymbols ) {
68+ matchingLatexSymbols = [ ] ;
69+ latexSymbolsByCanonicalForm . set ( canonicalName , matchingLatexSymbols ) ;
70+ }
71+ matchingLatexSymbols . push ( latexName ) ;
72+ }
73+
74+ const sortedLatexItems : vscode . QuickPickItem [ ] = [ ] ;
75+ // Walk between equivalence classes in the order in which their first entries appeared in `latex.ts`,
76+ // deliberately preserving (at least some of) that source ordering in the displayed list.
77+ // This relies on `Map.prototype.values()` iterating in insertion order (per ES spec)
78+ // as well as `for...in` iterating over a string-keyed object literal in insertion order (also per ES spec).
79+ for ( const matchingLatexSymbols of latexSymbolsByCanonicalForm . values ( ) ) {
80+ // Decorate–sort–undecorate (computing each member's `hammingDistance` exactly once)
81+ // allows us to avoid the O(n log n) re-evaluations that a bare
82+ // ```
83+ // (a, b) => hammingDistance(a, query) - hammingDistance(b, query)
84+ // ```
85+ // would otherwise incur.
86+ const sortedLatexSymbols = matchingLatexSymbols
87+ . map ( ( latexName ) => ( { latexName, distance : hammingDistance ( query , latexName ) } ) )
88+ . sort ( ( a , b ) => a . distance - b . distance ) ;
89+ for ( const { latexName } of sortedLatexSymbols ) {
90+ sortedLatexItems . push ( { description : latexName , label : latexSymbols [ latexName ] } ) ;
91+ }
92+ }
93+ quickPick . items = sortedLatexItems ;
94+ } ) ;
95+
96+ quickPick . onDidAccept ( ( ) => {
97+ const item = quickPick . selectedItems [ 0 ] ;
98+ quickPick . hide ( ) ;
99+ insertSymbol ( item ) ;
100+ } ) ;
101+
102+ quickPick . onDidHide ( ( ) => {
103+ quickPick . dispose ( ) ;
19104 } ) ;
20- }
21105
22- let insertion = vscode . commands . registerCommand ( 'unicode-latex.insertMathSymbol' , ( ) => {
23- vscode . window . showQuickPick ( latexItems , pickOptions ) . then ( insertSymbol ) ;
106+ quickPick . show ( ) ;
24107 } ) ;
108+
25109 let replacement = vscode . commands . registerCommand ( 'unicode-latex.replaceLatexNames' , ( ) => {
26110 replaceWithUnicode ( vscode . window . activeTextEditor ) ;
27111 } ) ;
0 commit comments