|
| 1 | +export function hammingDistance(query: string, latexName: string): number { |
| 2 | + // Count the number of case-toggling "edits" needed to equate: |
| 3 | + // - the query string |
| 4 | + // - the substring of the LaTeX name beginning at `indexOfFirstCaseInsensitiveMatch` |
| 5 | + // |
| 6 | + // Note that the LaTeX name includes its leading "\", |
| 7 | + // which might be convenient for users who habitually type "\" directly into the picker as part of the query string, |
| 8 | + // especially if the user has not bound the `insertMathSymbol` command to a keybinding that happens to include a backslash. |
| 9 | + // (A backslash case-folds to itself, so it contributes neither a mismatch nor a phantom match either way, FWIW.) |
| 10 | + const indexOfFirstCaseInsensitiveMatch = latexName.toLowerCase().indexOf(query.toLowerCase()); |
| 11 | + if (indexOfFirstCaseInsensitiveMatch < 0) { |
| 12 | + return Infinity; |
| 13 | + } |
| 14 | + |
| 15 | + let distance = 0; |
| 16 | + for (let i = 0; i < query.length; i++) { |
| 17 | + if (query[i] !== latexName[indexOfFirstCaseInsensitiveMatch + i]) { |
| 18 | + distance++; |
| 19 | + } |
| 20 | + } |
| 21 | + return distance; |
| 22 | +} |
| 23 | + |
| 24 | +export function sortedItemsForQuery( |
| 25 | + query: string, |
| 26 | + symbols: Record<string, string> |
| 27 | +): Array<{ description: string; label: string }> { |
| 28 | + const canonicalQuery = query.toLowerCase(); |
| 29 | + |
| 30 | + // Contiguous, case-insensitive substring search is what QuickPick itself uses for filtering |
| 31 | + // (see https://github.com/microsoft/vscode/blob/0ebc49192dd9e2004396003d7590cd6340daec04/src/vs/base/common/filters.ts#L67-L75). |
| 32 | + // Pre-filtering by the same criterion lets us dynamically _reorder_ those results based on |
| 33 | + // the casing of the user's query before handing the list back to QuickPick. |
| 34 | + const latexSymbolsByCanonicalForm = new Map<string, string[]>(); |
| 35 | + for (const latexName in symbols) { |
| 36 | + const canonicalName = latexName.toLowerCase(); |
| 37 | + if (!canonicalName.includes(canonicalQuery)) { |
| 38 | + continue; |
| 39 | + } |
| 40 | + |
| 41 | + let matchingLatexSymbols = latexSymbolsByCanonicalForm.get(canonicalName); |
| 42 | + if (!matchingLatexSymbols) { |
| 43 | + matchingLatexSymbols = []; |
| 44 | + latexSymbolsByCanonicalForm.set(canonicalName, matchingLatexSymbols); |
| 45 | + } |
| 46 | + matchingLatexSymbols.push(latexName); |
| 47 | + } |
| 48 | + |
| 49 | + const sortedItems: Array<{ description: string; label: string }> = []; |
| 50 | + // Walk between case-independent equivalence classes in the order in which their first entries appeared in `symbols`, |
| 51 | + // deliberately preserving (at least some of) that source ordering in the displayed list. |
| 52 | + // This relies on `Map.prototype.values()` iterating in insertion order, per EcmaScript spec, |
| 53 | + // and also relies on `for...in` iterating over a string-keyed object literal in insertion order, also per EcmaScript spec. |
| 54 | + for (const matchingLatexSymbols of latexSymbolsByCanonicalForm.values()) { |
| 55 | + // Decorate–sort–undecorate (computing each member's `hammingDistance` exactly once) |
| 56 | + // allows us to avoid the O(n log n) re-evaluations that a bare |
| 57 | + // ``` |
| 58 | + // (a, b) => hammingDistance(a, query) - hammingDistance(b, query) |
| 59 | + // ``` |
| 60 | + // would otherwise incur. |
| 61 | + const sortedLatexSymbols = matchingLatexSymbols |
| 62 | + .map((latexName) => ({ latexName, distance: hammingDistance(query, latexName) })) |
| 63 | + .sort((a, b) => a.distance - b.distance); |
| 64 | + for (const { latexName } of sortedLatexSymbols) { |
| 65 | + sortedItems.push({ description: latexName, label: symbols[latexName] }); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + return sortedItems; |
| 70 | +} |
0 commit comments