파일 비교: 바꾸기(Ctrl+H) · 덩어리 단위 반영 · 저장 형식 고르기

- 바꾸기: 찾기 칸 아래에 바꿀 말 줄이 붙는다. Ctrl+F 로 열면 안 뜬다 -
  찾기만 할 때 자리를 뺏지 않기 위해서다. "바꾸기" 는 고른 자리가 찾는 말일 때만
  바꾸고 다음으로 넘어간다 - 엉뚱한 자리를 덮어쓰는 것보다 한 번 더 누르게 하는 편이 낫다.
  "모두" 는 한 번에 바꾸고 몇 개인지 알려 준다(되돌리기는 편집기의 Ctrl+Z 가 맡는다).
- 덩어리 단위 반영: 줄 하나씩만 되면 연속 다섯 줄을 다섯 번 눌러야 한다.
  WinMerge·Beyond Compare 가 덩어리를 기본 단위로 쓰는 이유다. 덩어리는
  "차이로 잡힌 줄이 이어지는 구간"이고, 캐럿이 그 안에 있을 때만 켜진다. Alt+Shift+→.
- 저장 형식: 사내 소스에 CP949 로 저장된 파일이 섞여 있어 새로 만드는 파일과 형식이
  갈린다. 읽어서 보여 주기만 하면 그 갈림을 못 고치므로 저장할 때 고를 수 있게 했다.
  기본값 "원본 유지" 라 지금까지와 동작이 같다. Save & Commit 경로도 같은 값을 쓴다
  (전에는 File.WriteAllText 로 UTF-8 고정이었다).

실행 확인: PulseUs → PulseMicros 2개 바꿈 · 마지막 차이에서 2줄 한 번에 반영.
단축키 61 → 63건, 전체 305건 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-08 14:44:08 +09:00
parent 524a6a715d
commit de1a43cc70
6 changed files with 241 additions and 20 deletions

View File

@ -55,14 +55,14 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
되돌릴 수 없는 일이라 <b>화면 없이 검사할 수 있어야</b> 하기 때문이다 - 겹치는 파일은 덮고, 되돌릴 수 없는 일이라 <b>화면 없이 검사할 수 있어야</b> 하기 때문이다 - 겹치는 파일은 덮고,
대상에만 있던 파일은 남기고, 두 번 복사해도 같은지를 테스트로 못 박았다. 대상에만 있던 파일은 남기고, 두 번 복사해도 같은지를 테스트로 못 박았다.
### ② 파일 비교 ### ② 파일 비교 (2026-09-08)
| 상태 | 항목 | 내용 | | 상태 | 항목 | 내용 | 확인 |
|---|---|---| |---|---|---|---|
| 대기 | 바꾸기 (Ctrl+H) | 찾기만 있다 | | **완료** | 바꾸기 (Ctrl+H) | 찾기 칸 아래에 바꿀 말 줄이 붙는다(Ctrl+F 로 열면 안 뜬다 - 찾기만 할 때 자리를 뺏지 않는다). `바꾸기` 는 고른 자리가 <b>찾는 말일 때만</b> 바꾸고 다음으로 넘어간다 - 엉뚱한 자리를 덮어쓰는 것보다 한 번 더 누르게 하는 편이 낫다 | `PulseUs → PulseMicros` 2개 바꿈 |
| 대기 | 덩어리 단위 반영 | 줄 단위 아니면 전체뿐. 연속 5줄이면 5번 누른다 | | **완료** | 덩어리 단위 반영 | `이 덩어리 반영`(`Alt+Shift+→`). 차이로 잡힌 줄이 이어지는 구간을 한 번에 | 마지막 차이에서 2줄 한 번에 반영 |
| 대기 | 인코딩·줄바꿈 변환 저장 | CP949 ↔ UTF-8. 지금은 읽어서 보여주기만 | | **완료** | 인코딩·줄바꿈 변환 저장 | ACTIONS 의 `저장 형식` 두 칸. 기본 "원본 유지"라 지금까지와 똑같이 나간다. UTF-8 / UTF-8(BOM) / CP949 · CRLF / LF | 화면 확인 |
| 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 | | 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 | |
--- ---

