mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
fix(tui): preserve autocomplete punctuation context
This commit is contained in:
@@ -57,22 +57,38 @@ function findLastDelimiter(text: string): number {
|
||||
}
|
||||
|
||||
function isExplicitPathPrefixStart(prefix: string): boolean {
|
||||
return prefix.startsWith(".") || prefix.startsWith("/") || prefix.startsWith("~/") || prefix === "~";
|
||||
return (
|
||||
prefix.startsWith("./") ||
|
||||
prefix.startsWith("../") ||
|
||||
prefix.startsWith("/") ||
|
||||
prefix.startsWith("~/") ||
|
||||
prefix === "~"
|
||||
);
|
||||
}
|
||||
|
||||
function isLeadingPunctuationRun(token: string, endIndex: number): boolean {
|
||||
for (let i = 0; i <= endIndex; i += 1) {
|
||||
if (!AUTOCOMPLETE_BOUNDARY_PUNCTUATION.has(token[i] ?? "")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLeadingDotfilePrefixStart(token: string, punctuationIndex: number, suffix: string): boolean {
|
||||
return suffix.startsWith(".") && !suffix.startsWith("./") && isLeadingPunctuationRun(token, punctuationIndex);
|
||||
}
|
||||
|
||||
function findPathPrefixStartInToken(token: string): number {
|
||||
let tokenStart = 0;
|
||||
while (AUTOCOMPLETE_BOUNDARY_PUNCTUATION.has(token[tokenStart] ?? "")) {
|
||||
tokenStart += 1;
|
||||
}
|
||||
|
||||
for (let i = tokenStart; i < token.length; i += 1) {
|
||||
for (let i = 0; i < token.length; i += 1) {
|
||||
if (!AUTOCOMPLETE_BOUNDARY_PUNCTUATION.has(token[i] ?? "")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const suffix = token.slice(i + 1);
|
||||
if (isExplicitPathPrefixStart(suffix)) {
|
||||
if (isExplicitPathPrefixStart(suffix) || isLeadingDotfilePrefixStart(token, i, suffix)) {
|
||||
tokenStart = i + 1;
|
||||
}
|
||||
}
|
||||
@@ -496,7 +512,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||
const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
|
||||
const token = text.slice(tokenStart);
|
||||
|
||||
for (let i = 0; i < token.length; i += 1) {
|
||||
for (let i = token.length - 1; i >= 0; i -= 1) {
|
||||
const absoluteIndex = tokenStart + i;
|
||||
if (token[i] === "@" && isTokenStart(text, absoluteIndex)) {
|
||||
return text.slice(absoluteIndex);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, it, test } from "node:test";
|
||||
@@ -114,6 +114,38 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("@ prefix extraction", () => {
|
||||
let rootDir = "";
|
||||
let baseDir = "";
|
||||
let fakeFdPath = "";
|
||||
|
||||
beforeEach(() => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "pi-autocomplete-at-"));
|
||||
baseDir = join(rootDir, "cwd");
|
||||
mkdirSync(baseDir, { recursive: true });
|
||||
fakeFdPath = join(rootDir, "fake-fd.js");
|
||||
writeFileSync(fakeFdPath, '#!/usr/bin/env node\nprocess.stdout.write("old.ts\\nmain.ts\\n");\n');
|
||||
chmodSync(fakeFdPath, 0o755);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("uses the nearest @ after punctuation", async () => {
|
||||
const provider = new CombinedAutocompleteProvider([], baseDir, fakeFdPath);
|
||||
const line = "(@old),(@mai";
|
||||
const result = await getSuggestions(provider, [line], 0, line.length);
|
||||
|
||||
assert.notEqual(result, null, "Should return suggestions for the current @ prefix");
|
||||
assert.strictEqual(result?.prefix, "@mai");
|
||||
assert.deepStrictEqual(
|
||||
result?.items.map((item) => item.value),
|
||||
["@main.ts"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fd @ file suggestions", { skip: !isFdInstalled }, () => {
|
||||
let rootDir = "";
|
||||
let baseDir = "";
|
||||
@@ -495,6 +527,7 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
test("does not split path prefixes containing punctuation", async () => {
|
||||
setupFolder(baseDir, {
|
||||
files: {
|
||||
"(foo).txt": "content",
|
||||
"foo(bar).txt": "content",
|
||||
"name[part].txt": "content",
|
||||
},
|
||||
@@ -508,6 +541,30 @@ describe("CombinedAutocompleteProvider", () => {
|
||||
assert.strictEqual(parenResult?.prefix, "./foo(");
|
||||
assert.ok(parenResult?.items.some((item) => item.value === "./foo(bar).txt"));
|
||||
|
||||
const completedParenLine = "./foo(bar).t";
|
||||
const completedParenResult = await getSuggestions(
|
||||
provider,
|
||||
[completedParenLine],
|
||||
0,
|
||||
completedParenLine.length,
|
||||
true,
|
||||
);
|
||||
assert.notEqual(completedParenResult, null, "Should preserve punctuation before file extensions");
|
||||
assert.strictEqual(completedParenResult?.prefix, "./foo(bar).t");
|
||||
assert.ok(completedParenResult?.items.some((item) => item.value === "./foo(bar).txt"));
|
||||
|
||||
const leadingPunctuationLine = "(foo).t";
|
||||
const leadingPunctuationResult = await getSuggestions(
|
||||
provider,
|
||||
[leadingPunctuationLine],
|
||||
0,
|
||||
leadingPunctuationLine.length,
|
||||
true,
|
||||
);
|
||||
assert.notEqual(leadingPunctuationResult, null, "Should preserve leading punctuation in filenames");
|
||||
assert.strictEqual(leadingPunctuationResult?.prefix, "(foo).t");
|
||||
assert.ok(leadingPunctuationResult?.items.some((item) => item.value === "(foo).txt"));
|
||||
|
||||
const bracketLine = "./name[";
|
||||
const bracketResult = await getSuggestions(provider, [bracketLine], 0, bracketLine.length, true);
|
||||
assert.notEqual(bracketResult, null, "Should return suggestions for path containing brackets");
|
||||
|
||||
Reference in New Issue
Block a user