Skip to content

Commit 1d24533

Browse files
koki-developclaude
andcommitted
feat: Add EditAction component and FileEditor class
- Create EditAction component for displaying file edit diffs - Add FileEditor class for cat-like file editing functionality - Implement structured diff generation with Diff[] format - Support git-tracked file selection with fallback to glob search - Add random cat word replacement (up to 3 replacements) - Use ANSI256 colors for optimal diff background visibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 59540d1 commit 1d24533

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

src/components/EditAction.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import chalk from "chalk";
2+
import { Box, Text } from "ink";
3+
import type React from "react";
4+
import type { EditAction as EditActionType } from "./types";
5+
6+
interface EditActionProps {
7+
action: EditActionType;
8+
}
9+
10+
const formatDiffs = (diffs: EditActionType["diff"]["diffs"]): string => {
11+
const lines: string[] = [];
12+
13+
for (let i = 0; i < diffs.length; i++) {
14+
const diff = diffs[i];
15+
const nextDiff = diffs[i + 1];
16+
17+
if (!diff) continue;
18+
19+
// Deleted line
20+
if (diff.a !== "") {
21+
lines.push(
22+
`${chalk.gray(diff.rowNumber)} ${chalk.bgAnsi256(52)(`- ${diff.a}`)}`,
23+
);
24+
}
25+
// Added line
26+
if (diff.b !== "") {
27+
lines.push(
28+
`${chalk.gray(diff.rowNumber)} ${chalk.bgAnsi256(22)(`+ ${diff.b}`)}`,
29+
);
30+
}
31+
32+
// Add empty line if next diff exists and line numbers are not consecutive
33+
if (nextDiff && nextDiff.rowNumber !== diff.rowNumber + 1) {
34+
lines.push("");
35+
}
36+
}
37+
38+
return lines.join("\n");
39+
};
40+
41+
export const EditAction: React.FC<EditActionProps> = ({ action }) => {
42+
return (
43+
<Box flexDirection="column">
44+
<Text>
45+
<Text color="green"></Text> <Text bold>Update</Text>(
46+
{action.diff.fileName})
47+
</Text>
48+
<Box paddingLeft={2} paddingY={1}>
49+
<Text>{formatDiffs(action.diff.diffs)}</Text>
50+
</Box>
51+
</Box>
52+
);
53+
};

src/lib/fileEditor.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { readFile, writeFile } from "node:fs/promises";
2+
import { glob } from "glob";
3+
import type { Diff, FileDiff } from "../components/types";
4+
import { Git } from "./git";
5+
6+
export class FileEditor {
7+
private git = new Git();
8+
// Text file extension patterns
9+
// TODO: enhance these patterns
10+
private readonly textFilePatterns = [
11+
"**/*.txt",
12+
"**/*.md",
13+
"**/*.js",
14+
"**/*.jsx",
15+
"**/*.ts",
16+
"**/*.tsx",
17+
"**/*.json",
18+
"**/*.css",
19+
"**/*.html",
20+
"**/*.xml",
21+
"**/*.yml",
22+
"**/*.yaml",
23+
];
24+
25+
// Cat word candidates
26+
private readonly catWords = [
27+
"ニャー",
28+
"ミャー",
29+
"ニャン",
30+
"ミャン",
31+
"ニャッ",
32+
"ミャッ",
33+
"ニャオ~ン",
34+
"ミャオ~",
35+
"ゴロゴロ",
36+
];
37+
38+
/**
39+
* Get files under git management (when git is initialized)
40+
*/
41+
private async getGitTrackedFiles(): Promise<string[]> {
42+
const gitFiles = await this.git.lsFiles();
43+
return gitFiles.filter((file) => this.isTextFile(file));
44+
}
45+
46+
/**
47+
* Search for text files (fallback when git is not initialized)
48+
*/
49+
private async getTextFiles(): Promise<string[]> {
50+
return await glob(this.textFilePatterns);
51+
}
52+
53+
/**
54+
* Determine if a file is a text file
55+
*/
56+
private isTextFile(filePath: string): boolean {
57+
const extension = filePath.split(".").pop()?.toLowerCase();
58+
const textExtensions = [
59+
"txt",
60+
"md",
61+
"js",
62+
"ts",
63+
"tsx",
64+
"json",
65+
"css",
66+
"html",
67+
"xml",
68+
"yml",
69+
"yaml",
70+
];
71+
return textExtensions.includes(extension || "");
72+
}
73+
74+
/**
75+
* Select a file randomly
76+
*/
77+
async selectRandomFile(): Promise<string | null> {
78+
let files: string[];
79+
80+
// Check if git is initialized
81+
if (await this.git.isInited()) {
82+
files = await this.getGitTrackedFiles();
83+
} else {
84+
files = await this.getTextFiles();
85+
}
86+
87+
if (files.length === 0) {
88+
return null;
89+
}
90+
91+
const randomIndex = Math.floor(Math.random() * files.length);
92+
return files[randomIndex] as string;
93+
}
94+
95+
/**
96+
* Edit file content in a cat-like manner
97+
*/
98+
private editFileContent(content: string): string {
99+
const words = content.match(/\w+/g);
100+
if (!words || words.length === 0) return content;
101+
102+
const targetWord = words[
103+
Math.floor(Math.random() * words.length)
104+
] as string;
105+
const catWord = this.catWords[
106+
Math.floor(Math.random() * this.catWords.length)
107+
] as string;
108+
109+
// Replace up to 3 locations
110+
let replacedContent = content;
111+
let replacementCount = 0;
112+
const maxReplacements = 3;
113+
114+
replacedContent = replacedContent.replace(
115+
new RegExp(`\\b${targetWord}\\b`, "g"),
116+
(match) => {
117+
if (replacementCount < maxReplacements) {
118+
replacementCount++;
119+
return catWord;
120+
}
121+
return match;
122+
},
123+
);
124+
125+
return replacedContent;
126+
}
127+
128+
/**
129+
* Generate structured diff
130+
*/
131+
private generateDiffs(original: string, edited: string): Diff[] {
132+
const originalLines = original.split("\n");
133+
const editedLines = edited.split("\n");
134+
const diffs: Diff[] = [];
135+
136+
const maxLines = Math.max(originalLines.length, editedLines.length);
137+
138+
for (let i = 0; i < maxLines; i++) {
139+
const rowNumber = i + 1;
140+
const originalLine = originalLines[i] || "";
141+
const editedLine = editedLines[i] || "";
142+
143+
if (originalLine !== editedLine) {
144+
diffs.push({
145+
rowNumber,
146+
a: originalLine,
147+
b: editedLine,
148+
});
149+
}
150+
}
151+
152+
return diffs;
153+
}
154+
155+
/**
156+
* Edit file and generate diff
157+
*/
158+
async edit(): Promise<FileDiff | null> {
159+
const filePath = await this.selectRandomFile();
160+
if (!filePath) {
161+
return null;
162+
}
163+
164+
// Read file content
165+
const originalContent = await readFile(filePath, "utf-8");
166+
167+
// Edit
168+
const editedContent = this.editFileContent(originalContent);
169+
170+
// Write to file
171+
await writeFile(filePath, editedContent, "utf-8");
172+
173+
// Generate structured diff
174+
const diffs = this.generateDiffs(originalContent, editedContent);
175+
if (diffs.length === 0) {
176+
return null;
177+
}
178+
179+
return {
180+
fileName: filePath,
181+
diffs,
182+
};
183+
}
184+
}

0 commit comments

Comments
 (0)