View File

@ -21,11 +21,13 @@ namespace MergeHub.Services
GoToLine, GoToLine,
NextBookmark, NextBookmark,
ToggleBookmark, ToggleBookmark,
AcceptBlock,
NextConflict, NextConflict,
PrevConflict, PrevConflict,
AutoMerge, AutoMerge,
OpenFolderCompare, OpenFolderCompare,
Find, Find,
Replace,
FindNext, FindNext,
FindPrev, FindPrev,
CloseFind, CloseFind,
@ -118,6 +120,9 @@ namespace MergeHub.Services
"이전 충돌 (3-Way)"), "이전 충돌 (3-Way)"),
new ShortcutEntry(ShortcutAction.AutoMerge, Key.M, ModifierKeys.Alt, "병합·편집", new ShortcutEntry(ShortcutAction.AutoMerge, Key.M, ModifierKeys.Alt, "병합·편집",
"자동 병합 - 저쪽만 고친 줄을 한 번에 가져온다 (3-Way)"), "자동 병합 - 저쪽만 고친 줄을 한 번에 가져온다 (3-Way)"),
new ShortcutEntry(ShortcutAction.AcceptBlock, Key.Right, ModifierKeys.Alt | ModifierKeys.Shift, "병합·편집",
"이 덩어리 반영 - 차이가 이어지는 구간을 한 번에",
"줄 하나씩만 되면 연속 다섯 줄을 다섯 번 눌러야 한다."),
new ShortcutEntry(ShortcutAction.Undo, Key.Z, ModifierKeys.Control, "병합·편집", "실행 취소", 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.Save, Key.S, ModifierKeys.Control, "파일·보기", "저장"),
new ShortcutEntry(ShortcutAction.Find, Key.F, 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.FindNext, Key.F3, ModifierKeys.None, "찾기", "다음 찾기"),
new ShortcutEntry(ShortcutAction.FindPrev, Key.F3, ModifierKeys.Shift, "찾기", "이전 찾기"), new ShortcutEntry(ShortcutAction.FindPrev, Key.F3, ModifierKeys.Shift, "찾기", "이전 찾기"),
new ShortcutEntry(ShortcutAction.CloseFind, Key.Escape, ModifierKeys.None, "찾기", "찾기 칸 닫기"), new ShortcutEntry(ShortcutAction.CloseFind, Key.Escape, ModifierKeys.None, "찾기", "찾기 칸 닫기"),

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -52,24 +53,70 @@ namespace MergeHub.Services
return encoding.GetString(bytes, offset, bytes.Length - offset); return encoding.GetString(bytes, offset, bytes.Length - offset);
} }
/// <summary>저장할 때 고를 수 있는 인코딩. 빈 값(원본 유지)이 기본이다.</summary>
public const string KeepOriginal = "원본 유지";
public static IReadOnlyList<string> SaveEncodings { get; } =
new[] { KeepOriginal, "UTF-8", "UTF-8 (BOM)", "CP949" };
public static IReadOnlyList<string> SaveNewLines { get; } =
new[] { KeepOriginal, "CRLF", "LF" };
/// <summary> /// <summary>
/// 원래 파일의 인코딩과 줄바꿈을 그대로 유지해 덮어쓴다. /// 원래 파일의 인코딩과 줄바꿈을 그대로 유지해 덮어쓴다.
/// 새 파일이면 UTF-8(BOM 없음) · CRLF 로 쓴다. /// 새 파일이면 UTF-8(BOM 없음) · CRLF 로 쓴다.
/// </summary> /// </summary>
public static void Write(string path, string content) public static void Write(string path, string content) => Write(path, content, null, null);
/// <summary>
/// 인코딩·줄바꿈을 <b>바꿔서</b> 저장한다. 둘 다 <c>null</c> 이나 "원본 유지"면 지금까지와 같다.
///
/// 사내 소스에는 CP949 로 저장된 파일이 섞여 있어, 새로 만드는 파일과 형식이 갈린다.
/// 읽어서 보여 주기만 하면 그 갈림을 못 고치므로 저장할 때 고를 수 있게 한다.
/// </summary>
public static void Write(string path, string content, string? encodingName, string? newLineName)
{ {
var encoding = File.Exists(path) var encoding = ResolveEncoding(path, encodingName);
? Detect(File.ReadAllBytes(path)) var newLine = ResolveNewLine(path, newLineName);
: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
var newLine = File.Exists(path) ? Describe(path).NewLine : "CRLF";
var text = content.Replace("\r\n", "\n"); var text = content.Replace("\r\n", "\n");
if (newLine != "LF") text = text.Replace("\n", "\r\n"); if (newLine != "LF") text = text.Replace("\n", "\r\n");
File.WriteAllText(path, text, encoding); 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";
}
/// <summary> /// <summary>
/// 글이 아니라 이진 파일인지. 앞부분에 NUL 바이트가 있으면 이진으로 본다 - /// 글이 아니라 이진 파일인지. 앞부분에 NUL 바이트가 있으면 이진으로 본다 -
/// Git 을 비롯한 대부분의 도구가 쓰는 판별이고, 실제 텍스트에는 NUL 이 들어가지 않는다. /// Git 을 비롯한 대부분의 도구가 쓰는 판별이고, 실제 텍스트에는 NUL 이 들어가지 않는다.

View File

@ -406,6 +406,8 @@ namespace MergeHub.ViewModels
var content = BuildLocalText(); var content = BuildLocalText();
var message = CommitMessage; var message = CommitMessage;
var encoding = SaveEncoding;
var newLine = SaveNewLine;
string? sha = null; string? sha = null;
var ok = await _workspace.RunAsync("Save & Commit", () => var ok = await _workspace.RunAsync("Save & Commit", () =>
@ -420,7 +422,9 @@ namespace MergeHub.ViewModels
{ {
var full = Path.Combine(_workspace.RepositoryPath, var full = Path.Combine(_workspace.RepositoryPath,
path!.Replace('/', Path.DirectorySeparatorChar)); path!.Replace('/', Path.DirectorySeparatorChar));
File.WriteAllText(full, content);
// 저장 형식을 고른 값으로 쓴다("원본 유지"면 지금까지와 같다).
TextFile.Write(full, content, encoding, newLine);
_workspace.Git.Stage(path!); _workspace.Git.Stage(path!);
} }
@ -469,6 +473,7 @@ namespace MergeHub.ViewModels
SavePlainCommand.NotifyCanExecuteChanged(); SavePlainCommand.NotifyCanExecuteChanged();
AutoMergeCommand.NotifyCanExecuteChanged(); AutoMergeCommand.NotifyCanExecuteChanged();
AcceptBlockCommand.NotifyCanExecuteChanged();
NextConflictCommand.NotifyCanExecuteChanged(); NextConflictCommand.NotifyCanExecuteChanged();
PrevConflictCommand.NotifyCanExecuteChanged(); PrevConflictCommand.NotifyCanExecuteChanged();
} }
@ -1520,7 +1525,11 @@ namespace MergeHub.ViewModels
var content = BuildLocalText(); var content = BuildLocalText();
var target = RightFile; 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; if (!ok) return;
_workspace.OnUi(() => _workspace.OnUi(() =>
@ -1694,6 +1703,9 @@ namespace MergeHub.ViewModels
/// <summary>지금 캐럿이 놓인 행. 편집기 줄 번호와 1:1 이다.</summary> /// <summary>지금 캐럿이 놓인 행. 편집기 줄 번호와 1:1 이다.</summary>
[ObservableProperty] private int _caretRow; [ObservableProperty] private int _caretRow;
/// <summary>줄을 옮기면 "이 덩어리 반영"이 켜지고 꺼진다.</summary>
partial void OnCaretRowChanged(int value) => AcceptBlockCommand.NotifyCanExecuteChanged();
/// <summary>상태바에 띄우는 "차이 3 / 15".</summary> /// <summary>상태바에 띄우는 "차이 3 / 15".</summary>
[ObservableProperty] private string _diffPosition = "차이 -"; [ObservableProperty] private string _diffPosition = "차이 -";
@ -2023,6 +2035,57 @@ namespace MergeHub.ViewModels
RefreshProblems(); RefreshProblems();
} }
// ================= 저장 형식 · 덩어리 반영 =================
/// <summary>
/// 저장할 때 쓸 인코딩·줄바꿈. 기본은 "원본 유지"라 지금까지와 똑같이 동작한다.
///
/// 사내 소스에는 CP949 로 저장된 파일이 섞여 있어 새로 만드는 파일과 형식이 갈린다.
/// 읽어서 보여 주기만 하면 그 갈림을 못 고치므로 저장할 때 고를 수 있게 한다.
/// </summary>
public IReadOnlyList<string> SaveEncodings => TextFile.SaveEncodings;
public IReadOnlyList<string> SaveNewLines => TextFile.SaveNewLines;
[ObservableProperty] private string _saveEncoding = TextFile.KeepOriginal;
[ObservableProperty] private string _saveNewLine = TextFile.KeepOriginal;
/// <summary>
/// 지금 캐럿이 있는 <b>차이 덩어리</b> 전체를 BASE 내용으로 바꾼다.
///
/// 줄 하나씩만 되면 연속 다섯 줄을 다섯 번 눌러야 한다 - WinMerge·Beyond Compare 가
/// 덩어리를 기본 단위로 쓰는 이유다. 덩어리는 "차이로 잡힌 줄이 이어지는 구간"이다.
/// </summary>
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}줄 반영");
}
/// <summary>그 줄이 속한 차이 덩어리. 차이가 아니면 빈 목록이다.</summary>
private List<MergeLine> BlockAround(int row)
{
var found = new List<MergeLine>();
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;
}
// ================= 자동 병합 · 충돌만 보기 ================= // ================= 자동 병합 · 충돌만 보기 =================
/// <summary> /// <summary>

