4차 B-3: 폴더 비교에서 다음 다른 파일로

훑는 중인 목록을 GitWorkspace 에 두고 파일 비교 화면에서 이전/다음으로 딛는다.
화면에 보이는 순서를 따르고, 찾은 줄 목록도 같은 방식으로 훑는다.
먼저 묻고 자리를 옮긴다. Ctrl+PageUp/PageDown.
This commit is contained in:
김승준 2026-09-10 20:49:46 +09:00
parent b9456b7f14
commit 8083075ece
9 changed files with 205 additions and 3 deletions

View File

@ -196,4 +196,40 @@ MergeHub 파일 비교 결과
| 짝의 두 파일을 지운 뒤 | 목록에 그대로 남고 **흐리게** 보였다 |
| 그 죽은 짝을 고르기 | `두 파일 모두 찾을 수 없습니다` 토스트, 화면은 그대로 |
검사 33건 추가(`RecentTest`). 전체 **555건 통과** · 빌드 경고 0 · 오류 0.
검사 33건 추가(`RecentTest`). 전체 **555건 통과** · 빌드 경고 0 · 오류 0.
---
## B-3. 폴더 비교에서 다음 다른 파일로 — **완료** (2026-09-10)
**왜** - 파일 하나를 보고 **폴더 탭으로 돌아갔다가** 다음 파일을 다시 열어야 했다.
다른 파일이 스무 개면 이 왕복을 스무 번 한다.
`GitWorkspace` 에 훑는 중인 목록(`Queue`)과 자리(`QueueIndex`)를 뒀다.
폴더 비교가 채우고 파일 비교가 딛는다 - 두 화면이 서로를 모르는 채로 같은 목록을 봐야 해서
이미 통로 노릇을 하는 이 객체에 뒀다.
**원본 결과가 아니라 지금 트리를 훑어 목록을 만든다.** 필터로 "코드가 다른 것만" 을 걸어 뒀으면
그 안에서만 움직여야 한다 - 목록에 없는 파일로 넘어가면 왜 거기로 갔는지 알 수 없다.
**찾은 줄 목록도 훑을 목록이 된다.** 같은 파일이 여러 줄 걸렸으면 한 번만 담는다 -
다음 파일을 눌렀는데 같은 파일이 다시 열리면 멈춘 것처럼 보인다.
**최근 목록으로 연 파일은 목록을 비운다.** 남아 있던 목록을 그대로 두면 다음 파일이 엉뚱한 데로 간다.
**먼저 묻고 그 다음에 자리를 옮긴다.** 확인 창에서 취소하면 자리도 그대로여야 하는데,
자리를 먼저 옮기면 취소했을 때 목록 위치만 앞서 나간다. 물은 뒤에는 두 번 묻지 않도록 막는다.
단축키는 `Ctrl+PageUp` · `Ctrl+PageDown` 이다. `Alt+PageDown`(북마크)은 그대로 두었고
검사로 빼앗지 않았음을 확인한다.
**화면에서 확인한 것** (파일 네 개 · 하나는 하위 폴더)
| 한 일 | 자리 | 열린 파일 | 이전/다음 |
|---|---|---|---|
| 검색 결과로 열기 | 1 / 4 | A.cpp | 끔 / 켬 |
| 다음 파일 ×3 | 2 → 3 → 4 | B.cpp → C.cpp → D.cpp | 마지막에서 다음이 꺼짐 |
| 이전 파일 ×2 | 3 → 2 | C.cpp → B.cpp | 둘 다 켬 |
| 고친 뒤 다음 파일 → 취소 | **2 / 4 그대로** | B.cpp 그대로 | 편집 내용도 그대로 |
| 다시 눌러 버리고 계속 | 3 / 4 | C.cpp | B.cpp 파일은 손대지 않음 |
검사 5건 추가(`ShortcutTest` 63 → 68). 전체 **560건 통과** · 빌드 경고 0 · 오류 0.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
| 2026-09-10 | **4차 B-3** — 폴더 비교 목록을 파일 비교 화면에서 이전/다음으로 딛는다 (Ctrl+PageUp/PageDown) | 아래 |
| 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 |
| 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 |
| 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 |
@ -30,6 +31,7 @@
| 날짜 | 문서 | 내용 |
|---|---|---|
| 2026-09-10 | **4차 B-3** — 폴더 비교 목록을 파일 비교 화면에서 이전/다음으로 딛는다 (Ctrl+PageUp/PageDown) | 아래 |
| 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 |
| 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 |
| 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 |

