refactor(coding-agent): flattened directory structure, eliminated core/ folder

- Eliminated core/ directory (252 files, 23 subdirs → distributed)
- Reduced max nesting from 9 levels to 5 levels
- Promoted tool subdirs to top level: exa/, lsp/, patch/, task/, web/
- Merged web-scrapers/ + web-search/ into web/{scrapers,search}/
- Flattened modes/interactive/ to modes/
- Split execution/ into: ipy/ (python), exec/ (bash), ssh/
- Renamed ipy/python-*.ts to ipy/*.ts (executor, kernel, etc.)
- Flattened cursor/exec-bridge.ts to cursor.ts
- Created logical groupings: config/, session/, extensibility/, export/
- Updated all imports across 500+ files
This commit is contained in:
can1357
2026-01-23 12:24:47 +01:00
parent 7b5af2dd9f
commit cb5bbbf382
531 changed files with 1316 additions and 1322 deletions
@@ -0,0 +1,395 @@
/**
* Text normalization utilities for the edit tool.
*
* Handles line endings, BOM, whitespace, and Unicode normalization.
*/
// ═══════════════════════════════════════════════════════════════════════════
// Line Ending Utilities
// ═══════════════════════════════════════════════════════════════════════════
export type LineEnding = "\r\n" | "\n";
/** Detect the predominant line ending in content */
export function detectLineEnding(content: string): LineEnding {
const crlfIdx = content.indexOf("\r\n");
const lfIdx = content.indexOf("\n");
if (lfIdx === -1) return "\n";
if (crlfIdx === -1) return "\n";
return crlfIdx < lfIdx ? "\r\n" : "\n";
}
/** Normalize all line endings to LF */
export function normalizeToLF(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
/** Restore line endings to the specified type */
export function restoreLineEndings(text: string, ending: LineEnding): string {
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
}
// ═══════════════════════════════════════════════════════════════════════════
// BOM Handling
// ═══════════════════════════════════════════════════════════════════════════
export interface BomResult {
/** The BOM character if present, empty string otherwise */
bom: string;
/** The text without the BOM */
text: string;
}
/** Strip UTF-8 BOM if present */
export function stripBom(content: string): BomResult {
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
}
// ═══════════════════════════════════════════════════════════════════════════
// Whitespace Utilities
// ═══════════════════════════════════════════════════════════════════════════
/** Count leading whitespace characters in a line */
export function countLeadingWhitespace(line: string): number {
let count = 0;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === " " || char === "\t") {
count++;
} else {
break;
}
}
return count;
}
/** Get the leading whitespace string from a line */
export function getLeadingWhitespace(line: string): string {
return line.slice(0, countLeadingWhitespace(line));
}
/** Compute minimum indentation of non-empty lines */
export function minIndent(text: string): number {
const lines = text.split("\n");
let min = Infinity;
for (const line of lines) {
if (line.trim().length > 0) {
min = Math.min(min, countLeadingWhitespace(line));
}
}
return min === Infinity ? 0 : min;
}
/** Detect the indentation character used in text (space or tab) */
export function detectIndentChar(text: string): string {
const lines = text.split("\n");
for (const line of lines) {
const ws = getLeadingWhitespace(line);
if (ws.length > 0) {
return ws[0];
}
}
return " ";
}
function gcd(a: number, b: number): number {
let x = Math.abs(a);
let y = Math.abs(b);
while (y !== 0) {
const temp = y;
y = x % y;
x = temp;
}
return x;
}
interface IndentProfile {
lines: string[];
indentStrings: string[];
indentCounts: number[];
min: number;
char: " " | "\t" | undefined;
spaceOnly: boolean;
tabOnly: boolean;
mixed: boolean;
unit: number;
nonEmptyCount: number;
}
function buildIndentProfile(text: string): IndentProfile {
const lines = text.split("\n");
const indentStrings: string[] = [];
const indentCounts: number[] = [];
let min = Infinity;
let char: " " | "\t" | undefined;
let spaceOnly = true;
let tabOnly = true;
let mixed = false;
let nonEmptyCount = 0;
let unit = 0;
for (const line of lines) {
if (line.trim().length === 0) continue;
nonEmptyCount++;
const indent = getLeadingWhitespace(line);
indentStrings.push(indent);
indentCounts.push(indent.length);
min = Math.min(min, indent.length);
if (indent.includes(" ")) {
tabOnly = false;
}
if (indent.includes("\t")) {
spaceOnly = false;
}
if (indent.includes(" ") && indent.includes("\t")) {
mixed = true;
}
if (indent.length > 0) {
const currentChar = indent[0] as " " | "\t";
if (!char) {
char = currentChar;
} else if (char !== currentChar) {
mixed = true;
}
}
}
if (min === Infinity) {
min = 0;
}
if (spaceOnly && nonEmptyCount > 0) {
let current = 0;
for (const count of indentCounts) {
if (count === 0) continue;
current = current === 0 ? count : gcd(current, count);
}
unit = current;
}
if (tabOnly && nonEmptyCount > 0) {
unit = 1;
}
return {
lines,
indentStrings,
indentCounts,
min,
char,
spaceOnly,
tabOnly,
mixed,
unit,
nonEmptyCount,
};
}
export function convertLeadingTabsToSpaces(text: string, spacesPerTab: number): string {
if (spacesPerTab <= 0) return text;
return text
.split("\n")
.map((line) => {
const trimmed = line.trimStart();
if (trimmed.length === 0) return line;
const leading = getLeadingWhitespace(line);
if (!leading.includes("\t") || leading.includes(" ")) return line;
const converted = " ".repeat(leading.length * spacesPerTab);
return converted + trimmed;
})
.join("\n");
}
// ═══════════════════════════════════════════════════════════════════════════
// Unicode Normalization
// ═══════════════════════════════════════════════════════════════════════════
/**
* Normalize common Unicode punctuation to ASCII equivalents.
* Allows diffs with ASCII characters to match source files with typographic punctuation.
*/
export function normalizeUnicode(s: string): string {
return s
.trim()
.split("")
.map((c) => {
const code = c.charCodeAt(0);
// Various dash/hyphen code-points → ASCII '-'
if (
code === 0x2010 || // HYPHEN
code === 0x2011 || // NON-BREAKING HYPHEN
code === 0x2012 || // FIGURE DASH
code === 0x2013 || // EN DASH
code === 0x2014 || // EM DASH
code === 0x2015 || // HORIZONTAL BAR
code === 0x2212 // MINUS SIGN
) {
return "-";
}
// Fancy single quotes → '
if (
code === 0x2018 || // LEFT SINGLE QUOTATION MARK
code === 0x2019 || // RIGHT SINGLE QUOTATION MARK
code === 0x201a || // SINGLE LOW-9 QUOTATION MARK
code === 0x201b // SINGLE HIGH-REVERSED-9 QUOTATION MARK
) {
return "'";
}
// Fancy double quotes → "
if (
code === 0x201c || // LEFT DOUBLE QUOTATION MARK
code === 0x201d || // RIGHT DOUBLE QUOTATION MARK
code === 0x201e || // DOUBLE LOW-9 QUOTATION MARK
code === 0x201f // DOUBLE HIGH-REVERSED-9 QUOTATION MARK
) {
return '"';
}
// Non-breaking space and other odd spaces → normal space
if (
code === 0x00a0 || // NO-BREAK SPACE
code === 0x2002 || // EN SPACE
code === 0x2003 || // EM SPACE
code === 0x2004 || // THREE-PER-EM SPACE
code === 0x2005 || // FOUR-PER-EM SPACE
code === 0x2006 || // SIX-PER-EM SPACE
code === 0x2007 || // FIGURE SPACE
code === 0x2008 || // PUNCTUATION SPACE
code === 0x2009 || // THIN SPACE
code === 0x200a || // HAIR SPACE
code === 0x202f || // NARROW NO-BREAK SPACE
code === 0x205f || // MEDIUM MATHEMATICAL SPACE
code === 0x3000 // IDEOGRAPHIC SPACE
) {
return " ";
}
return c;
})
.join("");
}
/**
* Normalize a line for fuzzy comparison.
* Trims, collapses whitespace, and normalizes punctuation.
*/
export function normalizeForFuzzy(line: string): string {
const trimmed = line.trim();
if (trimmed.length === 0) return "";
return trimmed
.replace(/[""„‟«»]/g, '"')
.replace(/[''‚‛`´]/g, "'")
.replace(/[‐‑‒–—−]/g, "-")
.replace(/[ \t]+/g, " ");
}
// ═══════════════════════════════════════════════════════════════════════════
// Indentation Adjustment
// ═══════════════════════════════════════════════════════════════════════════
/**
* Adjust newText indentation to match the indentation delta between
* what was provided (oldText) and what was actually matched (actualText).
*
* If oldText has 0 indent but actualText has 12 spaces, we add 12 spaces
* to each line in newText.
*/
export function adjustIndentation(oldText: string, actualText: string, newText: string): string {
// If old text already matches actual text exactly, preserve agent's intended indentation
if (oldText === actualText) {
return newText;
}
// If the patch is purely an indentation change (same trimmed content), apply exactly as specified
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
if (oldLines.length === newLines.length) {
let indentationOnly = true;
for (let i = 0; i < oldLines.length; i++) {
if (oldLines[i].trim() !== newLines[i].trim()) {
indentationOnly = false;
break;
}
}
if (indentationOnly) {
return newText;
}
}
const oldProfile = buildIndentProfile(oldText);
const actualProfile = buildIndentProfile(actualText);
const newProfile = buildIndentProfile(newText);
if (newProfile.nonEmptyCount === 0 || oldProfile.nonEmptyCount === 0 || actualProfile.nonEmptyCount === 0) {
return newText;
}
if (oldProfile.mixed || actualProfile.mixed || newProfile.mixed) {
return newText;
}
if (oldProfile.char && actualProfile.char && oldProfile.char !== actualProfile.char) {
if (actualProfile.spaceOnly && oldProfile.tabOnly && newProfile.tabOnly && actualProfile.unit > 0) {
let consistent = true;
const lineCount = Math.min(oldProfile.lines.length, actualProfile.lines.length);
for (let i = 0; i < lineCount; i++) {
const oldLine = oldProfile.lines[i];
const actualLine = actualProfile.lines[i];
if (oldLine.trim().length === 0 || actualLine.trim().length === 0) continue;
const oldIndent = getLeadingWhitespace(oldLine);
const actualIndent = getLeadingWhitespace(actualLine);
if (oldIndent.length === 0) continue;
if (actualIndent.length !== oldIndent.length * actualProfile.unit) {
consistent = false;
break;
}
}
return consistent ? convertLeadingTabsToSpaces(newText, actualProfile.unit) : newText;
}
return newText;
}
const lineCount = Math.min(oldProfile.lines.length, actualProfile.lines.length);
const deltas: number[] = [];
for (let i = 0; i < lineCount; i++) {
const oldLine = oldProfile.lines[i];
const actualLine = actualProfile.lines[i];
if (oldLine.trim().length === 0 || actualLine.trim().length === 0) continue;
deltas.push(countLeadingWhitespace(actualLine) - countLeadingWhitespace(oldLine));
}
if (deltas.length === 0) {
return newText;
}
const delta = deltas[0];
if (!deltas.every((value) => value === delta)) {
return newText;
}
if (delta === 0) {
return newText;
}
if (newProfile.char && actualProfile.char && newProfile.char !== actualProfile.char) {
return newText;
}
const indentChar = actualProfile.char ?? oldProfile.char ?? detectIndentChar(actualText);
const adjusted = newText.split("\n").map((line) => {
if (line.trim().length === 0) {
return line;
}
if (delta > 0) {
return indentChar.repeat(delta) + line;
}
const toRemove = Math.min(-delta, countLeadingWhitespace(line));
return line.slice(toRemove);
});
return adjusted.join("\n");
}