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}"> + + + + + +