mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): handle CJK punctuation in file autocomplete (#9746)
This commit is contained in:
parent
c596d09d9c
commit
bfa6862400
5 changed files with 451 additions and 41 deletions
|
|
@ -147,6 +147,79 @@ describe("CombinedAutocompleteProvider", () => {
|
|||
assert.deepStrictEqual(values, ["@README.md", "@src/"].sort());
|
||||
});
|
||||
|
||||
test("recognizes @ after CJK punctuation without consuming the preceding text", async () => {
|
||||
setupFolder(baseDir, { files: { "README.md": "readme" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
for (const before of ["查看,", "\u3000", ...",.:;!?()[]{}“”‘’…—。、「」『』《》【】"]) {
|
||||
for (const force of [false, true]) {
|
||||
const line = `${before}@REA`;
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, force);
|
||||
assert.ok(result, line);
|
||||
assert.strictEqual(result.prefix, "@REA");
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
["@README.md"],
|
||||
);
|
||||
const applied = provider.applyCompletion([line], 0, line.length, result.items[0]!, result.prefix);
|
||||
assert.strictEqual(applied.lines[0], `${before}@README.md `);
|
||||
assert.strictEqual(applied.cursorCol, applied.lines[0]!.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves CJK characters and embedded @ in attachment paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: { "文档/说明.md": "text", "文档@备份/说明.md": "backup" },
|
||||
});
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
for (const before of ["", "查看,"]) {
|
||||
for (const directory of ["文档", "文档@备份"]) {
|
||||
const prefix = `@${directory}/说`;
|
||||
const line = before + prefix;
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
assert.ok(result, line);
|
||||
assert.strictEqual(result.prefix, prefix);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
[`@${directory}/说明.md`],
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("completes quoted CJK attachments after prose without losing path segments or quotes", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
for (const separator of [" ", "\u3000", ",", "。"]) {
|
||||
const directory = `我的${separator}文档`;
|
||||
setupFolder(baseDir, {
|
||||
files: { [`${directory}/说明.md`]: "text", "文档/说明.md": "not the quoted path" },
|
||||
});
|
||||
const line = `查看:@"${directory}/说"后文`;
|
||||
const cursorCol = line.indexOf('"后文');
|
||||
const result = await getSuggestions(provider, [line], 0, cursorCol);
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.prefix, `@"${directory}/说`);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
[`@"${directory}/说明.md"`],
|
||||
);
|
||||
const applied = provider.applyCompletion([line], 0, cursorCol, result.items[0]!, result.prefix);
|
||||
assert.strictEqual(applied.lines[0], `查看:@"${directory}/说明.md" 后文`);
|
||||
assert.strictEqual(applied.cursorCol, `查看:@"${directory}/说明.md" `.length);
|
||||
}
|
||||
});
|
||||
|
||||
test("does not interpret email addresses or @ after ASCII or CJK letters as attachment prefixes", async () => {
|
||||
setupFolder(baseDir, { files: { "README.md": "readme", "example.com": "text" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
for (const before of ["user", "查看", "あ", "カ", "한", "ㄅ", "𠮷", "か\u3099", "禰\u{e0100}", "々", "A"]) {
|
||||
for (const name of ["REA", "example.com"]) {
|
||||
const line = `${before}@${name}`;
|
||||
assert.strictEqual(await getSuggestions(provider, [line], 0, line.length), null, line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("matches file with extension in query", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
|
|
@ -300,20 +373,21 @@ describe("CombinedAutocompleteProvider", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("quotes paths with spaces for @ suggestions", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["my folder"],
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
},
|
||||
});
|
||||
|
||||
test("quotes paths containing whitespace or CJK punctuation for @ suggestions", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath());
|
||||
const line = "@my";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes('@"my folder/"'));
|
||||
for (const separator of [" ", "\u3000", ",", "。"]) {
|
||||
const directory = `my${separator}folder`;
|
||||
setupFolder(baseDir, { files: { [`${directory}/test.txt`]: "content" } });
|
||||
const line = "@my";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
assert.ok(result);
|
||||
const item = result.items.find((entry) => entry.value === `@"${directory}/"`);
|
||||
assert.ok(item, directory);
|
||||
const applied = provider.applyCompletion([line], 0, line.length, item, result.prefix);
|
||||
const continued = await getSuggestions(provider, applied.lines, 0, applied.cursorCol);
|
||||
assert.strictEqual(continued?.prefix, `@"${directory}/`);
|
||||
assert.ok(continued?.items.some((entry) => entry.value === `@"${directory}/test.txt"`));
|
||||
}
|
||||
});
|
||||
|
||||
test("includes hidden paths but excludes .git", async () => {
|
||||
|
|
@ -474,6 +548,100 @@ describe("CombinedAutocompleteProvider", () => {
|
|||
rmSync(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("completes Chinese path prefixes after whitespace or CJK punctuation on Tab", async () => {
|
||||
setupFolder(baseDir, { files: { "说明.md": "file", "文档/说明.md": "nested file" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const completions = [
|
||||
{ prefix: "说", value: "说明.md" },
|
||||
{ prefix: "文", value: "文档/" },
|
||||
{ prefix: "文档/说", value: "文档/说明.md" },
|
||||
{ prefix: "./文档/说", value: "./文档/说明.md" },
|
||||
];
|
||||
if (process.platform !== "win32") {
|
||||
completions.push({ prefix: `${baseDir}/文档/说`, value: `${baseDir}/文档/说明.md` });
|
||||
}
|
||||
for (const separator of " \t\u3000\u00a0,:;。!?(「《") {
|
||||
for (const { prefix, value } of completions) {
|
||||
const before = `查看𠮷${separator}`;
|
||||
const line = `${before}${prefix} 后文`;
|
||||
const cursorCol = before.length + prefix.length;
|
||||
const result = await getSuggestions(provider, [line], 0, cursorCol, true);
|
||||
assert.ok(result, line);
|
||||
assert.strictEqual(result.prefix, prefix);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
[value],
|
||||
);
|
||||
const applied = provider.applyCompletion([line], 0, cursorCol, result.items[0]!, result.prefix);
|
||||
assert.strictEqual(applied.lines[0], `${before}${value} 后文`);
|
||||
assert.strictEqual(applied.cursorCol, before.length + value.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("treats unquoted separators as boundaries even when a matching literal path exists", async () => {
|
||||
setupFolder(baseDir, { files: { "归档/说明.md": "other" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
for (const separator of [" ", "\u3000", ",", "。"]) {
|
||||
const directory = `资料${separator}归档`;
|
||||
setupFolder(baseDir, { files: { [`${directory}/说明.md`]: "archive" } });
|
||||
for (const marker of ["", "@"]) {
|
||||
const line = `${marker}${directory}/说`;
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
assert.ok(result, line);
|
||||
assert.strictEqual(result.prefix, "归档/说");
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
["归档/说明.md"],
|
||||
);
|
||||
}
|
||||
const quoted = `查看,"${directory}/说"后文`;
|
||||
const cursorCol = quoted.indexOf('"后文');
|
||||
const result = await getSuggestions(provider, [quoted], 0, cursorCol, true);
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.prefix, `"${directory}/说`);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
[`"${directory}/说明.md"`],
|
||||
);
|
||||
const applied = provider.applyCompletion([quoted], 0, cursorCol, result.items[0]!, result.prefix);
|
||||
assert.strictEqual(applied.lines[0], `查看,"${directory}/说明.md"后文`);
|
||||
const missing = `查看,"不存在${separator}归档/说`;
|
||||
assert.strictEqual(await getSuggestions(provider, [missing], 0, missing.length, true), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("handles an empty prefix after whitespace or CJK punctuation consistently", async () => {
|
||||
setupFolder(baseDir, { files: { "说明.md": "text" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
for (const separator of [" ", "\t", "\u3000", ",", "。"]) {
|
||||
for (const force of [false, true]) {
|
||||
const line = `查看${separator}`;
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, force);
|
||||
assert.ok(result, line);
|
||||
assert.strictEqual(result.prefix, "");
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
["说明.md"],
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.strictEqual(await getSuggestions(provider, [""], 0, 0), null);
|
||||
});
|
||||
|
||||
test("preserves CJK characters in unprefixed Tab completions", async () => {
|
||||
setupFolder(baseDir, { files: { "文档/说明.md": "text" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = "文档/说";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
assert.ok(result);
|
||||
assert.strictEqual(result.prefix, line);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
["文档/说明.md"],
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves ./ prefix when completing paths", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
|
|
@ -520,21 +688,35 @@ describe("CombinedAutocompleteProvider", () => {
|
|||
rmSync(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("quotes paths with spaces for direct completion", async () => {
|
||||
setupFolder(baseDir, {
|
||||
dirs: ["my folder"],
|
||||
files: {
|
||||
"my folder/test.txt": "content",
|
||||
},
|
||||
});
|
||||
|
||||
test("quotes paths containing whitespace or CJK punctuation for direct completion", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const line = "my";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
for (const separator of [" ", "\u3000", ",", "。"]) {
|
||||
const directory = `my${separator}folder`;
|
||||
setupFolder(baseDir, { files: { [`${directory}/test.txt`]: "content" } });
|
||||
const line = "my";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length, true);
|
||||
assert.ok(result);
|
||||
const item = result.items.find((entry) => entry.value === `"${directory}/"`);
|
||||
assert.ok(item, directory);
|
||||
const applied = provider.applyCompletion([line], 0, line.length, item, result.prefix);
|
||||
const continued = await getSuggestions(provider, applied.lines, 0, applied.cursorCol, true);
|
||||
assert.strictEqual(continued?.prefix, `"${directory}/`);
|
||||
assert.deepStrictEqual(
|
||||
continued?.items.map((entry) => entry.value),
|
||||
[`"${directory}/test.txt"`],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for path completion");
|
||||
const values = result?.items.map((item) => item.value);
|
||||
assert.ok(values?.includes('"my folder/"'));
|
||||
test("keeps quoted directories before files", async () => {
|
||||
setupFolder(baseDir, { dirs: ["z folder", "z,folder"], files: { "a.txt": "text" } });
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir);
|
||||
const result = await getSuggestions(provider, [""], 0, 0, true);
|
||||
assert.ok(result);
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.label.endsWith("/")),
|
||||
[true, true, false],
|
||||
);
|
||||
});
|
||||
|
||||
test("continues completion inside quoted paths", async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import assert from "node:assert";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
import { type AutocompleteProvider, CombinedAutocompleteProvider } from "../src/autocomplete.ts";
|
||||
|
|
@ -2129,6 +2132,209 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
describe("Autocomplete", () => {
|
||||
it("triggers and debounces symbol completion after CJK punctuation", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
for (const before of ["查看,", "\u3000", ...",.:;!?()[]{}“”‘’…—。、「」『』《》【】"]) {
|
||||
for (const trigger of ["@", "#", "$", "-"]) {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const requests: string[] = [];
|
||||
editor.setAutocompleteProvider({
|
||||
triggerCharacters: ["$", "-"],
|
||||
getSuggestions: async (lines, cursorLine, cursorCol) => {
|
||||
requests.push(lines[cursorLine]!.slice(0, cursorCol));
|
||||
return null;
|
||||
},
|
||||
applyCompletion,
|
||||
});
|
||||
editor.setText(before);
|
||||
editor.handleInput(trigger);
|
||||
t.mock.timers.tick(19);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, []);
|
||||
t.mock.timers.tick(1);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, [before + trigger]);
|
||||
|
||||
editor.handleInput("r");
|
||||
editor.handleInput("e");
|
||||
t.mock.timers.tick(19);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(requests.length, 1);
|
||||
t.mock.timers.tick(1);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, [before + trigger, `${before}${trigger}re`]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("does not auto-trigger after CJK letters or for unprefixed paths", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
let requests = 0;
|
||||
editor.setAutocompleteProvider({
|
||||
getSuggestions: async () => {
|
||||
requests++;
|
||||
return null;
|
||||
},
|
||||
applyCompletion,
|
||||
});
|
||||
for (const text of [
|
||||
"user@example.com",
|
||||
"张三@example.com",
|
||||
"查看@src",
|
||||
"あ@src",
|
||||
"カ@src",
|
||||
"한@src",
|
||||
"ㄅ@src",
|
||||
"𠮷@src",
|
||||
"か\u3099@src",
|
||||
"禰\u{e0100}@src",
|
||||
"々@src",
|
||||
"A@src",
|
||||
"文档@备份",
|
||||
"prefix#123",
|
||||
"问题#123",
|
||||
"查看,/path/",
|
||||
"查看,./文档/",
|
||||
"src/index.ts",
|
||||
"./文档/说明.md",
|
||||
"文档/说明.md",
|
||||
"查看src/index.ts",
|
||||
]) {
|
||||
editor.setText("");
|
||||
for (const char of text) editor.handleInput(char);
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(requests, 0, text);
|
||||
}
|
||||
});
|
||||
|
||||
it("requests path completion after CJK punctuation only on Tab", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const requests: Array<{ text: string; force: boolean | undefined }> = [];
|
||||
editor.setAutocompleteProvider({
|
||||
getSuggestions: async (lines, cursorLine, cursorCol, options) => {
|
||||
requests.push({ text: lines[cursorLine]!.slice(0, cursorCol), force: options.force });
|
||||
return null;
|
||||
},
|
||||
applyCompletion,
|
||||
});
|
||||
const text = "查看,/path/";
|
||||
for (const char of text) editor.handleInput(char);
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, []);
|
||||
editor.handleInput("\t");
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, [{ text, force: true }]);
|
||||
});
|
||||
|
||||
it("completes Chinese path prefixes after whitespace or CJK punctuation with Tab", async (t) => {
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "pi-editor-autocomplete-"));
|
||||
t.after(() => rmSync(baseDir, { recursive: true, force: true }));
|
||||
mkdirSync(join(baseDir, "文档"));
|
||||
writeFileSync(join(baseDir, "文档", "说明.md"), "text");
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
editor.setAutocompleteProvider(new CombinedAutocompleteProvider([], baseDir));
|
||||
for (const separator of [" ", "\t", "\u3000", "\u00a0", ",", "。"]) {
|
||||
editor.setText(`查看${separator}`);
|
||||
const before = editor.getText();
|
||||
editor.handleInput("文");
|
||||
editor.handleInput("\t");
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.getText(), `${before}文档/`);
|
||||
editor.handleInput("说");
|
||||
editor.handleInput("\t");
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.getText(), `${before}文档/说明.md`);
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: editor.getText().length });
|
||||
}
|
||||
});
|
||||
|
||||
it("ends unquoted trigger and debounce contexts at whitespace or CJK punctuation", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
for (const separator of [" ", "\u3000", ",", "。"]) {
|
||||
for (const trigger of ["@", "#", "$"]) {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const requests: string[] = [];
|
||||
const prefix = `${trigger}src`;
|
||||
editor.setAutocompleteProvider({
|
||||
triggerCharacters: ["$"],
|
||||
getSuggestions: async (lines, cursorLine, cursorCol) => {
|
||||
const text = lines[cursorLine]!.slice(0, cursorCol);
|
||||
requests.push(text);
|
||||
return text === prefix ? { prefix, items: [{ value: `${prefix}/`, label: "src/" }] } : null;
|
||||
},
|
||||
applyCompletion,
|
||||
});
|
||||
editor.setText(`${trigger}sr`);
|
||||
editor.handleInput("c");
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), true);
|
||||
editor.handleInput(separator);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, [prefix, prefix + separator]);
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), false);
|
||||
editor.handleInput("文");
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.deepStrictEqual(requests, [prefix, prefix + separator]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("re-triggers CJK path completion after accepting directories and deleting", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const provider = new CombinedAutocompleteProvider([], process.cwd());
|
||||
for (const directory of ["文档", "我的 文档", "资料,归档"]) {
|
||||
const quoted = directory !== "文档";
|
||||
const initial = quoted ? `@"${directory.slice(0, 2)}` : "@文";
|
||||
const directoryValue = quoted ? `@"${directory}/"` : `@${directory}/`;
|
||||
const fileValue = quoted ? `@"${directory}/说明.md"` : `@${directory}/说明.md`;
|
||||
const filePrefix = quoted ? `@"${directory}/说` : `@${directory}/说`;
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
editor.setAutocompleteProvider({
|
||||
getSuggestions: async (lines, cursorLine, cursorCol) => {
|
||||
const before = lines[cursorLine]!.slice(0, cursorCol);
|
||||
const prefix = before.slice(before.indexOf("@"));
|
||||
if (prefix === initial) {
|
||||
return { prefix, items: [{ value: directoryValue, label: `${directory}/` }] };
|
||||
}
|
||||
return prefix === filePrefix ? { prefix, items: [{ value: fileValue, label: "说明.md" }] } : null;
|
||||
},
|
||||
applyCompletion: (...args) => provider.applyCompletion(...args),
|
||||
});
|
||||
editor.setText(`查看:${initial}`);
|
||||
editor.handleInput("\t");
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.getText(), `查看:${directoryValue}`);
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), false);
|
||||
|
||||
editor.handleInput("说");
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), true);
|
||||
|
||||
for (const deletion of ["\x7f", "\x1b[3~"]) {
|
||||
editor.handleInput("错");
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), false);
|
||||
if (deletion === "\x1b[3~") editor.handleInput("\x1b[D");
|
||||
editor.handleInput(deletion);
|
||||
t.mock.timers.tick(20);
|
||||
await flushAutocomplete();
|
||||
assert.strictEqual(editor.isShowingAutocomplete(), true);
|
||||
}
|
||||
|
||||
editor.handleInput("\t");
|
||||
assert.strictEqual(editor.getText(), `查看:${fileValue} `);
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: editor.getText().length });
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-applies single force-file suggestion without showing menu", async () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue