From 4f33cacd4fbf6980ee0a80c4991fb2d585ceb29c Mon Sep 17 00:00:00 2001 From: ksj09406 Date: Wed, 9 Sep 2026 13:50:13 +0900 Subject: [PATCH] =?UTF-8?q?E-5,=20E-6:=20=EB=B0=98=EC=98=81=20=EC=83=89=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80=20=C2=B7=20=EC=B0=A8=EC=9D=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EA=B2=B9=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E-5. 줄을 반영한 뒤 비교 규칙을 하나 건드리면 초록(머지 반영함)이던 줄이 보라(직접 수정)로 바뀌었다. 반영 표시가 되돌리기 스택에만 있었다. 규칙을 바꾸면 화면을 통째로 다시 만들면서 그 스택을 비운다. 그러면 표시가 사라지고 남는 단서는 "글이 원본과 다르다"뿐이라 직접 수정으로 칠해진다. B-3 에서 Take Mine 이 충돌을 못 지우던 것과 같은 뿌리다. - 반영할 때 줄 번호로도 남긴다. BASE 줄 번호, 없으면 MINE 줄 번호. - 다시 만들 때 반영 여부를 먼저 본다. 반영한 줄은 직접 수정 조건에도 걸린다. - 되돌리면 그 기록도 함께 지운다. 곁들여 머지 주석 줄도 같은 이유로 보라색이 됐다. 주석 줄이라는 표시 역시 줄 객체에만 있어서 다시 만들면 사라진다. 글을 보고 알아보게 했다 (MergeNote.LooksLikeNote). E-6. Alt+위/아래로 차이를 옮겨 다니면 칸 아래 상태바 위에 코드가 겹쳐 그려졌다. 표시층은 편집기 글자와 눈높이를 맞추려고 RenderTransform 으로 살짝 밀어 두는데, 그건 배치가 아니라 그리기라 칸 밖으로 나가도 막는 것이 없다. 격자의 행도 자기 칸 밖으로 나가는 자식을 자르지 않는다. 코드가 놓이는 자리를 자르는 상자로 감쌌다. 네 칸 모두. 확인: 반영 후 공백/Aa 규칙을 눌러도 머지 반영함 2 · 직접 수정 0 그대로. Alt+아래 8회, Alt+위 6회 눌러도 마지막 줄이 경계에서 깨끗이 잘린다. NoteTest 13건 추가. 전체 399 -> 412건 통과. Co-Authored-By: Claude Opus 5 --- doc/진행상황.md | 37 +++++++++ src/MergeHub/Services/MergeNote.cs | 40 +++++++++- src/MergeHub/ViewModels/HomeViewModel.cs | 93 +++++++++++++++++------ src/MergeHub/Views/HomeView.xaml | 95 +++++++++++++++--------- 4 files changed, 208 insertions(+), 57 deletions(-) diff --git a/doc/진행상황.md b/doc/진행상황.md index ce42138..032f3eb 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -142,6 +142,8 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘 | **완료** | E-2 칸 폭을 줄였다 늘리면 행 높이가 안 돌아옴 | | **완료** | E-3 경로 줄과 코드 사이의 빈 공간 | | **완료** | E-4 2-Way 로 돌아오면 LOCAL 옆에 빈 칸이 남음 | +| **완료** | E-5 반영한 줄이 규칙을 바꾸면 "직접 수정" 색으로 바뀜 | +| **완료** | E-6 차이로 이동할 때 코드가 바닥 글 위에 겹쳐 그려짐 | --- @@ -232,6 +234,41 @@ REMOTE 칸이 접히면서 생긴 자리를 아무도 채우지 않는다 **확인**: 3-Way 에서 구분선을 끌고 2-Way 로 돌아와도 두 칸이 폭을 꽉 채운다. 2-Way 에서 끌어 놓은 비율은 다른 모니터를 다녀와도 그대로다. + +#### E-5. 반영한 줄이 규칙을 바꾸면 "직접 수정" 색이 되던 문제 + +줄을 반영(Accept)한 뒤 비교 규칙을 하나 건드리면 그 줄이 초록(머지 반영함)에서 +보라(직접 수정)로 바뀌었다. + +**반영 표시가 되돌리기 스택에만 있었다.** 규칙을 바꾸면 화면을 통째로 다시 만들면서 그 스택을 +비운다. 그러면 표시가 사라지고 남는 단서는 "글이 원본과 다르다"뿐이라 직접 수정으로 칠해진다. +B-3 에서 Take Mine 이 충돌을 못 지우던 것과 같은 뿌리다. + +- 반영할 때 줄 번호로도 남긴다. BASE 줄 번호, 없으면 MINE 줄 번호 - 둘 다 얼어 있는 값이라 + 다시 정렬해도 살아남는다. +- 다시 만들 때 반영 여부를 먼저 본다. 반영한 줄은 직접 수정 조건에도 걸리므로 순서가 중요하다. +- 되돌리면 그 기록도 함께 지운다. + +**곁들여** 머지 주석 줄도 같은 이유로 보라색이 됐다. 주석 줄이라는 표시 역시 줄 객체에만 있어서 +다시 만들면 사라진다. 글을 보고 알아보게 했다(`MergeNote.LooksLikeNote`). 사람이 우연히 같은 +모양으로 쓴 주석을 잘못 볼 수 있지만, 그래도 색이 달라질 뿐이고 반대로 못 알아보면 반영할 때마다 +"직접 수정"이 하나씩 늘어난다. + +**확인**: 반영 → 공백 규칙 → Aa 규칙 순으로 눌러도 `머지 반영함 2 · 직접 수정 0` 그대로. +`NoteTest` 13건 추가(16 → 29건). + +#### E-6. 차이로 이동하면 코드가 바닥 글 위에 겹쳐 그려지던 문제 + +`Alt+↑/↓` 로 차이를 옮겨 다니면 칸 아래 상태바 위에 코드가 겹쳐 그려졌다. + +표시층은 편집기 글자와 눈높이를 맞추려고 `RenderTransform` 으로 살짝 밀어 둔다. 그런데 +RenderTransform 은 배치가 아니라 그리기라 칸 밖으로 나가도 아무도 막지 않는다. +격자(Grid)의 행도 자기 칸 밖으로 나가는 자식을 자르지 않는다. + +코드가 놓이는 자리를 자르는 상자로 감쌌다(`ClipToBounds`). 네 칸 모두. + +**확인**: `Alt+↓` 8회 · `Alt+↑` 6회 눌러도 마지막 줄이 칸 경계에서 깨끗이 잘린다. + **세 가지 모두 확인**: 세로 막대를 끌면 세 칸이 함께 움직인다. 2560×1600 ↔ 1920×1080 을 오가도, 가운데 구분선을 끌었다 되돌려도 처음과 같은 화면이 된다. 테스트 399건 그대로 통과. diff --git a/src/MergeHub/Services/MergeNote.cs b/src/MergeHub/Services/MergeNote.cs index 5a8c8dd..bdf73cf 100644 --- a/src/MergeHub/Services/MergeNote.cs +++ b/src/MergeHub/Services/MergeNote.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.IO; +using System.Text.RegularExpressions; namespace MergeHub.Services { @@ -33,6 +34,43 @@ namespace MergeHub.Services return close is null ? $"{open} {body}" : $"{open} {body} {close}"; } + /// + /// "[사람] 2026-09-09" 로 시작하는 모양. 우리가 만든 주석의 몸통이다. + /// + private static readonly Regex NoteBody = + new Regex(@"^\[[^\]]+\]\s+\d{4}-\d{2}-\d{2}(\s|$)", RegexOptions.Compiled); + + /// + /// 이 줄이 우리가 남긴 머지 주석인지 글만 보고 판단한다. + /// + /// 왜 필요한가 - 주석 줄에는 "이건 주석 줄"이라는 표시가 붙어 있는데, 비교 규칙을 바꾸는 등 + /// 화면을 다시 만들면 줄 객체가 새로 생기면서 그 표시가 사라진다. 그러면 주석 줄이 + /// 사용자가 직접 넣은 줄로 보여 "직접 수정" 색이 된다. + /// 남아 있는 단서가 글자밖에 없으므로 여기서 다시 알아본다. + /// + /// 사람이 우연히 같은 모양의 주석을 썼다면 잘못 볼 수 있다. 그래도 색이 달라질 뿐이고, + /// 반대로 못 알아보면 반영할 때마다 "직접 수정"이 하나씩 늘어난다. + /// + public static bool LooksLikeNote(string? line, string? path) + { + if (string.IsNullOrWhiteSpace(line)) return false; + + var (open, close) = CommentTokensFor(path); + var text = line!.Trim(); + + if (!text.StartsWith(open, StringComparison.Ordinal)) return false; + + var body = text.Substring(open.Length).TrimStart(); + + if (close is not null) + { + if (!body.EndsWith(close, StringComparison.Ordinal)) return false; + body = body.Substring(0, body.Length - close.Length).TrimEnd(); + } + + return NoteBody.IsMatch(body); + } + /// 코드 줄 끝에 주석을 붙인다. 줄 끝 공백은 정리한다. public static string Append(string line, string note) { diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs index 46f2199..97c1657 100644 --- a/src/MergeHub/ViewModels/HomeViewModel.cs +++ b/src/MergeHub/ViewModels/HomeViewModel.cs @@ -635,8 +635,8 @@ namespace MergeHub.ViewModels _baseTextSnapshot = baseText; _remoteTextSnapshot = threeWay ? remoteText : string.Empty; _mineTextSnapshot = threeWay ? localText : string.Empty; - _resolvedBaseNumbers.Clear(); // 파일이 바뀌면 앞 파일의 결정은 뜻이 없다 - _resolvedMineNumbers.Clear(); + _appliedBaseNumbers.Clear(); // 파일이 바뀌면 앞 파일의 결정은 뜻이 없다 + _appliedMineNumbers.Clear(); _originalLocalText = BuildLocalText(); SyncEditorFromLines(); @@ -921,6 +921,9 @@ namespace MergeHub.ViewModels { InsertedNote = InsertNoteAbove(line, note), }); + + // 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다. + MarkApplied(line); _redo.Clear(); _workspace.AppendLog(description); @@ -1012,8 +1015,7 @@ namespace MergeHub.ViewModels // 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다. // 내용이 안 바뀌므로 다시 정렬하면 이 표시가 사라진다 - BASE 줄 번호로 따로 붙들어 둔다. line.IsApplied = true; - if (line.BaseNumber is int resolvedBase) _resolvedBaseNumbers.Add(resolvedBase); - else if (line.OriginalLocalNumber is int resolvedMine) _resolvedMineNumbers.Add(resolvedMine); + MarkApplied(line); AfterEdit(); return; } @@ -1028,6 +1030,9 @@ namespace MergeHub.ViewModels _undo.Push(new LineEdit(line, before, beforeHasLocal, after, afterHasLocal, EditKind.TakeBase, description)); + + // 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다. + MarkApplied(line); _redo.Clear(); _workspace.AppendLog(description); @@ -1095,6 +1100,9 @@ namespace MergeHub.ViewModels { InsertedNote = InsertNoteAbove(line, note), }); + + // 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다. + MarkApplied(line); _redo.Clear(); _workspace.AppendLog(description); @@ -1286,11 +1294,23 @@ namespace MergeHub.ViewModels Lines.Clear(); foreach (var row in rows) { - if (row.LocalNumber is int number && touched.Contains(number)) - row.IsManuallyEdited = true; + // 반영한 줄인지를 먼저 본다. + // + // 반영한 줄은 글이 원본과 달라지므로 아래 "직접 수정" 조건에도 걸린다. + // 그대로 두면 규칙을 하나 건드리는 순간 반영한 줄이 전부 보라색(직접 수정)이 된다. + // 사람이 직접 친 것과 버튼으로 가져온 것은 뜻이 다르니 섞이면 안 된다. + row.IsApplied = IsMarkedApplied(row); - // "내 것으로 간다"고 정했던 충돌은 내용이 그대로라 글만 봐서는 알 수 없다. - if (IsDecidedAsMine(row)) row.IsApplied = true; + // 우리가 남긴 머지 주석 줄은 "사용자가 직접 넣은 줄"이 아니다. + // 표시가 줄 객체에만 있어서 다시 만들면 사라진다 - 글을 보고 되찾는다. + if (MergeNote.LooksLikeNote(row.LocalText, SelectedFile)) + { + row.IsMergeNote = true; + row.IsApplied = true; + } + + if (!row.IsApplied && row.LocalNumber is int number && touched.Contains(number)) + row.IsManuallyEdited = true; Lines.Add(row); } @@ -1496,8 +1516,8 @@ namespace MergeHub.ViewModels _baseTextSnapshot = baseText; _remoteTextSnapshot = string.Empty; // 파일 두 개 비교는 늘 2-Way 다 _mineTextSnapshot = string.Empty; - _resolvedBaseNumbers.Clear(); - _resolvedMineNumbers.Clear(); + _appliedBaseNumbers.Clear(); + _appliedMineNumbers.Clear(); RefreshLanguage(); var rows = BuildLines(baseText, localText); @@ -1989,6 +2009,14 @@ namespace MergeHub.ViewModels edit.Line.LocalText = toBefore ? edit.Before : edit.After; edit.Line.HasLocal = toBefore ? edit.BeforeHasLocal : edit.AfterHasLocal; + // 되돌리면 반영 표시도 함께 풀어야 한다. 안 그러면 규칙을 바꿔 화면을 다시 만들 때 + // 되돌린 줄이 다시 "머지 반영함"으로 살아난다. + if (edit.Kind == EditKind.TakeBase) + { + if (toBefore) UnmarkApplied(edit.Line); + else MarkApplied(edit.Line); + } + // 이 편집이 끼워 넣은 주석 줄도 함께 넣고 뺀다. // 본문만 되돌리고 주석을 남기면 파일에 유령 주석이 쌓인다. if (edit.InsertedNote is not { } note) return; @@ -2006,21 +2034,44 @@ namespace MergeHub.ViewModels } /// - /// "머지 반영함"과 "직접 수정" 표시를 편집 이력에서 다시 계산한다. - /// 둘을 섞으면 안 된다 - 사용자가 직접 지운 줄이 "머지 반영함"으로 보이면 안 되기 때문이다. - /// Undo 로 되돌린 줄은 자동으로 표시가 풀린다. + /// "이 줄은 반영했다"를 줄 번호로 기억해 둔다. + /// + /// 반영 표시는 지금까지 되돌리기 스택에만 있었다. 그런데 비교 규칙을 바꾸면 화면을 통째로 + /// 다시 만들면서 스택을 비우므로, 그때 표시가 사라지고 글이 원본과 다르다는 이유만으로 + /// "직접 수정"으로 칠해졌다. 실제로 Accept 한 줄이 규칙 하나 건드리자 보라색이 됐다. + /// + /// 열쇠는 얼어 있는 번호여야 한다 - BASE 줄 번호, 없으면(양쪽이 각자 새 줄을 넣은 자리) + /// MINE 줄 번호. + /// + private void MarkApplied(MergeLine line) + { + if (line.BaseNumber is int baseNumber) _appliedBaseNumbers.Add(baseNumber); + else if (line.OriginalLocalNumber is int mineNumber) _appliedMineNumbers.Add(mineNumber); + } + + /// 되돌리면 그 표시도 함께 지운다. 안 그러면 되돌린 줄이 계속 "반영함"으로 남는다. + private void UnmarkApplied(MergeLine line) + { + if (line.BaseNumber is int baseNumber) _appliedBaseNumbers.Remove(baseNumber); + else if (line.OriginalLocalNumber is int mineNumber) _appliedMineNumbers.Remove(mineNumber); + } + /// - /// "내 것으로 간다"고 정해 둔 충돌 줄인지. + /// "반영했다"고 표시해 둔 줄인지. /// /// 그 결정은 글을 바꾸지 않는다(이미 내 원본과 같은 줄이다). 그래서 되돌리기 /// 스택에도 남지 않고, 다시 정렬하면 줄 객체 자체가 새로 만들어진다. /// 붙들어 둘 것이 줄 번호밖에 없어 여기에 따로 모아 둔다. /// - private bool IsDecidedAsMine(MergeLine line) => + private bool IsMarkedApplied(MergeLine line) => line.BaseNumber is int baseNumber - ? _resolvedBaseNumbers.Contains(baseNumber) - : line.OriginalLocalNumber is int mineNumber && _resolvedMineNumbers.Contains(mineNumber); + ? _appliedBaseNumbers.Contains(baseNumber) + : line.OriginalLocalNumber is int mineNumber && _appliedMineNumbers.Contains(mineNumber); + /// + /// "머지 반영함"과 "직접 수정" 표시를 편집 이력에서 다시 계산한다. + /// 둘을 섞으면 안 된다 - 사용자가 직접 지운 줄이 "머지 반영함"으로 보이면 안 되기 때문이다. + /// Undo 로 되돌린 줄은 자동으로 표시가 풀린다. /// private void RefreshAppliedFlags() { @@ -2040,8 +2091,8 @@ namespace MergeHub.ViewModels { // "내 것으로 간다"고 정한 충돌은 글을 바꾸지 않아 되돌리기 스택에 남지 않는다. // 그것까지 여기서 지워 버리면 Take Mine 을 눌러도 남은 충돌이 줄지 않는다. - line.IsApplied = applied.Contains(line) || IsDecidedAsMine(line); - line.IsManuallyEdited = edited.Contains(line); + line.IsApplied = applied.Contains(line) || IsMarkedApplied(line); + line.IsManuallyEdited = !line.IsApplied && edited.Contains(line); if (!line.IsApplied) line.AppliedNote = null; } @@ -2174,13 +2225,13 @@ namespace MergeHub.ViewModels /// 행이 새로 만들어지는데, 이걸 들고 있지 않으면 이미 정한 충돌이 되살아나 /// "남은 충돌"이 줄지 않는다. BASE 는 얼어 있으므로 그 줄 번호가 안전한 열쇠다. /// - private readonly HashSet _resolvedBaseNumbers = new HashSet(); + private readonly HashSet _appliedBaseNumbers = new HashSet(); /// /// BASE 에 없는 충돌(양쪽이 각자 새 줄을 넣은 자리)용. 이런 줄은 BASE 줄 번호가 없으므로 /// 역시 얼어 있는 MINE 줄 번호로 붙든다. /// - private readonly HashSet _resolvedMineNumbers = new HashSet(); + private readonly HashSet _appliedMineNumbers = new HashSet(); private bool CanAutoMerge() => IsThreeWay && Lines.Any(l => l.ThreeWay == ThreeWayState.RemoteChanged); diff --git a/src/MergeHub/Views/HomeView.xaml b/src/MergeHub/Views/HomeView.xaml index 004de26..ad0796c 100644 --- a/src/MergeHub/Views/HomeView.xaml +++ b/src/MergeHub/Views/HomeView.xaml @@ -810,7 +810,10 @@ - + + @@ -823,16 +826,20 @@ TextTrimming="CharacterEllipsis"/> - + + + + + @@ -911,16 +921,20 @@ - + + + + + @@ -1009,7 +1026,8 @@ - + + @@ -1233,7 +1251,10 @@ - + + @@ -1258,16 +1279,20 @@ - + + + +