View File

@ -955,6 +955,7 @@
Background="{DynamicResource Ubisam.Brush.Input.Background}" Background="{DynamicResource Ubisam.Brush.Input.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Accent}" BorderThickness="1" BorderBrush="{DynamicResource Ubisam.Brush.Accent}" BorderThickness="1"
Effect="{DynamicResource Ubisam.Effect.Popup}"> Effect="{DynamicResource Ubisam.Effect.Popup}">
<StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBox x:Name="FindBox" Width="180" Height="24" MinHeight="24" <TextBox x:Name="FindBox" Width="180" Height="24" MinHeight="24"
FontSize="11" VerticalContentAlignment="Center" FontSize="11" VerticalContentAlignment="Center"
@ -977,6 +978,22 @@
FontSize="10" Background="Transparent" BorderThickness="0" FontSize="10" Background="Transparent" BorderThickness="0"
ToolTip="닫기 (Esc)"/> ToolTip="닫기 (Esc)"/>
</StackPanel> </StackPanel>
<!-- 바꾸기 줄은 Ctrl+H 로 열 때만 뜬다. 찾기만 할 때 자리를 뺏지 않는다. -->
<StackPanel x:Name="ReplaceRow" Orientation="Horizontal" Margin="0,4,0,0"
Visibility="Collapsed">
<TextBox x:Name="ReplaceBox" Width="180" Height="24" MinHeight="24"
FontSize="11" VerticalContentAlignment="Center"
ToolTip="바꿀 말"/>
<Button Content="바꾸기" Click="Replace_Click" Padding="8,0" MinHeight="24"
FontSize="11" Margin="6,0,2,0"
ToolTip="고른 자리를 바꾸고 다음으로 넘어갑니다"/>
<Button Content="모두" Click="ReplaceAll_Click" Padding="8,0" MinHeight="24"
FontSize="11"
ToolTip="글 전체에서 한 번에 바꿉니다 (Ctrl+Z 로 되돌립니다)"/>
</StackPanel>
</StackPanel>
</Border> </Border>
</Grid> </Grid>
@ -1224,6 +1241,24 @@
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}" Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"
ToolTip="선택한 줄을 REMOTE(Theirs) 내용으로"/> ToolTip="선택한 줄을 REMOTE(Theirs) 내용으로"/>
<Button Content="이 덩어리 반영" Command="{Binding AcceptBlockCommand}"
MinHeight="30" Margin="2,4,2,0" FontSize="11"
ToolTip="차이가 이어지는 구간을 한 번에 반영합니다 (Alt+Shift+Right)"/>
<!--
저장 형식. 기본은 "원본 유지"라 지금까지와 똑같이 나간다.
사내 소스에 CP949 가 섞여 있어, 새로 만드는 파일과 형식이 갈릴 때 여기서 맞춘다.
-->
<TextBlock Text="저장 형식" Style="{StaticResource Muted}" FontSize="11" Margin="0,12,0,4"/>
<UniformGrid Columns="2">
<ComboBox ItemsSource="{Binding SaveEncodings}" SelectedItem="{Binding SaveEncoding}"
MinHeight="26" FontSize="11" Margin="2,0"
ToolTip="저장할 때 쓸 인코딩"/>
<ComboBox ItemsSource="{Binding SaveNewLines}" SelectedItem="{Binding SaveNewLine}"
MinHeight="26" FontSize="11" Margin="2,0"
ToolTip="저장할 때 쓸 줄바꿈"/>
</UniformGrid>
<!-- <!--
3-Way 흐름. "안 겹치는 변경은 자동으로, 사람은 충돌만" 이 순서다. 3-Way 흐름. "안 겹치는 변경은 자동으로, 사람은 충돌만" 이 순서다.
남은 충돌이 0 이 되면 그 자리에서 다 됐다고 알려 준다. 남은 충돌이 0 이 되면 그 자리에서 다 됐다고 알려 준다.

