feat: standardized tab expansion to fixed width
- Removed configurable tab width support and the `display.tabWidth` setting across all packages. - Deleted obsolete utility functions `getIndentation`, `getIndentationNoescape`, and `setDefaultTabWidth`. - Standardized tab expansion logic to use a fixed `DEFAULT_TAB_WIDTH` globally. - Cleaned up related configuration schemas, test suites, and internal API signatures to remove path-dependency.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `__advisor.jsonl` transcript persistence for advisor model usage attribution and visibility in the Agent Hub
|
- Added `__advisor.jsonl` transcript persistence for advisor model usage attribution and visibility in the Agent Hub
|
||||||
@@ -22,6 +21,10 @@
|
|||||||
- Refined `/compact` argument parsing to reject focus instructions for modes that do not support them (e.g., `snapcompact`)
|
- Refined `/compact` argument parsing to reject focus instructions for modes that do not support them (e.g., `snapcompact`)
|
||||||
- Protocol hosts (RPC/`rpc-ui`/ACP) now host-default the full advisor settings group — `advisor.syncBacklog` and `advisor.immuneTurns` in addition to `advisor.enabled`/`advisor.subagents` — so a host that opts the advisor in gets the default tuning instead of inheriting the user's local advisor preferences.
|
- Protocol hosts (RPC/`rpc-ui`/ACP) now host-default the full advisor settings group — `advisor.syncBacklog` and `advisor.immuneTurns` in addition to `advisor.enabled`/`advisor.subagents` — so a host that opts the advisor in gets the default tuning instead of inheriting the user's local advisor preferences.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed `display.tabWidth` setting and configurable tab width support
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed memory-leaking stale transcripts in the agent viewer when underlying files are deleted
|
- Fixed memory-leaking stale transcripts in the agent viewer when underlying files are deleted
|
||||||
|
|||||||
@@ -809,11 +809,6 @@ export const SETTINGS_SCHEMA = {
|
|||||||
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Display rendering
|
|
||||||
"display.tabWidth": {
|
|
||||||
type: "number",
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
"display.shimmer": {
|
"display.shimmer": {
|
||||||
type: "enum",
|
type: "enum",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
isEnoent,
|
isEnoent,
|
||||||
logger,
|
logger,
|
||||||
procmgr,
|
procmgr,
|
||||||
setDefaultTabWidth,
|
|
||||||
} from "@oh-my-pi/pi-utils";
|
} from "@oh-my-pi/pi-utils";
|
||||||
import { JSONC, YAML } from "bun";
|
import { JSONC, YAML } from "bun";
|
||||||
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
||||||
@@ -1103,11 +1102,6 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display.tabWidth": value => {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
setDefaultTabWidth(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider.appendOnlyContext": value => {
|
"provider.appendOnlyContext": value => {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
appendOnlyModeSignal.fire(value);
|
appendOnlyModeSignal.fire(value);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const HASHLINE_LINE_PREFIX = /^[ *]?(\d+)(?:-(\d+))?:/;
|
|||||||
/**
|
/**
|
||||||
* The 1-indexed file lines a hashline-formatted body actually displayed.
|
* The 1-indexed file lines a hashline-formatted body actually displayed.
|
||||||
* Single `NN:` rows contribute that line; a collapsed summary `NN-MM:` row
|
* Single `NN:` rows contribute that line; a collapsed summary `NN-MM:` row
|
||||||
* (a `{ .. }` brace pair) contributes only its boundary lines `NN` and `MM` —
|
* (a `{ … }` brace pair) contributes only its boundary lines `NN` and `MM` —
|
||||||
* the elided interior was never shown, so editing inside it must be rejected.
|
* the elided interior was never shown, so editing inside it must be rejected.
|
||||||
*/
|
*/
|
||||||
export function parseSeenLinesFromHashlineBody(body: string): number[] {
|
export function parseSeenLinesFromHashlineBody(body: string): number[] {
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ function truncateEditTitlePath(displayPath: string, maxWidth: number | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatEditTitlePath(pathValue: string, maxWidth?: number): string {
|
function formatEditTitlePath(pathValue: string, maxWidth?: number): string {
|
||||||
return truncateEditTitlePath(replaceTabs(shortenPath(pathValue), pathValue), maxWidth);
|
return truncateEditTitlePath(replaceTabs(shortenPath(pathValue)), maxWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEditPathDisplay(
|
function formatEditPathDisplay(
|
||||||
@@ -326,11 +326,11 @@ function renderEditHeader(
|
|||||||
return buildHeader(fitted.description);
|
return buildHeader(fitted.description);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string): string {
|
function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string): string {
|
||||||
const previewLines = sanitizeText(text).split("\n");
|
const previewLines = sanitizeText(text).split("\n");
|
||||||
let preview = "\n\n";
|
let preview = "\n\n";
|
||||||
for (const line of previewLines.slice(0, CALL_TEXT_PREVIEW_LINES)) {
|
for (const line of previewLines.slice(0, CALL_TEXT_PREVIEW_LINES)) {
|
||||||
preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line, filePath), CALL_TEXT_PREVIEW_WIDTH))}\n`;
|
preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), CALL_TEXT_PREVIEW_WIDTH))}\n`;
|
||||||
}
|
}
|
||||||
if (previewLines.length > CALL_TEXT_PREVIEW_LINES) {
|
if (previewLines.length > CALL_TEXT_PREVIEW_LINES) {
|
||||||
preview += uiTheme.fg("dim", `… ${previewLines.length - CALL_TEXT_PREVIEW_LINES} more lines`);
|
preview += uiTheme.fg("dim", `… ${previewLines.length - CALL_TEXT_PREVIEW_LINES} more lines`);
|
||||||
@@ -395,7 +395,7 @@ function formatMultiFileStreamingDiff(
|
|||||||
if (!preview.diff && !preview.error) continue;
|
if (!preview.diff && !preview.error) continue;
|
||||||
const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
|
const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
|
||||||
if (preview.error) {
|
if (preview.error) {
|
||||||
parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error, preview.path))}`);
|
parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (preview.diff) {
|
if (preview.diff) {
|
||||||
@@ -637,7 +637,7 @@ export const editToolRenderer = {
|
|||||||
callPreviewCaches,
|
callPreviewCaches,
|
||||||
);
|
);
|
||||||
if (applyPatchSummary?.error) {
|
if (applyPatchSummary?.error) {
|
||||||
body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
|
body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error), Math.max(1, width - 2)))}`;
|
||||||
}
|
}
|
||||||
const bodyLines = body ? body.split("\n") : [];
|
const bodyLines = body ? body.split("\n") : [];
|
||||||
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
||||||
@@ -733,11 +733,11 @@ function renderSingleFileResult(
|
|||||||
|
|
||||||
let body = "";
|
let body = "";
|
||||||
if (isError) {
|
if (isError) {
|
||||||
if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
|
if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
|
||||||
} else if (details?.diff) {
|
} else if (details?.diff) {
|
||||||
body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
||||||
} else if (editDiffPreview) {
|
} else if (editDiffPreview) {
|
||||||
if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
|
if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error));
|
||||||
else if (editDiffPreview.diff)
|
else if (editDiffPreview.diff)
|
||||||
body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getIndentation, sanitizeText } from "@oh-my-pi/pi-utils";
|
import { DEFAULT_TAB_WIDTH, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||||
import * as Diff from "diff";
|
import * as Diff from "diff";
|
||||||
import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
|
import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
|
||||||
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
|
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
|
||||||
@@ -13,12 +13,12 @@ const DIM_OFF = "\x1b[22m";
|
|||||||
* before the first non-whitespace character; remaining tabs in code
|
* before the first non-whitespace character; remaining tabs in code
|
||||||
* content are replaced with spaces (like replaceTabs).
|
* content are replaced with spaces (like replaceTabs).
|
||||||
*/
|
*/
|
||||||
function visualizeIndent(text: string, filePath?: string): string {
|
function visualizeIndent(text: string): string {
|
||||||
const match = text.match(/^([ \t]+)/);
|
const match = text.match(/^([ \t]+)/);
|
||||||
if (!match) return replaceTabs(text, filePath);
|
if (!match) return replaceTabs(text);
|
||||||
const indent = match[1];
|
const indent = match[1];
|
||||||
const rest = text.slice(indent.length);
|
const rest = text.slice(indent.length);
|
||||||
const tabWidth = getIndentation(filePath);
|
const tabWidth = DEFAULT_TAB_WIDTH;
|
||||||
const leftPadding = Math.floor(tabWidth / 2);
|
const leftPadding = Math.floor(tabWidth / 2);
|
||||||
const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
|
const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
|
||||||
const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
|
const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
|
||||||
@@ -30,7 +30,7 @@ function visualizeIndent(text: string, filePath?: string): string {
|
|||||||
visible += `${DIM}·${DIM_OFF}`;
|
visible += `${DIM}·${DIM_OFF}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `${visible}${replaceTabs(rest, filePath)}`;
|
return `${visible}${replaceTabs(rest)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,7 +153,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|||||||
// unicode ellipsis.
|
// unicode ellipsis.
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
const isGapRow = trimmed.length === 0 || trimmed === "..." || trimmed === "…";
|
const isGapRow = trimmed.length === 0 || trimmed === "..." || trimmed === "…";
|
||||||
result.push(theme.fg("toolDiffContext", isGapRow ? "…" : replaceTabs(line, options.filePath)));
|
result.push(theme.fg("toolDiffContext", isGapRow ? "…" : replaceTabs(line)));
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -184,47 +184,24 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|||||||
replaceTabs(added.content),
|
replaceTabs(added.content),
|
||||||
);
|
);
|
||||||
|
|
||||||
result.push(
|
result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removedLine))));
|
||||||
theme.fg(
|
result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine))));
|
||||||
"toolDiffRemoved",
|
|
||||||
formatLine("-", removed.lineNum, visualizeIndent(removedLine, options.filePath)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
result.push(
|
|
||||||
theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine, options.filePath))),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
for (const removed of removedLines) {
|
for (const removed of removedLines) {
|
||||||
result.push(
|
result.push(
|
||||||
theme.fg(
|
theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removed.content))),
|
||||||
"toolDiffRemoved",
|
|
||||||
formatLine("-", removed.lineNum, visualizeIndent(removed.content, options.filePath)),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const added of addedLines) {
|
for (const added of addedLines) {
|
||||||
result.push(
|
result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(added.content))));
|
||||||
theme.fg(
|
|
||||||
"toolDiffAdded",
|
|
||||||
formatLine("+", added.lineNum, visualizeIndent(added.content, options.filePath)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (parsed.prefix === "+") {
|
} else if (parsed.prefix === "+") {
|
||||||
result.push(
|
result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, visualizeIndent(parsed.content))));
|
||||||
theme.fg(
|
|
||||||
"toolDiffAdded",
|
|
||||||
formatLine("+", parsed.lineNum, visualizeIndent(parsed.content, options.filePath)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
i++;
|
||||||
} else {
|
} else {
|
||||||
const highlighted = contextHighlights.get(i);
|
const highlighted = contextHighlights.get(i);
|
||||||
const content =
|
const content = highlighted !== undefined ? replaceTabs(highlighted) : visualizeIndent(parsed.content);
|
||||||
highlighted !== undefined
|
|
||||||
? replaceTabs(highlighted, options.filePath)
|
|
||||||
: visualizeIndent(parsed.content, options.filePath);
|
|
||||||
result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, content)));
|
result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, content)));
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
||||||
import * as fs from "node:fs/promises";
|
|
||||||
import * as os from "node:os";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
||||||
import { getDefaultTabWidth, getIndentation, Snowflake, setDefaultTabWidth } from "@oh-my-pi/pi-utils";
|
|
||||||
import { beginSettingsTest, restoreSettingsTestState, type SettingsTestState } from "./helpers/settings-test-state";
|
|
||||||
|
|
||||||
describe("indentation resolver", () => {
|
|
||||||
let settingsState: SettingsTestState | undefined;
|
|
||||||
let tempDir = "";
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
settingsState = beginSettingsTest();
|
|
||||||
setDefaultTabWidth(3);
|
|
||||||
tempDir = path.join(os.tmpdir(), "pi-spacing", Snowflake.next());
|
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
restoreSettingsTestState(settingsState);
|
|
||||||
settingsState = undefined;
|
|
||||||
if (tempDir) {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
tempDir = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies current display tab width during initial settings load", async () => {
|
|
||||||
await Settings.init({ inMemory: true, cwd: tempDir, overrides: { "display.tabWidth": 7 } });
|
|
||||||
expect(getDefaultTabWidth()).toBe(7);
|
|
||||||
expect(getIndentation()).toBe(7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies nearest editorconfig rules for the target file", async () => {
|
|
||||||
const filePath = path.join(tempDir, "src", "feature.ts");
|
|
||||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
||||||
await fs.writeFile(filePath, "\tconst x = 1;\n");
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(tempDir, ".editorconfig"),
|
|
||||||
["root = true", "", "[*]", "indent_size = 2", "", "[*.md]", "indent_size = 4"].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getIndentation(filePath)).toBe(2);
|
|
||||||
expect(getIndentation(path.join(tempDir, "README.md"))).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges editorconfig files from root to leaf", async () => {
|
|
||||||
const nestedDir = path.join(tempDir, "packages", "feature");
|
|
||||||
const filePath = path.join(nestedDir, "index.ts");
|
|
||||||
await fs.mkdir(nestedDir, { recursive: true });
|
|
||||||
await fs.writeFile(filePath, "\tconst y = 2;\n");
|
|
||||||
|
|
||||||
await fs.writeFile(path.join(tempDir, ".editorconfig"), ["root = true", "", "[*]", "indent_size = 2"].join("\n"));
|
|
||||||
await fs.writeFile(path.join(tempDir, "packages", ".editorconfig"), ["[*.ts]", "indent_size = 6"].join("\n"));
|
|
||||||
|
|
||||||
expect(getIndentation(filePath)).toBe(6);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
import { vi } from "bun:test";
|
import { vi } from "bun:test";
|
||||||
import { resetSettingsForTest } from "@oh-my-pi/pi-coding-agent/config/settings";
|
import { resetSettingsForTest } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||||
import { isTuiTight, setTuiTight } from "@oh-my-pi/pi-tui";
|
import { isTuiTight, setTuiTight } from "@oh-my-pi/pi-tui";
|
||||||
import {
|
import { getAgentDir, getProjectDir, setAgentDir, setProjectDir } from "@oh-my-pi/pi-utils";
|
||||||
getAgentDir,
|
|
||||||
getDefaultTabWidth,
|
|
||||||
getProjectDir,
|
|
||||||
setAgentDir,
|
|
||||||
setDefaultTabWidth,
|
|
||||||
setProjectDir,
|
|
||||||
} from "@oh-my-pi/pi-utils";
|
|
||||||
|
|
||||||
export interface SettingsTestState {
|
export interface SettingsTestState {
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
env: Record<string, string | undefined>;
|
env: Record<string, string | undefined>;
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
tabWidth: number;
|
|
||||||
tuiTight: boolean;
|
tuiTight: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +22,6 @@ export function beginSettingsTest(): SettingsTestState {
|
|||||||
agentDir: getAgentDir(),
|
agentDir: getAgentDir(),
|
||||||
env,
|
env,
|
||||||
projectDir: getProjectDir(),
|
projectDir: getProjectDir(),
|
||||||
tabWidth: getDefaultTabWidth(),
|
|
||||||
tuiTight: isTuiTight(),
|
tuiTight: isTuiTight(),
|
||||||
};
|
};
|
||||||
resetSettingsForTest();
|
resetSettingsForTest();
|
||||||
@@ -43,7 +34,6 @@ export function restoreSettingsTestState(state: SettingsTestState | undefined):
|
|||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
restoreEnv(state.env);
|
restoreEnv(state.env);
|
||||||
setDefaultTabWidth(state.tabWidth);
|
|
||||||
setProjectDir(state.projectDir);
|
setProjectDir(state.projectDir);
|
||||||
setAgentDir(state.agentDir);
|
setAgentDir(state.agentDir);
|
||||||
setTuiTight(state.tuiTight);
|
setTuiTight(state.tuiTight);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Removed `getIndentation` and `getIndentationNoescape` exported utilities
|
||||||
|
- Tab-related operations no longer respect per-file or globally configured indentation settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Standardized tab expansion to use a fixed display width instead of configurable settings
|
||||||
|
- Removed support for custom tab width configuration in text rendering and input handling
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Corrected logic in string truncation to prevent improper truncation of short strings
|
- Corrected logic in string truncation to prevent improper truncation of short strings
|
||||||
|
|||||||
+12
-19
@@ -7,11 +7,11 @@ import {
|
|||||||
wrapTextWithAnsi as nativeWrapTextWithAnsi,
|
wrapTextWithAnsi as nativeWrapTextWithAnsi,
|
||||||
type SliceResult,
|
type SliceResult,
|
||||||
} from "@oh-my-pi/pi-natives";
|
} from "@oh-my-pi/pi-natives";
|
||||||
import { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
|
import { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
||||||
|
|
||||||
export { Ellipsis } from "@oh-my-pi/pi-natives";
|
export { Ellipsis } from "@oh-my-pi/pi-natives";
|
||||||
|
|
||||||
export { getDefaultTabWidth, getIndentation } from "@oh-my-pi/pi-utils";
|
export { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
||||||
|
|
||||||
export type TextSizingScale = 1 | 2 | 3;
|
export type TextSizingScale = 1 | 2 | 3;
|
||||||
export type TextSizingVerticalAlign = "top" | "bottom" | "center";
|
export type TextSizingVerticalAlign = "top" | "bottom" | "center";
|
||||||
@@ -74,7 +74,7 @@ export function encodeTextSized(text: string, options: TextSizingOptions = {}):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult {
|
export function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult {
|
||||||
return nativeSliceWithWidth(line, startCol, length, strict ?? null, getDefaultTabWidth());
|
return nativeSliceWithWidth(line, startCol, length, strict ?? null, DEFAULT_TAB_WIDTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truncateToWidth(
|
export function truncateToWidth(
|
||||||
@@ -94,12 +94,12 @@ export function truncateToWidth(
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
(ellipsisKind === "" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
|
(ellipsisKind === "" ? Ellipsis.Omit : ellipsisKind) ?? Ellipsis.Unicode,
|
||||||
pad ?? false,
|
pad ?? false,
|
||||||
getDefaultTabWidth(),
|
DEFAULT_TAB_WIDTH,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapTextWithAnsi(text: string, width: number): string[] {
|
export function wrapTextWithAnsi(text: string, width: number): string[] {
|
||||||
return nativeWrapTextWithAnsi(text, width, getDefaultTabWidth());
|
return nativeWrapTextWithAnsi(text, width, DEFAULT_TAB_WIDTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractSegments(
|
export function extractSegments(
|
||||||
@@ -109,24 +109,17 @@ export function extractSegments(
|
|||||||
afterLen: number,
|
afterLen: number,
|
||||||
strictAfter: boolean,
|
strictAfter: boolean,
|
||||||
): ExtractSegmentsResult {
|
): ExtractSegmentsResult {
|
||||||
return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, getDefaultTabWidth());
|
return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, DEFAULT_TAB_WIDTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-allocated space buffer for padding
|
// Pre-allocated space buffer for padding
|
||||||
const SPACE_BUFFER = " ".repeat(512);
|
const SPACE_BUFFER = " ".repeat(512);
|
||||||
|
|
||||||
/**
|
|
||||||
* Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
|
|
||||||
*/
|
|
||||||
export function getIndentationNoescape(file?: string): number {
|
|
||||||
return getIndentation(file, process.cwd());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Replace tabs with configured spacing for consistent rendering.
|
* Replace tabs with the fixed display tab width for consistent rendering.
|
||||||
*/
|
*/
|
||||||
export function replaceTabs(text: string, file?: string): string {
|
export function replaceTabs(text: string): string {
|
||||||
return text.replaceAll("\t", " ".repeat(getIndentation(file)));
|
return text.replaceAll("\t", " ".repeat(DEFAULT_TAB_WIDTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,7 +176,7 @@ export function visibleWidth(str: string): number {
|
|||||||
for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
||||||
tabCount++;
|
tabCount++;
|
||||||
}
|
}
|
||||||
if (tabCount > 0) width += tabCount * getDefaultTabWidth();
|
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +193,7 @@ export function visibleWidth(str: string): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (i === str.length) {
|
if (i === str.length) {
|
||||||
return tabCount === 0 ? str.length : str.length + tabCount * (getDefaultTabWidth() - 1);
|
return tabCount === 0 ? str.length : str.length + tabCount * (DEFAULT_TAB_WIDTH - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabCount === 0) {
|
if (tabCount === 0) {
|
||||||
@@ -221,7 +214,7 @@ export function visibleWidth(str: string): number {
|
|||||||
// the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
|
// the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
|
||||||
// CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
|
// CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
|
||||||
let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
||||||
if (tabCount > 0) width += tabCount * getDefaultTabWidth();
|
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
||||||
|
|
||||||
// OSC 66: add back each stripped span as `scale * (explicit w ?? payload
|
// OSC 66: add back each stripped span as `scale * (explicit w ?? payload
|
||||||
// width)`. Matched rather than replaced to avoid reallocating the string.
|
// width)`. Matched rather than replaced to avoid reallocating the string.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Editor } from "@oh-my-pi/pi-tui/components/editor";
|
|||||||
import { KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "@oh-my-pi/pi-tui/keybindings";
|
import { KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "@oh-my-pi/pi-tui/keybindings";
|
||||||
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
||||||
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
||||||
import { setDefaultTabWidth } from "@oh-my-pi/pi-utils";
|
|
||||||
import { defaultEditorTheme } from "./test-themes";
|
import { defaultEditorTheme } from "./test-themes";
|
||||||
|
|
||||||
describe("Editor component", () => {
|
describe("Editor component", () => {
|
||||||
@@ -545,16 +544,10 @@ describe("Editor component", () => {
|
|||||||
expect(text).toBe("Hällö Wörld! 😀 äöüÄÖÜß");
|
expect(text).toBe("Hällö Wörld! 😀 äöüÄÖÜß");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the configured tab width when loading text programmatically", () => {
|
it("expands tabs to the fixed display width when loading text programmatically", () => {
|
||||||
const editor = new Editor(defaultEditorTheme);
|
const editor = new Editor(defaultEditorTheme);
|
||||||
|
editor.setText("foo\tbar");
|
||||||
try {
|
expect(editor.getText()).toBe("foo bar");
|
||||||
setDefaultTabWidth(5);
|
|
||||||
editor.setText("foo\tbar");
|
|
||||||
expect(editor.getText()).toBe("foo bar");
|
|
||||||
} finally {
|
|
||||||
setDefaultTabWidth(3);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips control characters from programmatically loaded text before render", () => {
|
it("strips control characters from programmatically loaded text before render", () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CURSOR_MARKER } from "@oh-my-pi/pi-tui";
|
|||||||
import { Input } from "@oh-my-pi/pi-tui/components/input";
|
import { Input } from "@oh-my-pi/pi-tui/components/input";
|
||||||
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
||||||
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
||||||
import { getIndentation } from "@oh-my-pi/pi-utils";
|
import { DEFAULT_TAB_WIDTH } from "@oh-my-pi/pi-utils";
|
||||||
|
|
||||||
function renderedWidth(input: Input, width: number): number {
|
function renderedWidth(input: Input, width: number): number {
|
||||||
const [line] = input.render(width);
|
const [line] = input.render(width);
|
||||||
@@ -155,7 +155,7 @@ describe("Input component", () => {
|
|||||||
setKittyProtocolActive(false);
|
setKittyProtocolActive(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes tabs in buffered bracketed paste using configured indentation", () => {
|
it("normalizes tabs in buffered bracketed paste using the fixed display width", () => {
|
||||||
const input = setupAtEnd("");
|
const input = setupAtEnd("");
|
||||||
|
|
||||||
input.handleInput("\x1b[200~a\t");
|
input.handleInput("\x1b[200~a\t");
|
||||||
@@ -165,7 +165,7 @@ describe("Input component", () => {
|
|||||||
expect(input.getValue()).toBe("");
|
expect(input.getValue()).toBe("");
|
||||||
|
|
||||||
input.handleInput("c\x1b[201~");
|
input.handleInput("c\x1b[201~");
|
||||||
expect(input.getValue()).toBe(`a${" ".repeat(getIndentation())}bc`);
|
expect(input.getValue()).toBe(`a${" ".repeat(DEFAULT_TAB_WIDTH)}bc`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decodes tmux re-encoded control bytes in bracketed paste without leaking tails or storing raw C0", () => {
|
it("decodes tmux re-encoded control bytes in bracketed paste without leaking tails or storing raw C0", () => {
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { visibleWidth as nativeVisibleWidth } from "@oh-my-pi/pi-natives";
|
import { visibleWidth as nativeVisibleWidth } from "@oh-my-pi/pi-natives";
|
||||||
import { getDefaultTabWidth, visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
import { DEFAULT_TAB_WIDTH, visibleWidth } from "@oh-my-pi/pi-tui/utils";
|
||||||
|
|
||||||
const ESC = "\x1b";
|
const ESC = "\x1b";
|
||||||
const ST = "\x1b\\";
|
const ST = "\x1b\\";
|
||||||
const BEL = "\x07";
|
const BEL = "\x07";
|
||||||
const TAB = getDefaultTabWidth();
|
const TAB = DEFAULT_TAB_WIDTH;
|
||||||
|
|
||||||
describe("visibleWidth — parity with the native width engine", () => {
|
describe("visibleWidth — parity with the native width engine", () => {
|
||||||
const corpus: [string, string][] = [
|
const corpus: [string, string][] = [
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed `getIndentation`, `setDefaultTabWidth`, and `getDefaultTabWidth` helpers
|
||||||
|
|
||||||
## [16.0.8] - 2026-06-18
|
## [16.0.8] - 2026-06-18
|
||||||
|
|
||||||
@@ -160,4 +163,4 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
|
- Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
|
||||||
@@ -20,11 +20,8 @@ const NAME_MAX_BYTES = 255;
|
|||||||
|
|
||||||
const EDITORCONFIG_NAME = ".editorconfig";
|
const EDITORCONFIG_NAME = ".editorconfig";
|
||||||
|
|
||||||
let defaultTabWidth = DEFAULT_TAB_WIDTH;
|
|
||||||
|
|
||||||
const editorConfigCache = new Map<string, ParsedEditorConfig | null>();
|
const editorConfigCache = new Map<string, ParsedEditorConfig | null>();
|
||||||
const editorConfigChainCache = new Map<string, ChainEntry[]>();
|
const editorConfigChainCache = new Map<string, ChainEntry[]>();
|
||||||
const indentationCache = new Map<string, number>();
|
|
||||||
|
|
||||||
interface EditorConfigSection {
|
interface EditorConfigSection {
|
||||||
pattern: string;
|
pattern: string;
|
||||||
@@ -266,31 +263,6 @@ function resolveEditorConfigMatch(absoluteFile: string): EditorConfigMatch | und
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEditorConfigTabWidth(match: EditorConfigMatch | undefined, fallback: number): number | undefined {
|
|
||||||
if (match === undefined) return undefined;
|
|
||||||
|
|
||||||
if (match.indentSize?.kind === "spaces") {
|
|
||||||
return match.indentSize.n;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.indentSize?.kind === "tab") {
|
|
||||||
if (match.tabWidth !== undefined) {
|
|
||||||
return match.tabWidth;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.tabWidth !== undefined) {
|
|
||||||
return match.tabWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.indentStyle === IndentStyle.Tab) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasOverlongPathComponent(filePath: string): boolean {
|
function hasOverlongPathComponent(filePath: string): boolean {
|
||||||
for (const part of filePath.split(/[\\/]/)) {
|
for (const part of filePath.split(/[\\/]/)) {
|
||||||
if (part.length > 0 && Buffer.byteLength(part) > NAME_MAX_BYTES) {
|
if (part.length > 0 && Buffer.byteLength(part) > NAME_MAX_BYTES) {
|
||||||
@@ -300,53 +272,12 @@ function hasOverlongPathComponent(filePath: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultTabWidth(): number {
|
|
||||||
return defaultTabWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDefaultTabWidth(width: number): void {
|
|
||||||
defaultTabWidth = clampTabWidth(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visible tab width in columns for `file` (from `.editorconfig` + default), or the default when `file` is omitted.
|
|
||||||
*/
|
|
||||||
export function getIndentation(file?: string | null, projectDir?: string | null): number {
|
|
||||||
const fallback = defaultTabWidth;
|
|
||||||
if (file === undefined || file === null || file === "") {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cwd = projectDir ?? process.cwd();
|
|
||||||
const absoluteFile = resolveFilePath(cwd, file);
|
|
||||||
|
|
||||||
// Renderers can hand us arbitrary strings (e.g. a malformed edit tool
|
|
||||||
// call whose `file_path` is gibberish). Reject paths whose normalized
|
|
||||||
// absolute form still has any component longer than `NAME_MAX_BYTES` —
|
|
||||||
// the editorconfig chain would only trip `ENAMETOOLONG` from
|
|
||||||
// `readFileSync` and escape.
|
|
||||||
if (hasOverlongPathComponent(absoluteFile)) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
const absKey = absoluteFile;
|
|
||||||
const cached = indentationCache.get(absKey);
|
|
||||||
if (cached !== undefined) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorMatch = resolveEditorConfigMatch(absoluteFile);
|
|
||||||
const resolved = resolveEditorConfigTabWidth(editorMatch, fallback) ?? fallback;
|
|
||||||
const clamped = clampTabWidth(resolved);
|
|
||||||
indentationCache.set(absKey, clamped);
|
|
||||||
return clamped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `.editorconfig`-derived formatting options for an LSP `textDocument/formatting` request.
|
* `.editorconfig`-derived formatting options for an LSP `textDocument/formatting` request.
|
||||||
*
|
*
|
||||||
* Both fields are absent when the resolved `.editorconfig` chain does not pin them, so callers
|
* Both fields are absent when the resolved `.editorconfig` chain does not pin them, so callers
|
||||||
* can layer their own fallbacks (content sniffing, project defaults) underneath. Returned values
|
* can layer their own fallbacks (content sniffing, project defaults) underneath. Returned values
|
||||||
* are clamped to {@link MIN_TAB_WIDTH}..{@link MAX_TAB_WIDTH} for parity with {@link getIndentation}.
|
* are clamped to {@link MIN_TAB_WIDTH}..{@link MAX_TAB_WIDTH}.
|
||||||
*/
|
*/
|
||||||
export interface EditorConfigFormatting {
|
export interface EditorConfigFormatting {
|
||||||
/** Effective indent width in columns, from `indent_size` or `tab_width`. */
|
/** Effective indent width in columns, from `indent_size` or `tab_width`. */
|
||||||
@@ -359,8 +290,8 @@ export interface EditorConfigFormatting {
|
|||||||
* Resolve `.editorconfig` formatting hints for `file` without falling back to any default.
|
* Resolve `.editorconfig` formatting hints for `file` without falling back to any default.
|
||||||
*
|
*
|
||||||
* Used by the LSP format-on-write path so a missing `.editorconfig` declaration falls through
|
* Used by the LSP format-on-write path so a missing `.editorconfig` declaration falls through
|
||||||
* to caller-provided defaults instead of clobbering the file with the renderer's display
|
* to caller-provided defaults instead of clobbering the file with the renderer's
|
||||||
* `defaultTabWidth` (issue #2329).
|
* display tab width (issue #2329).
|
||||||
*/
|
*/
|
||||||
export function getEditorConfigFormatting(file?: string | null, projectDir?: string | null): EditorConfigFormatting {
|
export function getEditorConfigFormatting(file?: string | null, projectDir?: string | null): EditorConfigFormatting {
|
||||||
if (file === undefined || file === null || file === "") {
|
if (file === undefined || file === null || file === "") {
|
||||||
@@ -370,7 +301,7 @@ export function getEditorConfigFormatting(file?: string | null, projectDir?: str
|
|||||||
const cwd = projectDir ?? process.cwd();
|
const cwd = projectDir ?? process.cwd();
|
||||||
const absoluteFile = resolveFilePath(cwd, file);
|
const absoluteFile = resolveFilePath(cwd, file);
|
||||||
|
|
||||||
// Same NAME_MAX guard as `getIndentation`: editorconfig discovery is
|
// NAME_MAX guard: editorconfig discovery is
|
||||||
// best-effort and must never escape as `ENAMETOOLONG` from a renderer's
|
// best-effort and must never escape as `ENAMETOOLONG` from a renderer's
|
||||||
// stray gibberish path.
|
// stray gibberish path.
|
||||||
if (hasOverlongPathComponent(absoluteFile)) {
|
if (hasOverlongPathComponent(absoluteFile)) {
|
||||||
|
|||||||
@@ -4,96 +4,7 @@ import * as os from "node:os";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
import { getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
||||||
import { Snowflake } from "@oh-my-pi/pi-utils/snowflake";
|
import { Snowflake } from "@oh-my-pi/pi-utils/snowflake";
|
||||||
import { getEditorConfigFormatting, getIndentation, setDefaultTabWidth } from "@oh-my-pi/pi-utils/tab-spacing";
|
import { getEditorConfigFormatting } from "@oh-my-pi/pi-utils/tab-spacing";
|
||||||
|
|
||||||
describe("spacing", () => {
|
|
||||||
let tempDir = "";
|
|
||||||
let previousProjectDir = "";
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
previousProjectDir = getProjectDir();
|
|
||||||
tempDir = path.join(os.tmpdir(), "pi-utils-spacing", Snowflake.next());
|
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
|
||||||
setProjectDir(tempDir);
|
|
||||||
setDefaultTabWidth(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
setDefaultTabWidth(3);
|
|
||||||
setProjectDir(previousProjectDir);
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves editorconfig rules for file path and falls back to default", async () => {
|
|
||||||
const filePath = path.join(tempDir, "src", "feature.ts");
|
|
||||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(tempDir, ".editorconfig"),
|
|
||||||
["root = true", "", "[*]", "indent_size = 2", "", "[*.md]", "indent_size = 4"].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(" ".repeat(getIndentation(filePath))).toBe(" ");
|
|
||||||
expect(" ".repeat(getIndentation(path.join(tempDir, "README.md")))).toBe(" ");
|
|
||||||
expect(" ".repeat(getIndentation(path.join(tempDir, "missing.txt")))).toBe(" ");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges nested editorconfig files from root to leaf", async () => {
|
|
||||||
const nestedDir = path.join(tempDir, "packages", "feature");
|
|
||||||
const filePath = path.join(nestedDir, "index.ts");
|
|
||||||
await fs.mkdir(nestedDir, { recursive: true });
|
|
||||||
|
|
||||||
await fs.writeFile(path.join(tempDir, ".editorconfig"), ["root = true", "", "[*]", "indent_size = 2"].join("\n"));
|
|
||||||
await fs.writeFile(path.join(tempDir, "packages", ".editorconfig"), ["[*.ts]", "indent_size = 6"].join("\n"));
|
|
||||||
|
|
||||||
expect(" ".repeat(getIndentation(filePath))).toBe(" ");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not throw when the path's segment exceeds NAME_MAX (#1871)", () => {
|
|
||||||
// A garbage path segment (e.g. 2KiB of garbage Unicode produced by a
|
|
||||||
// hallucinating model) makes `fs.readFileSync` reject with
|
|
||||||
// ENAMETOOLONG. The editorconfig probe MUST swallow it and fall back to
|
|
||||||
// the default tab width — anything else crashes the TUI mid-render.
|
|
||||||
const huge = "a".repeat(2048);
|
|
||||||
const phonyPath = path.join(tempDir, huge, "leaf.ts");
|
|
||||||
expect(() => getIndentation(phonyPath)).not.toThrow();
|
|
||||||
expect(getIndentation(phonyPath)).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns the default tab width for paths with an overlong component (no syscall)", () => {
|
|
||||||
// Repro of #1872: a malformed edit tool call lands a long gibberish
|
|
||||||
// string in `file_path`, the renderer routes it through `replaceTabs ->
|
|
||||||
// getIndentation`, and `readFileSync` of `<dir>/.editorconfig` would
|
|
||||||
// throw `ENAMETOOLONG`. The path gate must short-circuit before any
|
|
||||||
// syscall so renderers never see the exception.
|
|
||||||
const longSegment = "amálpthgadasJennzier".repeat(40);
|
|
||||||
const overlong = `${longSegment}/inner.ts`;
|
|
||||||
expect(Buffer.byteLength(longSegment)).toBeGreaterThan(255);
|
|
||||||
expect(() => getIndentation(overlong)).not.toThrow();
|
|
||||||
expect(getIndentation(overlong)).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("normalizes paths before rejecting overlong components", async () => {
|
|
||||||
const longSegment = "amálpthgadasJennzier".repeat(40);
|
|
||||||
const noisyPath = path.join(tempDir, longSegment, "..", "src", "feature.ts");
|
|
||||||
await fs.writeFile(path.join(tempDir, ".editorconfig"), ["root = true", "", "[*]", "indent_size = 2"].join("\n"));
|
|
||||||
|
|
||||||
expect(Buffer.byteLength(longSegment)).toBeGreaterThan(255);
|
|
||||||
expect(" ".repeat(getIndentation(noisyPath))).toBe(" ");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tolerates filesystem errors while walking the editorconfig chain (ENOTDIR)", async () => {
|
|
||||||
// Defense in depth: when a non-directory sits where a directory is
|
|
||||||
// expected, `parseCachedEditorConfig` previously caught only `ENOENT`
|
|
||||||
// and let `ENOTDIR` (and `ENAMETOOLONG`, `EACCES`, `ELOOP`, …)
|
|
||||||
// escape. Editorconfig discovery is best-effort and must absorb any
|
|
||||||
// `FsError`.
|
|
||||||
const notADir = path.join(tempDir, "not-a-dir");
|
|
||||||
await fs.writeFile(notADir, "");
|
|
||||||
const fakeChild = path.join(notADir, "inner.ts");
|
|
||||||
expect(() => getIndentation(fakeChild)).not.toThrow();
|
|
||||||
expect(getIndentation(fakeChild)).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getEditorConfigFormatting", () => {
|
describe("getEditorConfigFormatting", () => {
|
||||||
let tempDir = "";
|
let tempDir = "";
|
||||||
@@ -176,4 +87,12 @@ describe("getEditorConfigFormatting", () => {
|
|||||||
expect(() => getEditorConfigFormatting(overlong)).not.toThrow();
|
expect(() => getEditorConfigFormatting(overlong)).not.toThrow();
|
||||||
expect(getEditorConfigFormatting(overlong)).toEqual({});
|
expect(getEditorConfigFormatting(overlong)).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("tolerates filesystem errors while walking the editorconfig chain (ENOTDIR)", async () => {
|
||||||
|
const notADir = path.join(tempDir, "not-a-dir");
|
||||||
|
await fs.writeFile(notADir, "");
|
||||||
|
const fakeChild = path.join(notADir, "inner.ts");
|
||||||
|
expect(() => getEditorConfigFormatting(fakeChild)).not.toThrow();
|
||||||
|
expect(getEditorConfigFormatting(fakeChild)).toEqual({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ const codingAgentSingletonContentMarkers = [
|
|||||||
"Settings.instance",
|
"Settings.instance",
|
||||||
"resetSettingsForTest",
|
"resetSettingsForTest",
|
||||||
"setAgentDir(",
|
"setAgentDir(",
|
||||||
"setDefaultTabWidth(",
|
|
||||||
"vi.useFakeTimers(",
|
"vi.useFakeTimers(",
|
||||||
"vi.useRealTimers(",
|
"vi.useRealTimers(",
|
||||||
"vi.stubEnv(",
|
"vi.stubEnv(",
|
||||||
|
|||||||
Reference in New Issue
Block a user