View File

@ -31,7 +31,7 @@
|---|---|
| **완료** | B-1 최근 비교 목록 |
| 대기 | B-2 같은 줄 접기 |
| 대기 | B-3 폴더 비교에서 다음 다른 파일로 |
| **완료** | B-3 폴더 비교에서 다음 다른 파일로 |
| 대기 | B-4 외부 편집기로 열기 |
### C. 화면 구성 정리

View File

@ -13,6 +13,24 @@ using UbisamBase.Core.SettingItems;
namespace MergeHub.Services
{
/// <summary>
/// 줄 단위로 견줄 파일 한 짝. 폴더 비교가 파일 비교로 넘기는 목록의 단위다.
/// </summary>
public sealed class ComparePair
{
public ComparePair(string left, string right, string name)
{
Left = left ?? string.Empty;
Right = right ?? string.Empty;
Name = name ?? string.Empty;
}
public string Left { get; }
public string Right { get; }
/// <summary>화면에 적을 이름. 대개 상대 경로다.</summary>
public string Name { get; }
}
/// <summary>
/// 홈(머지툴) 탭과 Git 탭이 함께 쓰는 저장소 상태.
/// IGitService 는 스레드 안전하지 않으므로 모든 호출을 여기서 한 줄로 직렬화한다.
@ -408,6 +426,34 @@ namespace MergeHub.Services
OnUi(DeliverPendingPlainCompare);
}
// ================= 폴더 비교에서 훑는 중인 파일 목록 =================
/// <summary>
/// 폴더 비교가 넘긴 "지금 훑는 중인 파일들". 파일 비교 화면의 이전/다음 파일이 이걸 딛는다.
///
/// <b>화면에 보이는 순서 그대로</b> 담는다 - 필터로 "코드가 다른 것만" 을 걸어 뒀으면
/// 그 안에서만 움직여야 한다. 목록에 없는 파일로 넘어가면 왜 거기로 갔는지 알 수 없다.
/// </summary>
public IReadOnlyList<ComparePair> Queue { get; private set; } = Array.Empty<ComparePair>();
/// <summary>지금 몇 번째를 보고 있는지. 목록이 없으면 -1.</summary>
public int QueueIndex { get; set; } = -1;
/// <summary>목록이나 자리가 바뀌었다.</summary>
public event EventHandler? QueueChanged;
public void SetQueue(IReadOnlyList<ComparePair> queue, int index)
{
Queue = queue ?? Array.Empty<ComparePair>();
QueueIndex = index;
OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty));
}
/// <summary>목록을 비운다. 폴더를 거치지 않고 연 파일은 딛고 갈 자리가 없다.</summary>
public void ClearQueue() => SetQueue(Array.Empty<ComparePair>(), -1);
public void RaiseQueueChanged() => OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty));
/// <summary>넘긴 파일에서 짚어 줄 자리. 없으면 0.</summary>
public int RequestedLine { get; private set; }

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
@ -20,6 +20,8 @@ namespace MergeHub.Services
Recompare,
GoToLine,
NextBookmark,
PrevFile,
NextFile,
ToggleBookmark,
AcceptBlock,
NextConflict,
@ -133,6 +135,12 @@ namespace MergeHub.Services
new ShortcutEntry(ShortcutAction.NextBookmark, Key.F2, ModifierKeys.None, "이동·북마크", "다음 북마크 (대체 키)"),
new ShortcutEntry(ShortcutAction.GoToLine, Key.G, ModifierKeys.Control, "이동·북마크", "줄 번호로 이동"),
new ShortcutEntry(ShortcutAction.PrevFile, Key.PageUp, ModifierKeys.Control, "이동·북마크",
"폴더 비교 목록에서 앞 파일로"),
new ShortcutEntry(ShortcutAction.NextFile, Key.PageDown, ModifierKeys.Control, "이동·북마크",
"폴더 비교 목록에서 다음 파일로",
"폴더 비교에서 파일을 열었을 때만 동작한다. 파일 하나를 보고 폴더 탭으로 돌아갔다 다시 여는 왕복을 없애려는 것이다."),
new ShortcutEntry(ShortcutAction.OpenFolderCompare, Key.D, ModifierKeys.Control, "파일·보기", "폴더 비교 열기",
"WinMerge 는 새 비교 창을 띄우지만 여기서는 탭 하나로 돌아간다 - 창을 여러 개 띄우는 도구가 아니다."),
new ShortcutEntry(ShortcutAction.Recompare, Key.F5, ModifierKeys.None, "파일·보기", "다시 비교(새로고침)"),

View File

@ -540,6 +540,19 @@ namespace MergeHub.ViewModels
return;
}
// 찾은 줄 목록도 훑을 목록이 된다. 같은 파일이 여러 줄 걸렸으면 한 번만 담는다 -
// 다음 파일을 눌렀는데 같은 파일이 다시 열리면 멈춘 것처럼 보인다.
var files = new List<ComparePair>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var one in SearchHits)
{
if (seen.Add(one.RelativePath))
files.Add(new ComparePair(one.LeftPath, one.RightPath, one.RelativePath));
}
_workspace.SetQueue(files, files.FindIndex(p =>
string.Equals(p.Name, hit.RelativePath, StringComparison.OrdinalIgnoreCase)));
OpenInDiffRequested?.Invoke(this, (hit.LeftPath, hit.RightPath));
// 오른쪽에서만 나온 줄이면 오른쪽 파일 기준 번호다.
@ -629,6 +642,9 @@ namespace MergeHub.ViewModels
return;
}
// 지금 화면에 보이는 순서대로 목록을 넘긴다. 파일 비교 화면이 이걸로 이전/다음 파일을 딛는다.
SendQueue(node);
OpenInDiffRequested?.Invoke(this, (node.LeftPath, node.RightPath));
// 요청을 먼저 걸고 탭을 연다. 파일 비교 화면이 아직 없으면 그 화면이 만들어지면서
@ -637,6 +653,31 @@ namespace MergeHub.ViewModels
UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home);
}
/// <summary>
/// 지금 목록에서 열 수 있는 파일을 <b>보이는 차례대로</b> 모아 넘긴다.
///
/// 필터를 걸어 뒀으면 그 안에서만 움직여야 한다 - 목록에 없는 파일로 넘어가면
/// 왜 거기로 갔는지 알 수 없다. 그래서 원본 결과가 아니라 <b>지금 트리</b>를 훑는다.
/// </summary>
private void SendQueue(FolderNode opened)
{
var list = new List<ComparePair>();
foreach (var root in Nodes) Collect(root, list);
var at = list.FindIndex(p =>
string.Equals(p.Left, opened.LeftPath, StringComparison.OrdinalIgnoreCase) &&
string.Equals(p.Right, opened.RightPath, StringComparison.OrdinalIgnoreCase));
_workspace.SetQueue(list, at);
}
private static void Collect(FolderNode node, List<ComparePair> into)
{
if (node.CanOpenInDiff) into.Add(new ComparePair(node.LeftPath, node.RightPath, node.RelativePath));
foreach (var child in node.Children) Collect(child, into);
}
private bool CanCopyToRight() => SelectedNode is { } node && node.Item.LeftExists;
[RelayCommand(CanExecute = nameof(CanCopyToRight))]

View File

@ -38,6 +38,7 @@ namespace MergeHub.ViewModels
_workspace.StateChanged += RaiseCanExecuteChanged;
_workspace.PlainCompareRequested += OnPlainCompareRequested;
_workspace.Recent.Changed += (_, _) => _workspace.OnUi(RefreshRecent);
_workspace.QueueChanged += (_, _) => RefreshQueue();
_reflowTimer.Tick += (_, _) => Reflow();
RefreshRecent();
@ -190,6 +191,57 @@ namespace MergeHub.ViewModels
/// <summary>비교 대상 파일 목록(충돌 중이면 충돌 파일, 아니면 선택 커밋의 파일).</summary>
public ObservableCollection<string> Files { get; } = new ObservableCollection<string>();
// ================= 폴더에서 훑는 중인 파일 이동 =================
/// <summary>훑을 목록이 있는지. 폴더를 거치지 않고 연 파일에는 없다.</summary>
public bool HasFileQueue => _workspace.Queue.Count > 0 && _workspace.QueueIndex >= 0;
/// <summary>지금 몇 번째인지. 예: <c>3 / 27</c>.</summary>
public string FileQueuePosition =>
HasFileQueue ? $"{_workspace.QueueIndex + 1} / {_workspace.Queue.Count}" : string.Empty;
private bool CanPrevFile() => HasFileQueue && _workspace.QueueIndex > 0;
private bool CanNextFile() => HasFileQueue && _workspace.QueueIndex < _workspace.Queue.Count - 1;
[RelayCommand(CanExecute = nameof(CanPrevFile))]
private void PrevFile() => MoveFile(-1);
[RelayCommand(CanExecute = nameof(CanNextFile))]
private void NextFile() => MoveFile(1);
/// <summary>
/// 폴더 비교 목록에서 앞뒤 파일로 옮긴다.
///
/// 여기서 <b>먼저 묻고</b> 그 다음에 자리를 옮긴다. 확인 창에서 취소하면 자리도 그대로여야 하는데,
/// 자리를 먼저 옮기면 취소했을 때 목록 위치만 앞서 나간다.
/// 물은 뒤에는 <c>_suspendGuard</c> 로 막는다 - 여는 길(<c>RequestPlainCompare</c>)이
/// 같은 것을 한 번 더 묻기 때문이다.
/// </summary>
private void MoveFile(int delta)
{
var next = _workspace.QueueIndex + delta;
if (next < 0 || next >= _workspace.Queue.Count) return;
var pair = _workspace.Queue[next];
if (!ConfirmDiscard($"다른 파일을 엽니다: {pair.Name}")) return;
_workspace.QueueIndex = next;
_suspendGuard = true;
try { _workspace.RequestPlainCompare(pair.Left, pair.Right); }
finally { _suspendGuard = false; }
RefreshQueue();
}
private void RefreshQueue()
{
OnPropertyChanged(nameof(HasFileQueue));
OnPropertyChanged(nameof(FileQueuePosition));
PrevFileCommand.NotifyCanExecuteChanged();
NextFileCommand.NotifyCanExecuteChanged();
}
// ================= 최근 비교 목록 =================
/// <summary>최근에 견준 파일 짝. 위 줄의 목록에 뿌린다.</summary>
@ -218,6 +270,9 @@ namespace MergeHub.ViewModels
return;
}
// 최근 목록으로 연 파일은 딛고 갈 목록이 없다. 남아 있던 목록을 그대로 두면
// 다음 파일이 엉뚱한 데로 간다.
_workspace.ClearQueue();
_workspace.RequestPlainCompare(pair.Left, pair.Right);
}

View File

@ -397,6 +397,17 @@
Background="Transparent"
AllowDrop="True" DragOver="View_DragOver" Drop="RightFile_Drop"/>
<!-- 폴더 비교에서 열었을 때만 켜진다. 목록 없이 연 파일에는 딛고 갈 자리가 없다. -->
<Button Content="◀ 이전 파일" Command="{Binding PrevFileCommand}"
Height="26" MinHeight="26" Padding="8,2" FontSize="11" Margin="14,0,4,0"
ToolTip="폴더 비교 목록에서 앞 파일로 (Ctrl+PageUp)"/>
<TextBlock Text="{Binding FileQueuePosition}" VerticalAlignment="Center"
Style="{StaticResource Muted}" FontSize="11" MinWidth="42"
TextAlignment="Center"/>
<Button Content="다음 파일 ▶" Command="{Binding NextFileCommand}"
Height="26" MinHeight="26" Padding="8,2" FontSize="11" Margin="4,0,0,0"
ToolTip="폴더 비교 목록에서 다음 파일로 (Ctrl+PageDown)"/>
<TextBlock Text="최근" VerticalAlignment="Center" Style="{StaticResource Muted}"
FontSize="11" Margin="14,0,4,0"/>
<ComboBox x:Name="RecentBox" ItemsSource="{Binding RecentFiles}"

View File

@ -881,6 +881,9 @@ namespace MergeHub.Views
case ShortcutAction.NextBookmark: return Run(vm.NextBookmarkCommand);
case ShortcutAction.PrevFile: return Run(vm.PrevFileCommand);
case ShortcutAction.NextFile: return Run(vm.NextFileCommand);
case ShortcutAction.ToggleBookmark:
if (vm.SelectedLine is null) return false;
vm.ToggleBookmarkCommand.Execute(vm.SelectedLine);