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 @@
-
+
+
+
+