diff --git a/doc/진행상황.md b/doc/진행상황.md
index 032f3eb..238a72a 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -127,10 +127,10 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
| 상태 | 항목 |
|---|---|
| 대기 | D-1 주석만 다른 파일을 다른 색으로 |
-| 대기 | D-2 C++ 문법 강조 |
+| **완료** | D-2 C++ 문법 강조 (이미 되고 있었음 · 전처리 지시문만 추가) |
| 대기 | D-3 폴더 안 전체 검색 |
| ~~취소~~ | ~~D-4 비교 창 여러 개~~ (2026-09-09 사용자 판단으로 뺌) |
-| 대기 | D-5 덩어리 단위 반영 (2-Way 는 이미 됨 · 3-Way 확인 필요) |
+| **완료** | D-5 덩어리 단위 반영 (3-Way 에서 BASE 를 가져오던 것을 REMOTE 로) |
| 대기 | D-6 비교 결과 텍스트 내보내기 |
| 대기 | D-7 드래그 앤 드롭으로 열기 |
@@ -147,6 +147,35 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
---
+### D-2 · D-5 — **완료** (2026-09-09)
+
+#### D-2. C++ 문법 강조
+
+**이미 되고 있었다.** `LanguageProfile.CFamily` 가 `.c .h .cpp .hpp .cc .hh .cxx .hxx .inl` 을
+모두 맡고 있고 예약어·주석·문자열도 들어 있다. 지시서에서 확인만 하라고 적어 둔 대로 확인했다.
+
+**빠진 것은 전처리 지시문 하나였다.** `#include` 는 `#` 과 `include` 로 쪼개져 보통 낱말처럼
+칠해졌다. 줄 첫머리가 `#` 이면 그 줄 전체를 지시문 색으로 칠하게 했다 - C# 쪽과 같은 규칙이다.
+같은 모양인데 언어마다 다르게 보이면 오히려 헷갈린다.
+언어표에 `hasPreprocessor` 를 두어 C 계열에서만 켠다. 자바·자바스크립트는 `#` 이 지시문이 아니다.
+
+**검사**: `LangTest` 12건 추가(40 → 52건).
+
+#### D-5. 덩어리 단위 반영
+
+**2-Way 는 이미 됐다**(`이 덩어리 반영` · `Alt+Shift+→`). 확인했다.
+
+**3-Way 가 잘못 동작하고 있었다.** 덩어리를 BASE 에서 가져오고 있었는데, 3-Way 에서 BASE 는
+공통 조상이다. 저쪽(REMOTE)을 가져오는 것이 `Take Theirs` 이고 덩어리 반영은 그것을
+줄 묶음으로 하는 것인데, BASE 를 가져오면 조상으로 되돌리는 셈이라 뜻이 전혀 달라진다.
+덩어리를 묶는 기준도 같이 바꿨다 - 2-Way 는 "BASE 를 가져올 수 있는 줄", 3-Way 는
+"저쪽을 가져올 수 있는 줄"이 이어진 구간이다.
+
+**확인**: 충돌 6개짜리 C++ 파일에서 `다음 충돌` → `이 덩어리 반영` 한 번에
+네 줄이 REMOTE 값으로 바뀌고 `Conflict 6 → 2`, `머지 반영함 4`.
+단축키(`Alt+Shift+→`)도 같은 명령을 부르므로 함께 고쳐졌다.
+---
+
### C. 잔손질 — **완료** (2026-09-09)
#### C-1. 코드 위 툴팁 제거
diff --git a/src/MergeHub/Services/CodeTokenizer.cs b/src/MergeHub/Services/CodeTokenizer.cs
index 2f4b8e8..0b95a9c 100644
--- a/src/MergeHub/Services/CodeTokenizer.cs
+++ b/src/MergeHub/Services/CodeTokenizer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using MergeHub.Views.Controls;
@@ -46,6 +46,23 @@ namespace MergeHub.Services
stillInBlockComment = false;
}
+ // ---- 전처리 지시문 ----
+ //
+ // 줄 첫머리가 # 이면 그 줄 전체가 지시문이다(#include · #define · #ifdef).
+ // C# 쪽과 같은 규칙으로 칠한다 - 같은 모양인데 언어마다 다르게 보이면 더 헷갈린다.
+ // 블록 주석이 이미 열려 있던 줄은 해당 없다(i 가 0 이 아니다).
+ if (profile.HasPreprocessor && i == 0)
+ {
+ var firstVisible = 0;
+ while (firstVisible < line.Length && char.IsWhiteSpace(line[firstVisible])) firstVisible++;
+
+ if (firstVisible < line.Length && line[firstVisible] == '#')
+ {
+ tokens.Add(new CodeToken(firstVisible, line.Length - firstVisible, CodeTokenKind.Preprocessor));
+ return tokens;
+ }
+ }
+
while (i < line.Length)
{
var start = i;
diff --git a/src/MergeHub/Services/LanguageProfile.cs b/src/MergeHub/Services/LanguageProfile.cs
index 2580ff9..9f9a868 100644
--- a/src/MergeHub/Services/LanguageProfile.cs
+++ b/src/MergeHub/Services/LanguageProfile.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
@@ -29,7 +29,8 @@ namespace MergeHub.Services
bool caseSensitive = true,
bool isCode = true,
bool hasBraces = false,
- bool useCSharpTokenizer = false)
+ bool useCSharpTokenizer = false,
+ bool hasPreprocessor = false)
{
Name = name;
Extensions = extensions;
@@ -41,6 +42,7 @@ namespace MergeHub.Services
IsCode = isCode;
HasBraces = hasBraces;
UseCSharpTokenizer = useCSharpTokenizer;
+ HasPreprocessor = hasPreprocessor;
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
Keywords = new HashSet(keywords ?? Array.Empty(), comparer);
@@ -56,6 +58,12 @@ namespace MergeHub.Services
public char[] Quotes { get; }
public bool CaseSensitive { get; }
+ ///
+ /// #include · #define 처럼 줄 첫머리의 # 지시문을 쓰는 언어인지.
+ /// C 계열이 그렇다. 켜 두면 그 줄 전체를 지시문 색으로 칠한다 - C# 과 같은 규칙이다.
+ ///
+ public bool HasPreprocessor { get; }
+
/// 문법 색을 입힐 대상인지. 평문·표 형식 문서는 false 다.
public bool IsCode { get; }
@@ -111,7 +119,8 @@ namespace MergeHub.Services
"inline", "operator", "new", "delete", "this", "nullptr", "true", "false",
"using", "typedef", "sizeof", "friend", "explicit", "mutable", "constexpr"
},
- controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true);
+ controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true,
+ hasPreprocessor: true);
public static readonly LanguageProfile Java = new LanguageProfile(
"Java", new[] { ".java" },
diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs
index 97c1657..c4fa5ca 100644
--- a/src/MergeHub/ViewModels/HomeViewModel.cs
+++ b/src/MergeHub/ViewModels/HomeViewModel.cs
@@ -2193,24 +2193,40 @@ namespace MergeHub.ViewModels
var block = BlockAround(CaretRow);
if (block.Count == 0) return;
- foreach (var line in block) Apply(line, requery: false);
+ // 2-Way 는 BASE 를, 3-Way 는 저쪽(REMOTE)을 가져온다. BlockAround 주석 참고.
+ foreach (var line in block)
+ {
+ if (IsThreeWay) ApplyRemote(line, requery: false);
+ else Apply(line, requery: false);
+ }
AfterEdit();
MessageUtil.ShowSuccessToast($"{block.Count}줄 반영");
}
- /// 그 줄이 속한 차이 덩어리. 차이가 아니면 빈 목록이다.
+ ///
+ /// 그 줄이 속한 차이 덩어리. 차이가 아니면 빈 목록이다.
+ ///
+ /// 무엇을 가져오는지가 모드마다 다르다.
+ /// 2-Way : BASE 를 결과로 가져온다. 덩어리는 "BASE 를 가져올 수 있는 줄"이 이어진 구간.
+ /// 3-Way : BASE 는 공통 조상이라 가져올 것이 아니다. 저쪽(REMOTE)을 가져오는 것이
+ /// Take Theirs 이고, 덩어리 반영은 그것을 줄 묶음으로 하는 것이다.
+ /// 여기서 BASE 를 가져오면 조상으로 되돌리는 셈이라 뜻이 전혀 달라진다.
+ ///
private List BlockAround(int row)
{
var found = new List();
if (row < 0 || row >= Lines.Count) return found;
- if (!Lines[row].CanTakeBase) return found;
+
+ bool Takeable(MergeLine line) => IsThreeWay ? line.CanTakeRemote : line.CanTakeBase;
+
+ if (!Takeable(Lines[row])) return found;
var first = row;
- while (first > 0 && Lines[first - 1].CanTakeBase) first--;
+ while (first > 0 && Takeable(Lines[first - 1])) first--;
var last = row;
- while (last + 1 < Lines.Count && Lines[last + 1].CanTakeBase) last++;
+ while (last + 1 < Lines.Count && Takeable(Lines[last + 1])) last++;
for (var i = first; i <= last; i++) found.Add(Lines[i]);
return found;