Skip to content

Commit 965a619

Browse files
committed
Sort LaTeX symbol search results based on the matching letters' cases
Also bring the EcmaScript version a bit more up to date, thereby allowing the use of `Object.entries`.
1 parent 678a7eb commit 965a619

2 files changed

Lines changed: 100 additions & 14 deletions

File tree

src/extension.ts

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,108 @@ import {LatexCompletionItemProvider} from './completion'
44

55
const 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

1230
export 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
});

tsconfig.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"compilerOptions": {
33
"module": "commonjs",
4-
"target": "es6",
4+
"target": "ES2022",
5+
"lib": [
6+
"ES2022"
7+
],
58
"outDir": "out",
6-
"lib": ["es6"],
79
"sourceMap": true,
810
"rootDir": "."
911
},

0 commit comments

Comments
 (0)