파일 비교: 바꾸기(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:
parent
524a6a715d
commit
de1a43cc70
14
doc/진행상황.md
14
doc/진행상황.md
@ -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 | 화면 확인 |
|
||||||
| 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 |
|
| 대기 | 바이너리·이미지 비교 | 감지해서 안내만 하고 끝난다 | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -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, "찾기", "찾기 칸 닫기"),
|
||||||
|
|||||||
@ -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 이 들어가지 않는다.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 이 되면 그 자리에서 다 됐다고 알려 준다.
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user