diff --git a/doc/진행상황.md b/doc/진행상황.md
index 82b8965..b5a88b5 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -55,14 +55,14 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
되돌릴 수 없는 일이라 화면 없이 검사할 수 있어야 하기 때문이다 - 겹치는 파일은 덮고,
대상에만 있던 파일은 남기고, 두 번 복사해도 같은지를 테스트로 못 박았다.
-### ② 파일 비교
+### ② 파일 비교 (2026-09-08)
-| 상태 | 항목 | 내용 |
-|---|---|---|
-| 대기 | 바꾸기 (Ctrl+H) | 찾기만 있다 |
-| 대기 | 덩어리 단위 반영 | 줄 단위 아니면 전체뿐. 연속 5줄이면 5번 누른다 |
-| 대기 | 인코딩·줄바꿈 변환 저장 | CP949 ↔ UTF-8. 지금은 읽어서 보여주기만 |
-| 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 |
+| 상태 | 항목 | 내용 | 확인 |
+|---|---|---|---|
+| **완료** | 바꾸기 (Ctrl+H) | 찾기 칸 아래에 바꿀 말 줄이 붙는다(Ctrl+F 로 열면 안 뜬다 - 찾기만 할 때 자리를 뺏지 않는다). `바꾸기` 는 고른 자리가 찾는 말일 때만 바꾸고 다음으로 넘어간다 - 엉뚱한 자리를 덮어쓰는 것보다 한 번 더 누르게 하는 편이 낫다 | `PulseUs → PulseMicros` 2개 바꿈 |
+| **완료** | 덩어리 단위 반영 | `이 덩어리 반영`(`Alt+Shift+→`). 차이로 잡힌 줄이 이어지는 구간을 한 번에 | 마지막 차이에서 2줄 한 번에 반영 |
+| **완료** | 인코딩·줄바꿈 변환 저장 | ACTIONS 의 `저장 형식` 두 칸. 기본 "원본 유지"라 지금까지와 똑같이 나간다. UTF-8 / UTF-8(BOM) / CP949 · CRLF / LF | 화면 확인 |
+| 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 | |
---
diff --git a/src/MergeHub/Services/ShortcutMap.cs b/src/MergeHub/Services/ShortcutMap.cs
index 9400bed..fae5dd4 100644
--- a/src/MergeHub/Services/ShortcutMap.cs
+++ b/src/MergeHub/Services/ShortcutMap.cs
@@ -21,11 +21,13 @@ namespace MergeHub.Services
GoToLine,
NextBookmark,
ToggleBookmark,
+ AcceptBlock,
NextConflict,
PrevConflict,
AutoMerge,
OpenFolderCompare,
Find,
+ Replace,
FindNext,
FindPrev,
CloseFind,
@@ -118,6 +120,9 @@ namespace MergeHub.Services
"이전 충돌 (3-Way)"),
new ShortcutEntry(ShortcutAction.AutoMerge, Key.M, ModifierKeys.Alt, "병합·편집",
"자동 병합 - 저쪽만 고친 줄을 한 번에 가져온다 (3-Way)"),
+ new ShortcutEntry(ShortcutAction.AcceptBlock, Key.Right, ModifierKeys.Alt | ModifierKeys.Shift, "병합·편집",
+ "이 덩어리 반영 - 차이가 이어지는 구간을 한 번에",
+ "줄 하나씩만 되면 연속 다섯 줄을 다섯 번 눌러야 한다."),
new ShortcutEntry(ShortcutAction.Undo, Key.Z, ModifierKeys.Control, "병합·편집", "실행 취소",
"머지 반영을 되돌린다. 되돌릴 머지 작업이 없으면 편집기의 글자 되돌리기가 그대로 동작한다."),
@@ -135,6 +140,7 @@ namespace MergeHub.Services
new ShortcutEntry(ShortcutAction.Save, Key.S, ModifierKeys.Control, "파일·보기", "저장"),
new ShortcutEntry(ShortcutAction.Find, Key.F, ModifierKeys.Control, "찾기", "텍스트 찾기"),
+ new ShortcutEntry(ShortcutAction.Replace, Key.H, ModifierKeys.Control, "찾기", "바꾸기 (찾기 칸이 열리며 바꿀 말 칸이 함께 뜬다)"),
new ShortcutEntry(ShortcutAction.FindNext, Key.F3, ModifierKeys.None, "찾기", "다음 찾기"),
new ShortcutEntry(ShortcutAction.FindPrev, Key.F3, ModifierKeys.Shift, "찾기", "이전 찾기"),
new ShortcutEntry(ShortcutAction.CloseFind, Key.Escape, ModifierKeys.None, "찾기", "찾기 칸 닫기"),
diff --git a/src/MergeHub/Services/TextFile.cs b/src/MergeHub/Services/TextFile.cs
index 53f9bd0..2edd056 100644
--- a/src/MergeHub/Services/TextFile.cs
+++ b/src/MergeHub/Services/TextFile.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -52,24 +53,70 @@ namespace MergeHub.Services
return encoding.GetString(bytes, offset, bytes.Length - offset);
}
+ /// 저장할 때 고를 수 있는 인코딩. 빈 값(원본 유지)이 기본이다.
+ public const string KeepOriginal = "원본 유지";
+
+ public static IReadOnlyList SaveEncodings { get; } =
+ new[] { KeepOriginal, "UTF-8", "UTF-8 (BOM)", "CP949" };
+
+ public static IReadOnlyList SaveNewLines { get; } =
+ new[] { KeepOriginal, "CRLF", "LF" };
+
///
/// 원래 파일의 인코딩과 줄바꿈을 그대로 유지해 덮어쓴다.
/// 새 파일이면 UTF-8(BOM 없음) · CRLF 로 쓴다.
///
- public static void Write(string path, string content)
+ public static void Write(string path, string content) => Write(path, content, null, null);
+
+ ///
+ /// 인코딩·줄바꿈을 바꿔서 저장한다. 둘 다 null 이나 "원본 유지"면 지금까지와 같다.
+ ///
+ /// 사내 소스에는 CP949 로 저장된 파일이 섞여 있어, 새로 만드는 파일과 형식이 갈린다.
+ /// 읽어서 보여 주기만 하면 그 갈림을 못 고치므로 저장할 때 고를 수 있게 한다.
+ ///
+ public static void Write(string path, string content, string? encodingName, string? newLineName)
{
- var encoding = File.Exists(path)
- ? Detect(File.ReadAllBytes(path))
- : new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ var encoding = ResolveEncoding(path, encodingName);
+ var newLine = ResolveNewLine(path, newLineName);
- var newLine = File.Exists(path) ? Describe(path).NewLine : "CRLF";
var text = content.Replace("\r\n", "\n");
-
if (newLine != "LF") text = text.Replace("\n", "\r\n");
File.WriteAllText(path, text, encoding);
}
+ private static Encoding ResolveEncoding(string path, string? name)
+ {
+ switch (name)
+ {
+ case "UTF-8": return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ case "UTF-8 (BOM)": return new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
+ case "CP949": return GetCp949();
+ }
+
+ return File.Exists(path)
+ ? Detect(File.ReadAllBytes(path))
+ : new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ }
+
+ private static Encoding GetCp949()
+ {
+ try
+ {
+ return Encoding.GetEncoding(949);
+ }
+ catch (ArgumentException)
+ {
+ return Ansi; // 코드페이지가 없는 장비면 시스템 기본으로 물러난다
+ }
+ }
+
+ private static string ResolveNewLine(string path, string? name)
+ {
+ if (name == "CRLF" || name == "LF") return name;
+ return File.Exists(path) ? Describe(path).NewLine : "CRLF";
+ }
+
///
/// 글이 아니라 이진 파일인지. 앞부분에 NUL 바이트가 있으면 이진으로 본다 -
/// Git 을 비롯한 대부분의 도구가 쓰는 판별이고, 실제 텍스트에는 NUL 이 들어가지 않는다.
diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs
index 869473c..cecc7cc 100644
--- a/src/MergeHub/ViewModels/HomeViewModel.cs
+++ b/src/MergeHub/ViewModels/HomeViewModel.cs
@@ -406,6 +406,8 @@ namespace MergeHub.ViewModels
var content = BuildLocalText();
var message = CommitMessage;
+ var encoding = SaveEncoding;
+ var newLine = SaveNewLine;
string? sha = null;
var ok = await _workspace.RunAsync("Save & Commit", () =>
@@ -420,7 +422,9 @@ namespace MergeHub.ViewModels
{
var full = Path.Combine(_workspace.RepositoryPath,
path!.Replace('/', Path.DirectorySeparatorChar));
- File.WriteAllText(full, content);
+
+ // 저장 형식을 고른 값으로 쓴다("원본 유지"면 지금까지와 같다).
+ TextFile.Write(full, content, encoding, newLine);
_workspace.Git.Stage(path!);
}
@@ -469,6 +473,7 @@ namespace MergeHub.ViewModels
SavePlainCommand.NotifyCanExecuteChanged();
AutoMergeCommand.NotifyCanExecuteChanged();
+ AcceptBlockCommand.NotifyCanExecuteChanged();
NextConflictCommand.NotifyCanExecuteChanged();
PrevConflictCommand.NotifyCanExecuteChanged();
}
@@ -1520,7 +1525,11 @@ namespace MergeHub.ViewModels
var content = BuildLocalText();
var target = RightFile;
- var ok = await _workspace.RunAsync("파일 저장", () => TextFile.Write(target, content));
+ var encoding = SaveEncoding;
+ var newLine = SaveNewLine;
+
+ var ok = await _workspace.RunAsync("파일 저장",
+ () => TextFile.Write(target, content, encoding, newLine));
if (!ok) return;
_workspace.OnUi(() =>
@@ -1694,6 +1703,9 @@ namespace MergeHub.ViewModels
/// 지금 캐럿이 놓인 행. 편집기 줄 번호와 1:1 이다.
[ObservableProperty] private int _caretRow;
+ /// 줄을 옮기면 "이 덩어리 반영"이 켜지고 꺼진다.
+ partial void OnCaretRowChanged(int value) => AcceptBlockCommand.NotifyCanExecuteChanged();
+
/// 상태바에 띄우는 "차이 3 / 15".
[ObservableProperty] private string _diffPosition = "차이 -";
@@ -2023,6 +2035,57 @@ namespace MergeHub.ViewModels
RefreshProblems();
}
+ // ================= 저장 형식 · 덩어리 반영 =================
+
+ ///
+ /// 저장할 때 쓸 인코딩·줄바꿈. 기본은 "원본 유지"라 지금까지와 똑같이 동작한다.
+ ///
+ /// 사내 소스에는 CP949 로 저장된 파일이 섞여 있어 새로 만드는 파일과 형식이 갈린다.
+ /// 읽어서 보여 주기만 하면 그 갈림을 못 고치므로 저장할 때 고를 수 있게 한다.
+ ///
+ public IReadOnlyList SaveEncodings => TextFile.SaveEncodings;
+ public IReadOnlyList SaveNewLines => TextFile.SaveNewLines;
+
+ [ObservableProperty] private string _saveEncoding = TextFile.KeepOriginal;
+ [ObservableProperty] private string _saveNewLine = TextFile.KeepOriginal;
+
+ ///
+ /// 지금 캐럿이 있는 차이 덩어리 전체를 BASE 내용으로 바꾼다.
+ ///
+ /// 줄 하나씩만 되면 연속 다섯 줄을 다섯 번 눌러야 한다 - WinMerge·Beyond Compare 가
+ /// 덩어리를 기본 단위로 쓰는 이유다. 덩어리는 "차이로 잡힌 줄이 이어지는 구간"이다.
+ ///
+ private bool CanAcceptBlock() => BlockAround(CaretRow).Count > 0;
+
+ [RelayCommand(CanExecute = nameof(CanAcceptBlock))]
+ private void AcceptBlock()
+ {
+ var block = BlockAround(CaretRow);
+ if (block.Count == 0) return;
+
+ foreach (var line in block) Apply(line, requery: false);
+
+ AfterEdit();
+ MessageUtil.ShowSuccessToast($"{block.Count}줄 반영");
+ }
+
+ /// 그 줄이 속한 차이 덩어리. 차이가 아니면 빈 목록이다.
+ private List BlockAround(int row)
+ {
+ var found = new List();
+ if (row < 0 || row >= Lines.Count) return found;
+ if (!Lines[row].CanTakeBase) return found;
+
+ var first = row;
+ while (first > 0 && Lines[first - 1].CanTakeBase) first--;
+
+ var last = row;
+ while (last + 1 < Lines.Count && Lines[last + 1].CanTakeBase) last++;
+
+ for (var i = first; i <= last; i++) found.Add(Lines[i]);
+ return found;
+ }
+
// ================= 자동 병합 · 충돌만 보기 =================
///
diff --git a/src/MergeHub/Views/HomeView.xaml b/src/MergeHub/Views/HomeView.xaml
index d2a1483..a4fbe86 100644
--- a/src/MergeHub/Views/HomeView.xaml
+++ b/src/MergeHub/Views/HomeView.xaml
@@ -955,6 +955,7 @@
Background="{DynamicResource Ubisam.Brush.Input.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Accent}" BorderThickness="1"
Effect="{DynamicResource Ubisam.Effect.Popup}">
+
+
+
+
+
+
+
+
+
+
@@ -1224,6 +1241,24 @@
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"
ToolTip="선택한 줄을 REMOTE(Theirs) 내용으로"/>
+
+
+
+
+
+
+
+
+