Skip to content

Commit 7b93f2f

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 7b93f2f

3 files changed

Lines changed: 110 additions & 16 deletions

File tree

src/extension.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,49 @@
11
import * as vscode from 'vscode';
22
import {latexSymbols} from './latex';
3-
import {LatexCompletionItemProvider} from './completion'
3+
import { LatexCompletionItemProvider } from './completion';
4+
import { sortedItemsForQuery } from './sort';
45

56
const RE_LATEX_NAME = /(\\\S+)/g;
67

7-
let latexItems: vscode.QuickPickItem[] = [];
8-
let pickOptions: vscode.QuickPickOptions = {
9-
matchOnDescription: true,
10-
};
11-
128
export function activate(context: vscode.ExtensionContext) {
139

14-
latexItems = [];
15-
for (let name in latexSymbols) {
16-
latexItems.push({
17-
description: name,
18-
label: latexSymbols[name],
10+
const insertion = vscode.commands.registerCommand('unicode-latex.insertMathSymbol', function showQuickPickForLatexSymbols() {
11+
/*
12+
* Similar to `vscode.window.showQuickPick`,
13+
* but in addition to filtering items (based on case-_insensitive_ substring search),
14+
* this also dynamically sorts the results based on how closely they case-_sensitively_ match the search query
15+
* rather than naively preserving the order in which items were provided when first opening the picker.
16+
* (Compared to LaTeX symbol names, other VS Code functionality that uses the picker
17+
* presumably doesn't have as strong a need for case-sensitivity.)
18+
* Note that the various case-independent equivalence classes, themselves,
19+
* are still arranged in the order in which their first entries appeared in `latex.ts`.
20+
*/
21+
22+
const quickPick = vscode.window.createQuickPick();
23+
quickPick.matchOnDescription = true;
24+
25+
// Pre-populate the picker with some symbols visible even before the user starts typing a query,
26+
// thus matching the behavior of VS Code's file opener and command palette
27+
// (rather than starting with an empty dropdown menu).
28+
quickPick.items = sortedItemsForQuery('', latexSymbols);
29+
30+
quickPick.onDidChangeValue((query: string) => {
31+
quickPick.items = sortedItemsForQuery(query, latexSymbols);
1932
});
20-
}
2133

22-
let insertion = vscode.commands.registerCommand('unicode-latex.insertMathSymbol', () => {
23-
vscode.window.showQuickPick(latexItems, pickOptions).then(insertSymbol);
34+
quickPick.onDidAccept(() => {
35+
const item = quickPick.selectedItems[0];
36+
quickPick.hide();
37+
insertSymbol(item);
38+
});
39+
40+
quickPick.onDidHide(() => {
41+
quickPick.dispose();
42+
});
43+
44+
quickPick.show();
2445
});
46+
2547
let replacement = vscode.commands.registerCommand('unicode-latex.replaceLatexNames', () => {
2648
replaceWithUnicode(vscode.window.activeTextEditor);
2749
});

src/sort.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}

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)