Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions ide/electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ ipcMain.handle("schutz:replaceInFiles", async (_e, root, query, replacement, opt
const inc = globToMatcher(opts?.include);
const exc = globToMatcher(opts?.exclude);
let changed = 0, files = 0;
const skipped = []; // UTF-8 이 아니라 건드리지 않은 파일
async function walk(dirAbs, relBase, depth) {
if (depth > MAX_DEPTH) return;
let items;
Expand All @@ -850,8 +851,14 @@ ipcMain.handle("schutz:replaceInFiles", async (_e, root, query, replacement, opt
const abs = path.join(dirAbs, it.name);
let st; try { st = await fs.stat(abs); } catch { continue; }
if (st.size > MAX_FILE_BYTES) continue;
let text; try { text = await fs.readFile(abs, "utf8"); } catch { continue; }
if (text.indexOf(String.fromCharCode(0)) >= 0) continue;
let buf; try { buf = await fs.readFile(abs); } catch { continue; }
// UTF-8 이 아니면 손대지 않는다. 여기가 readFile 보다 위험하다 — 사용자가 열어
// 보지도 않은 파일을 훑으며 쓰기 때문에, CP949 로 된 파일 하나가 치환 한 번에
// 조용히 파괴됐다(26바이트 → 39바이트, 한글이 전부 U+FFFD 로). 성공 토스트까지
// 떴다. 무엇을 건너뛰었는지는 아래에서 돌려준다 — 조용히 빼면 "다 됐다" 로 읽힌다.
const kind = encoding.detect(buf);
if (kind) { skipped.push(rel); continue; }
const text = buf.toString("utf8");
re.lastIndex = 0;
const matches = text.match(re);
if (!matches || !matches.length) continue;
Expand All @@ -864,9 +871,9 @@ ipcMain.handle("schutz:replaceInFiles", async (_e, root, query, replacement, opt
}
// walk 가 중간에 죽으면 부분 결과가 남는다 — 성공인 척하지 말고 어디까지 됐는지 알린다
try { await walk(root, "", 0); } catch (e) {
return { changed, files, partial: true, error: e && e.message ? e.message : String(e) };
return { changed, files, skipped, partial: true, error: e && e.message ? e.message : String(e) };
}
return { changed, files };
return { changed, files, skipped };
});

// ── 에이전트 명령 실행 ─────────────────────────────────────────────────────
Expand Down
5 changes: 5 additions & 0 deletions ide/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5259,6 +5259,11 @@ ${(r.output || "").slice(0, 2000)}`;
if (r.error) { this.toast("error", t("sc3.replaceFailed") + r.error); return; }
if (r.partial) this.toast("error", t("sc3.replacePartial", { files: r.files, changed: r.changed }));
else this.toast("ok", t("sc3.replaceResult", { files: r.files, changed: r.changed }));
// UTF-8 이 아니라 건너뛴 파일은 반드시 말한다. 안 말하면 "전부 바꿨다" 로 읽히고,
// 그 파일들만 옛 이름이 남아 나중에 빌드가 깨진 뒤에야 알게 된다.
if (r.skipped?.length) {
this.toast("error", t("enc.replaceSkipped", { n: r.skipped.length, files: r.skipped.slice(0, 6).join(", ") }));
}
// 모든 non-dirty owned 모델 재로드 — 열린 탭뿐 아니라 preload(닫힌) 모델도 디스크 반영(#8: 나중에 열면 stale 방지). dirty 는 위에서 차단됨.
void projectModels.reloadAll(ws.root, (r, rel) => window.schutz!.readFile(r, rel), this.isDirtyRel);
this.setState(st => { const pv = { ...st.paneVer }; for (const p of this.allOpen(st)) pv[p] = (pv[p] ?? 0) + 1; return { paneVer: pv }; });
Expand Down
2 changes: 2 additions & 0 deletions ide/src/i18n/dict/enc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
export const dict: Record<string, { ko: string; en: string; de: string; ja: string }> = {
"enc.notUtf8": { ko: "이 파일은 UTF-8 이 아닙니다. 열면 글자가 깨지고 저장하면 원본이 사라지므로 열지 않았습니다. UTF-8 로 변환한 뒤 다시 열어 주세요.", en: "This file is not UTF-8. Opening it would garble the text and saving would destroy the original, so it was not opened. Convert it to UTF-8 and try again.", de: "Diese Datei ist nicht UTF-8. Das Öffnen würde den Text zerstören und das Speichern das Original vernichten — sie wurde nicht geöffnet. Bitte in UTF-8 konvertieren.", ja: "このファイルは UTF-8 ではありません。開くと文字化けし、保存すると原本が失われるため開きませんでした。UTF-8 に変換してから開いてください。" },
"enc.utf16": { ko: "이 파일은 UTF-16 입니다. 아직 UTF-8 만 다룰 수 있어 열지 않았습니다 — 열었다면 저장하는 순간 원본이 사라졌을 것입니다.", en: "This file is UTF-16. Only UTF-8 is supported, so it was not opened — saving it would have destroyed the original.", de: "Diese Datei ist UTF-16. Es wird nur UTF-8 unterstützt, daher wurde sie nicht geöffnet — beim Speichern wäre das Original verloren gegangen.", ja: "このファイルは UTF-16 です。現在 UTF-8 のみ対応のため開きませんでした — 保存していれば原本が失われていました。" },
// 찾아 바꾸기가 건너뛴 파일 — 조용히 빼면 "전부 바꿨다" 로 읽힌다.
"enc.replaceSkipped": { ko: "UTF-8 이 아니라 건드리지 않은 파일 {n}개: {files}", en: "{n} file(s) left untouched because they are not UTF-8: {files}", de: "{n} Datei(en) unberührt gelassen, da nicht UTF-8: {files}", ja: "UTF-8 ではないため触れなかったファイル {n} 件: {files}" },
"enc.binary": { ko: "텍스트 파일이 아닙니다(NUL 바이트가 있습니다). 편집기로 열면 내용이 망가집니다.", en: "This is not a text file (it contains NUL bytes). Opening it in the editor would corrupt it.", de: "Das ist keine Textdatei (sie enthält NUL-Bytes). Im Editor zu öffnen würde sie beschädigen.", ja: "テキストファイルではありません(NUL バイトがあります)。エディターで開くと壊れます。" },
};
5 changes: 3 additions & 2 deletions ide/src/schutz.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ interface SchutzApi {
Promise<{ ok: boolean; quota?: QuotaInfo; error?: string }>;
onQuota(cb: (line: string) => void): () => void;

/** error=정규식 거부 등으로 아무것도 안 함 · partial=도중 실패해 일부만 적용됨 */
replaceInFiles(root: string, query: string, replacement: string, opts?: any): Promise<{ changed: number; files: number; error?: string; partial?: boolean }>;
/** error=정규식 거부 등으로 아무것도 안 함 · partial=도중 실패해 일부만 적용됨
* skipped=UTF-8 이 아니라 건드리지 않은 파일(쓰면 원본이 파괴된다) */
replaceInFiles(root: string, query: string, replacement: string, opts?: any): Promise<{ changed: number; files: number; error?: string; partial?: boolean; skipped?: string[] }>;
cliCheck(): Promise<{ agents: Record<string, { ok: boolean; version: string; hasConfig: boolean }> }>;
agentCommands(root: string | null): Promise<{ commands: { name: string; origin: "claude" | "codex"; scope: "user" | "project"; description: string; argHint: string; body: string }[] }>;
cliChatCounts(): Promise<{ counts: Record<string, number> }>;
Expand Down
Loading