mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-30 02:21:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f480c2a1 | ||
|
|
17ee648f48 |
No files matched your search
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
||||
|
||||
import { getCompletions } from "./autocomplete/completionEngine";
|
||||
import { DEFAULT_AUTOCOMPLETE_SETTINGS } from "./autocomplete/useTerminalAutocomplete";
|
||||
import { getPinyinCacheStatsForTests, resetPinyinCacheForTests } from "../../lib/searchMatcher";
|
||||
import type { Snippet } from "../../domain/models";
|
||||
|
||||
const deploySnippet: Snippet = { id: "d", label: "deploy", command: "kubectl apply -f ." };
|
||||
@@ -83,3 +84,34 @@ test("getCompletions returns more than 8 snippet matches when default maxSuggest
|
||||
);
|
||||
assert.equal(snippetMatches.length, 20);
|
||||
});
|
||||
|
||||
|
||||
test("argument completion skips ASCII transliteration in a large snippet library", async () => {
|
||||
const snippets: Snippet[] = Array.from({ length: 5000 }, (_, index) => ({
|
||||
id: `snippet-${index}`,
|
||||
label: `deploy-service-${index}`,
|
||||
command: `kubectl --namespace production rollout restart deployment service-${index}`,
|
||||
}));
|
||||
snippets.push({ id: "mail", label: "邮箱", command: "user@example.com" });
|
||||
resetPinyinCacheForTests();
|
||||
try {
|
||||
for (const input of ["git pull zz", "git pull zzq", "git pull zzqx"]) {
|
||||
assert.deepEqual(await getCompletions(input, { snippets, skipPathCompletion: true }), []);
|
||||
}
|
||||
assert.deepEqual(
|
||||
getPinyinCacheStatsForTests().keys,
|
||||
["邮箱"],
|
||||
"ASCII command tokens cannot produce a new pinyin prefix and must not enter its cache",
|
||||
);
|
||||
const result = await getCompletions("git pull yx", { snippets, skipPathCompletion: true });
|
||||
assert.equal(result.find((suggestion) => suggestion.source === "snippet")?.snippet?.id, "mail");
|
||||
} finally {
|
||||
resetPinyinCacheForTests();
|
||||
}
|
||||
});
|
||||
|
||||
test("ASCII argument completion retains compact query prefixes", async () => {
|
||||
const snippets: Snippet[] = [{ id: "compact", label: "production-foobar", command: "echo ready" }];
|
||||
const result = await getCompletions("git pull foo_bar", { snippets, skipPathCompletion: true });
|
||||
assert.equal(result.find((suggestion) => suggestion.source === "snippet")?.snippet?.id, "compact");
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
# Terminal argument completion performance
|
||||
|
||||
Issue: #3715. This follows #3623's argument-position matching for #3622; it does not resolve #3622's remaining interactive-email UX request.
|
||||
|
||||
`matchesWordPrefixQuery` tried pinyin for every word after a failed literal prefix comparison. A large snippet library therefore repeated transliteration/cache work for ordinary ASCII command words on each completion query. Printable ASCII words cannot gain another pinyin prefix, so they now return a miss immediately after the literal check. Chinese and other non-ASCII words keep the existing path.
|
||||
|
||||
## Verification
|
||||
|
||||
The regression exercises the actual `getCompletions` argument path with 5,000 snippets. Before the fix, it fails because the pinyin cache contains 5,011 entries, including ASCII commands and numbers. Afterward, only the Chinese label is cached and its initials still find the snippet. Additional cases cover empty queries, case, fullwidth normalization, word boundaries, Chinese full pinyin/initials, and mixed ASCII/Chinese words.
|
||||
|
||||
Run the real Electron renderer measurement:
|
||||
|
||||
```sh
|
||||
node_modules/.bin/electron scripts/terminal-snippet-completion-performance.live.test.cjs
|
||||
```
|
||||
|
||||
This uses isolated application data and synthetic snippets, calls the production completion engine, measures completion time and a queued input timer, and checks that ASCII words bypass pinyin while Chinese initials still work. It prints timings; the regression gate is the unnecessary-work assertion, avoiding a machine-specific timing threshold.
|
||||
|
||||
On the audit machine, an initial fixture with Chinese labels and ASCII commands measured:
|
||||
|
||||
| Library | Before | After |
|
||||
| --- | --- | --- |
|
||||
| 100 snippets | 0.4–2.4 ms | 0.1–1.3 ms |
|
||||
| 1,000 snippets | 4.2–7.0 ms | 1.7–2.3 ms |
|
||||
| 5,000 snippets | 52.9–86.0 ms | 8.8–9.9 ms |
|
||||
|
||||
The corresponding 5,000-entry queued input delay fell from 53.1–86.1 ms to 8.8–9.9 ms. The committed fixture uses ASCII labels plus one Chinese label; the final three rounds measured 6.5–11.0 ms. These are local completion/render-thread measurements, not network or terminal throughput measurements.
|
||||
|
||||
An isolated real Electron window also mounted production `TerminalAutocomplete` with real xterm and 5,001 snippets. Typing `git pull yx` displayed the Chinese mailbox snippet; clicking it selected the correct snippet. No connected SSH server, personal snippets, or user settings were used.
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getHostSearchMatch,
|
||||
matchesHostSearchQuery,
|
||||
matchesSearchQuery,
|
||||
matchesWordPrefixQuery,
|
||||
matchesWorkspaceHostPickerQuery,
|
||||
resetPinyinCacheForTests,
|
||||
} from "../lib/searchMatcher.ts";
|
||||
@@ -274,3 +275,19 @@ test("pinyin cache stays hard-bounded by retained characters, not just entries",
|
||||
assert.ok(getPinyinCacheStatsForTests().size > 0);
|
||||
resetPinyinCacheForTests();
|
||||
});
|
||||
|
||||
|
||||
test("word prefixes retain literal, normalized, and non-ASCII matching", () => {
|
||||
assert.equal(matchesWordPrefixQuery("", "deploy"), true);
|
||||
assert.equal(matchesWordPrefixQuery("DEP", "deploy-service"), true);
|
||||
assert.equal(matchesWordPrefixQuery("ser", "deploy-service"), true);
|
||||
assert.equal(matchesWordPrefixQuery("123", "item-1234"), true);
|
||||
assert.equal(matchesWordPrefixQuery("dep", "DEPLOY"), true);
|
||||
assert.equal(matchesWordPrefixQuery("caf", "café"), true);
|
||||
assert.equal(matchesWordPrefixQuery("mail", "gmail"), false);
|
||||
assert.equal(matchesWordPrefixQuery("zz", "deploy-service"), false);
|
||||
assert.equal(matchesWordPrefixQuery("--de", "deploy-service"), false);
|
||||
assert.equal(matchesWordPrefixQuery("bushu", "部署服务"), true);
|
||||
assert.equal(matchesWordPrefixQuery("bsfw", "部署服务"), true);
|
||||
assert.equal(matchesWordPrefixQuery("apibu", "API部署"), true);
|
||||
});
|
||||
@@ -334,6 +334,9 @@ export function matchesWordPrefixQuery(
|
||||
return words.some((word) => {
|
||||
if (word.startsWith(normalizedQuery)) return true;
|
||||
if (!compactQuery) return false;
|
||||
// ASCII command words cannot gain a different pinyin prefix. Avoid the
|
||||
// transliteration/cache work for every token in large snippet libraries.
|
||||
if (/^[\x20-\x7E]+$/.test(word)) return word.startsWith(compactQuery);
|
||||
const { full, initials } = getPinyinVariants(word);
|
||||
return (
|
||||
(full !== "" && full.startsWith(compactQuery))
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
/* global process, __dirname, console */
|
||||
|
||||
if (!process.versions.electron) {
|
||||
require("node:test")("large snippet libraries keep argument completion responsive", {
|
||||
skip: "run with Electron to measure the actual renderer completion path",
|
||||
}, () => {});
|
||||
} else {
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const esbuild = require("esbuild");
|
||||
const temp = require("../electron/bridges/tempDirBridge.cjs");
|
||||
const userData = fs.mkdtempSync(`${temp.getTempFilePath("snippet-completion-perf")}-`);
|
||||
app.setPath("userData", userData);
|
||||
app.on("window-all-closed", () => {});
|
||||
let window;
|
||||
const cleanup = (code) => {
|
||||
window?.destroy();
|
||||
fs.rmSync(userData, { recursive: true, force: true });
|
||||
app.exit(code);
|
||||
};
|
||||
|
||||
void app.whenReady().then(async () => {
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const bundle = esbuild.buildSync({
|
||||
stdin: {
|
||||
contents: [
|
||||
'export { getCompletions } from "./components/terminal/autocomplete/completionEngine";',
|
||||
'export { resetPinyinCacheForTests, getPinyinCacheStatsForTests } from "./lib/searchMatcher";',
|
||||
].join("\n"),
|
||||
resolveDir: root,
|
||||
loader: "ts",
|
||||
},
|
||||
bundle: true,
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
write: false,
|
||||
define: { "import.meta.env.DEV": "false", "import.meta.env.PROD": "true", "import.meta": "{}" },
|
||||
}).outputFiles[0].text;
|
||||
window = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false, sandbox: false, backgroundThrottling: false },
|
||||
});
|
||||
await window.loadURL("data:text/html,<body>");
|
||||
const result = await window.webContents.executeJavaScript(`(async () => {
|
||||
const loaded = { exports: {} };
|
||||
((module, exports) => { ${bundle} })(loaded, loaded.exports);
|
||||
const { getCompletions, resetPinyinCacheForTests, getPinyinCacheStatsForTests } = loaded.exports;
|
||||
const snippets = Array.from({ length: 5000 }, (_, index) => ({
|
||||
id: String(index), label: 'deploy-service-' + index,
|
||||
command: 'kubectl --namespace production rollout restart deployment service-' + index,
|
||||
}));
|
||||
snippets.push({ id: 'mail', label: '邮箱', command: 'user@example.com' });
|
||||
await getCompletions('git pull zz', { snippets: [] });
|
||||
resetPinyinCacheForTests();
|
||||
const rows = [];
|
||||
for (let round = 0; round < 3; round++) {
|
||||
for (const input of ['git pull zz', 'git pull zzq', 'git pull zzqx']) {
|
||||
const started = performance.now();
|
||||
let resolveInput;
|
||||
const inputTurn = new Promise(resolve => { resolveInput = resolve; });
|
||||
setTimeout(() => resolveInput(performance.now() - started), 0);
|
||||
const suggestions = await getCompletions(input, { snippets, skipPathCompletion: true });
|
||||
const completionMs = performance.now() - started;
|
||||
rows.push({ round, input, completionMs, inputDelayMs: await inputTurn, matches: suggestions.length });
|
||||
}
|
||||
}
|
||||
const pinyinKeys = getPinyinCacheStatsForTests().keys;
|
||||
const chinese = await getCompletions('git pull yx', { snippets, skipPathCompletion: true });
|
||||
return { rows, pinyinKeys, chineseSnippetId: chinese.find(item => item.source === 'snippet')?.snippet?.id };
|
||||
})()`);
|
||||
assert.ok(result.rows.every(row => row.matches === 0));
|
||||
assert.deepEqual(result.pinyinKeys, ["邮箱"], "ASCII command words must bypass transliteration");
|
||||
assert.equal(result.chineseSnippetId, "mail", "Chinese initials must still reach the snippet");
|
||||
// Report timings without a machine-dependent threshold. The cache-work
|
||||
// assertion above is the deterministic regression gate on this real path.
|
||||
console.log("TERMINAL_SNIPPET_COMPLETION_PERFORMANCE_OK", JSON.stringify(result));
|
||||
cleanup(0);
|
||||
}).catch(error => { console.error(error); cleanup(1); });
|
||||
}
|
||||
Reference in new issue
Block a user