View File

@ -7,6 +7,7 @@ using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using MergeHub.Models; using MergeHub.Models;
using MergeHub.Services; using MergeHub.Services;
using UbisamBase.Core.Messaging;
using MergeHub.ViewModels; using MergeHub.ViewModels;
namespace MergeHub.Views namespace MergeHub.Views
@ -720,7 +721,10 @@ namespace MergeHub.Views
vm.ToggleBookmarkCommand.Execute(vm.SelectedLine); vm.ToggleBookmarkCommand.Execute(vm.SelectedLine);
return true; return true;
case ShortcutAction.Find: OpenFind(); return true; case ShortcutAction.AcceptBlock: return Run(vm.AcceptBlockCommand);
case ShortcutAction.Find: OpenFind(showReplace: false); return true;
case ShortcutAction.Replace: OpenFind(showReplace: true); return true;
case ShortcutAction.FindNext: FindStep(forward: true); return true; case ShortcutAction.FindNext: FindStep(forward: true); return true;
case ShortcutAction.FindPrev: FindStep(forward: false); return true; case ShortcutAction.FindPrev: FindStep(forward: false); return true;
@ -755,15 +759,16 @@ namespace MergeHub.Views
/// 찾기 칸을 연다. 글자를 골라 둔 채로 눌렀으면 그 말을 그대로 채워 준다 - /// 찾기 칸을 연다. 글자를 골라 둔 채로 눌렀으면 그 말을 그대로 채워 준다 -
/// 화면에서 눈에 띈 이름을 다시 타이핑하지 않게 하려는 것이다. /// 화면에서 눈에 띈 이름을 다시 타이핑하지 않게 하려는 것이다.
/// </summary> /// </summary>
private void OpenFind() private void OpenFind(bool showReplace)
{ {
FindBar.Visibility = Visibility.Visible; FindBar.Visibility = Visibility.Visible;
ReplaceRow.Visibility = showReplace ? Visibility.Visible : Visibility.Collapsed;
var picked = LocalEditor.SelectedText; var picked = LocalEditor.SelectedText;
if (!string.IsNullOrEmpty(picked) && !picked.Contains("\n")) FindBox.Text = picked; if (!string.IsNullOrEmpty(picked) && !picked.Contains("\n")) FindBox.Text = picked;
FindBox.Focus(); if (showReplace) { ReplaceBox.Focus(); ReplaceBox.SelectAll(); }
FindBox.SelectAll(); else { FindBox.Focus(); FindBox.SelectAll(); }
} }
private void CloseFind_Click(object sender, RoutedEventArgs e) => CloseFind(); private void CloseFind_Click(object sender, RoutedEventArgs e) => CloseFind();
@ -778,6 +783,71 @@ namespace MergeHub.Views
private void FindNext_Click(object sender, RoutedEventArgs e) => FindStep(forward: true); private void FindNext_Click(object sender, RoutedEventArgs e) => FindStep(forward: true);
private void FindPrev_Click(object sender, RoutedEventArgs e) => FindStep(forward: false); private void FindPrev_Click(object sender, RoutedEventArgs e) => FindStep(forward: false);
/// <summary>
/// 지금 고른 자리를 바꾸고 다음으로 넘어간다.
///
/// 고른 자리가 찾는 말이 아니면 바꾸지 않고 <b>먼저 찾기만 한다</b> -
/// 엉뚱한 자리를 덮어쓰는 것보다 한 번 더 누르게 하는 편이 낫다.
/// </summary>
private void Replace_Click(object sender, RoutedEventArgs e)
{
var needle = FindBox.Text;
if (string.IsNullOrEmpty(needle)) return;
var how = FindMatchCase.IsChecked == true
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
if (LocalEditor.SelectionLength == needle.Length &&
string.Equals(LocalEditor.SelectedText, needle, how))
{
LocalEditor.SelectedText = ReplaceBox.Text;
}
FindStep(forward: true);
}
/// <summary>글 전체에서 한 번에 바꾼다. 되돌리기는 편집기의 Ctrl+Z 가 맡는다.</summary>
private void ReplaceAll_Click(object sender, RoutedEventArgs e)
{
var needle = FindBox.Text;
if (string.IsNullOrEmpty(needle)) return;
var how = FindMatchCase.IsChecked == true
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
var text = LocalEditor.Text;
var count = CountMatches(text, needle, how);
if (count == 0) { FindStatus.Text = "없음"; return; }
var caret = LocalEditor.CaretIndex;
LocalEditor.Text = ReplaceAll(text, needle, ReplaceBox.Text, how);
LocalEditor.CaretIndex = Math.Min(caret, LocalEditor.Text.Length);
FindStatus.Text = $"{count}개 바꿈";
MessageUtil.ShowSuccessToast($"{count}개를 바꿨습니다");
}
private static string ReplaceAll(string text, string needle, string with, StringComparison how)
{
var sb = new System.Text.StringBuilder(text.Length);
var at = 0;
while (true)
{
var next = text.IndexOf(needle, at, how);
if (next < 0) break;
sb.Append(text, at, next - at).Append(with);
at = next + needle.Length;
}
sb.Append(text, at, text.Length - at);
return sb.ToString();
}
/// <summary>찾기 칸에서 Enter 는 다음, Shift+Enter 는 이전, Esc 는 닫기.</summary> /// <summary>찾기 칸에서 Enter 는 다음, Shift+Enter 는 이전, Esc 는 닫기.</summary>
private void FindBox_KeyDown(object sender, KeyEventArgs e) private void FindBox_KeyDown(object sender, KeyEventArgs e)
{ {
@ -804,7 +874,7 @@ namespace MergeHub.Views
var needle = FindBox.Text; var needle = FindBox.Text;
if (string.IsNullOrEmpty(needle)) if (string.IsNullOrEmpty(needle))
{ {
OpenFind(); OpenFind(showReplace: ReplaceRow.Visibility == Visibility.Visible);
return; return;
} }