[<] 단추가 있는 줄만 글이 밀리던 것 - 단추 자리를 늘 잡아 둔다

사용자가 그림으로 짚어 준 것. 12차 B-1 로 넣은 [<] 단추가 든 칸의 폭이 Auto 여서,
단추가 보이는 줄에서만 칸이 벌어져 뒤 칸이 통째로 오른쪽으로 밀렸다.
같은 칸 안에서도 줄마다 글이 시작하는 자리가 달랐다.

  BASE                          LOCAL
    01  class Axis                01  class Axis
  < 03      int speed = 100;      03 >     int speed = 200;   <- 밀림
  < 04      // left only line     05 > <     void Stop() { }  <- 더 밀림
    05  void Home() { }           06  }

BASE 는 [<] 가 맨 앞 칸이라 번호까지, LOCAL 은 번호 뒤 칸이라 코드가 밀렸고,
단추가 하나 늘 때마다 한 칸씩 더 밀렸다.

Visibility="Collapsed" 은 자리까지 없애므로, 자리를 지키려면 칸 폭이 고정이어야 한다.

- GutterBothWaysWidth 를 두어 BASE · LOCAL 이 같은 자리를 늘 잡아 둔다
- 화면이 모드에 따라 갈아 끼운다(ApplyGutterReserve) -
  두 파일 비교는 단추 폭, Git 모드와 네 칸은 0(그 단추가 아예 없다 - 여백이 예전 그대로)
- 투명한 편집기가 아래 표시층과 어긋나면 캐럿이 글자와 다른 자리에 선다 -
  EditorGutterMargin 도 같은 값만큼 민다
- 글자 크기(11차 F-5)를 바꾸면 단추 폭이 따라 바뀌므로, 잡아 두는 자리도
  CodeFontMetrics 한 벌에서 나온다

화면으로 확인: 한 칸의 모든 줄이 같은 x(BASE 137 · LOCAL 1283),
글자 크기를 키워도 한 x(147), Git 모드도 한 x(459). 오류 글 0.
E-2 화면 세 벌 · 왼쪽 번호 한 벌 모두 그대로 통과. 빌드 경고 0 · 오류 0.

남겨 둔 것: BASE 줄 끝의 [>](11차 F-10)는 아직 Auto 다. 왼쪽 가장자리를 밀지 않고
오른쪽에서 코드 칸을 조금 좁힐 뿐이라 눈에 띄지 않는다. 워드랩을 켜면 그 줄만
접히는 자리가 달라질 수 있다 - 워드랩은 기본이 꺼짐이라 이번에는 두었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-28 14:09:52 +09:00
parent 62126e5e0e
commit 06d78e2fc7
5 changed files with 98 additions and 4 deletions

View File

@ -285,6 +285,60 @@ BASE 칸 자신은 견줄 것이 없어 `4줄 · 기준` 이라고만 적는다.
실제로 켜서 잰 하단 탭 줄이 **111px · 항목 109px**(배율 150% 기준 72.7 DIP)로 "후" 값과 맞았다.
앞서 적었던 "지금 배포 폴더에는 예전 것이 그대로" 는 그 뒤 빌드로 낡은 글이 되어 고쳤다.
## B-1 을 쓰다 나온 것 (2) - [<] 단추가 있는 줄만 글이 밀린다 (2026-09-28, 사용자 보고)
사용자가 그림으로 짚어 준 것이 <b>이것</b>이었다. 아래 "왼쪽 줄 번호" 건은 이것을 쫓다 따로 찾은 다른 흠이다.
### 먼저 재현 (화면, 값)
`[<]` 단추가 보이는 줄만 코드가 오른쪽으로 밀려, <b>같은 칸 안에서도 줄마다 글 시작 자리가 달랐다.</b>
```
BASE LOCAL
01 class Axis 01 class Axis
02 { 02 {
< 03 int speed = 100; 03 > int speed = 200; ← 밀림
< 04 // left only line 04 void Home() { }
05 void Home() { } 05 > < void Stop() { } ← 더 밀림
06 } 06 }
```
오른쪽 칸은 단추가 하나 늘 때마다 한 칸씩 더 밀렸다.
### 까닭
단추가 든 칸의 폭이 `Auto` 였다. 단추가 안 보이는 줄에서는 0 이 되고 보이는 줄에서는 단추 폭만큼 벌어지니,
그 줄만 뒤 칸이 통째로 오른쪽으로 밀린다. `Visibility="Collapsed"` 는 <b>자리까지 없앤다</b> -
자리를 지키려면 칸 폭이 고정이어야 한다.
- BASE 는 `[<]` 가 맨 앞 칸(`Auto`)이라 **번호까지 함께** 밀렸다.
- LOCAL 은 `[<]` 가 번호 뒤 칸(`Auto`)이라 **코드만** 밀렸다.
### 고친 것
`GutterBothWaysWidth` 를 두고 두 칸이 같은 값을 쓴다. 화면이 모드에 따라 0 ↔ 단추 폭으로 갈아 끼운다(`ApplyGutterReserve`).
- **두 파일 비교**: 단추 폭만큼 늘 잡아 둔다 - 단추가 없는 줄도 같은 자리에서 글이 시작한다.
- **Git 모드 · 네 칸**: `[<]` 가 아예 없으므로 0 - 여백이 예전 그대로라 코드 칸이 좁아지지 않는다.
- **편집기 여백**(`EditorGutterMargin`)도 같은 값만큼 민다. 투명한 편집기와 아래 표시층이 어긋나면
캐럿이 글자와 다른 자리에 선다.
- 글자 크기(11차 F-5)를 바꾸면 단추 폭도 따라 바뀌므로, 잡아 두는 자리도 `CodeFontMetrics` 한 벌에서 나온다.
### 화면으로 확인 (값)
| 확인 | 값 |
|---|---|
| 두 파일 비교 BASE | 여섯 줄 모두 `x=137` (전에는 단추 줄만 달랐다) |
| 두 파일 비교 LOCAL | 여섯 줄 모두 `x=1283` |
| 글자 크기 키운 뒤 | 모두 한 x(`147`) |
| Git 모드(회귀) | 모두 한 x(`459`) · 여백 예전 그대로 |
### 남겨 둔 것
BASE 줄 <b>끝</b>의 `[>]`(LOCAL 에서 지운 줄 되살리기, 11차 F-10)는 아직 `Auto` 다. 이것은 왼쪽 가장자리를
밀지 않고 오른쪽에서 코드 칸을 조금 좁힐 뿐이라 눈에 띄지 않는다. 워드랩을 켜면 그 줄만 접히는 자리가
달라질 수 있다 - 워드랩은 기본이 꺼짐이고, 손대면 네 칸 폭을 다시 맞춰야 해서 이번에는 두었다.
## B-1 을 쓰다 나온 것 - 왼쪽 줄 번호가 밀린다 (2026-09-28, 사용자 보고)
### 먼저 재현 (화면, 값)

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
| 2026-09-28 | **`[<]` 단추가 있는 줄만 글이 밀리던 것**(사용자 보고 — 그림으로 짚어 준 것이 이것이다). 12차 B-1 로 넣은 `[<]` 단추가 든 칸의 폭이 `Auto` 여서, 단추가 보이는 줄에서만 칸이 벌어져 뒤 칸이 통째로 오른쪽으로 밀렸다 — BASE 는 맨 앞 칸이라 <b>번호까지</b>, LOCAL 은 번호 뒤 칸이라 <b>코드가</b> 밀렸고 단추가 하나 늘 때마다 더 밀렸다. `Visibility="Collapsed"` 는 자리까지 없애므로 칸 폭이 고정이어야 한다. `GutterBothWaysWidth` 를 두어 두 칸이 같은 자리를 늘 잡아 두고, 두 파일 비교에서는 단추 폭 · Git 모드와 네 칸에서는 0 으로 화면이 갈아 끼운다(`ApplyGutterReserve`). 투명한 편집기가 아래 표시층과 어긋나지 않도록 `EditorGutterMargin` 도 같은 값만큼 민다. 글자 크기(11차 F-5)를 바꾸면 단추 폭이 따라 바뀌므로 잡아 두는 자리도 `CodeFontMetrics` 한 벌에서 나온다. 값으로 확인: 한 칸의 모든 줄이 같은 x(BASE 137 · LOCAL 1283), 글자 크기 · Git 모드도 그대로. BASE 줄 끝의 `[>]`(11차 F-10)는 왼쪽 가장자리를 밀지 않아 이번에는 `Auto` 로 두었다 | 아래 |
| 2026-09-28 | **왼쪽 줄 번호가 밀리던 것**(사용자 보고) — 12차 B-1 로 왼쪽에도 반영할 수 있게 되면서 왼쪽 줄이 늘고 주는데, 번호는 정렬기가 처음 준 것 그대로였다. 오른쪽에만 있던 줄을 왼쪽에 넣으면 그 줄은 번호가 비고 뒤 줄은 옛 번호를 달아 `01 02 · 03 04 05 · 06` 으로 보였다 — 왼쪽 파일은 8줄인데 번호는 6까지라 어느 줄을 고쳤는지 짚을 수 없었다. `RenumberBase` 를 `RenumberMerged`(8차 B-3) 옆에 두고 두 파일 비교에서만 왼쪽 줄을 1부터 다시 맨다. Git 모드 · 네 칸의 BASE 는 고칠 수 없는 쪽이라 건드리지 않는다. "머지 반영함" 표시가 BASE 줄 번호를 열쇠로 붙들고 있어 번호가 실제로 달라질 때만 그 열쇠도 옛 번호 → 새 번호로 옮긴다. 화면으로 넣기 · 빼기 · 되돌리기 · 저장 · Git 회귀 확인, 하네스 27벌 1,787건 · 실패 0. **왼쪽에만 있는 줄을 뺄 때 줄 수가 주는지는 값으로 확정하지 못했다**(시험 도구가 낡은 줄 칸을 집는 한계) | 아래 |
| 2026-09-28 | **12차 E-2** — 파일 비교가 짝 여러 개를 동시에 든다(문서 탭). 지금까지는 한 짝만 들어서 짝을 바꾸면 "저장하지 않은 편집이 있습니다" 를 묻고 버리면 고친 글이 사라졌다(재현: 16줄 → 15줄). 이제 떠 있던 탭이 그 글을 맡아 두므로 **묻지 않는다**. 탭마다 `HomeViewModel` 을 따로 만들지 않고(GitWorkspace 의 한 자리짜리 약속 때문) 화면은 하나로 두고 탭이 **파일에 없는 것만** 맡는다 — 고친 글 · 왼쪽 글(B-1) · 되돌리기 이력 · 두 기준점 · 캐럿 · 스크롤 · 저장 형식. 탭 이름은 파일 이름(다르면 `왼 ↔ 오`, 겹치면 위 폴더를 갈릴 때까지 덧붙임), 저장 안 한 탭은 `•`. 탭 줄은 둘 이상일 때만 보인다. `◀ 이전 · 다음 ▶` 은 지금 탭의 짝만 바꾸고(탭이 늘지 않음), 폴더 비교에서 두 번 누르면 새 탭(사용자 결정). Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W · 가운데 단추 닫기. 탭이 사라져도 고친 글은 **보관함**(짝 50개)에 남아 다시 열면 되살아난다 — E-1 은 이 보관함을 파일로 옮겨 적으면 된다. 원본이 그 사이 바뀌면 되살리고 알린다("연 시점"은 새로 읽은 글로 둔다). 탭 줄은 ItemsControl 이 아니라 ListBox 다(ItemsControl 은 UIA 에 컨트롤로 안 보여 화면 시험이 못 읽는다). 켤 때 저절로 열리는 짝만 탭이 없어 편집이 사라지던 것을 화면에서 찾아 `EnsureTabForCurrentPair` 로 고쳤다. 켜 놓고 눈으로 보다 두 가지를 더 고쳤다(사용자가 고름) — 중괄호 맞춤으로만 바뀐 것에는 점을 붙이지 않고(묻는 자리와 같은 기준), 탭 이름은 양쪽이 같은 부분을 한 번만 적는다(`WeldController_기준.cs ↔ WeldController_내작...` → `WeldController_기준 ↔ 내작업.cs`, 421 → 288px). 뒤쪽은 확장자일 때만 접는다 — 글자 단위로 세면 `a-lef ↔ right.cs` 처럼 낱말 가운데서 잘린다(하네스에서 걸렸다). 하네스 `DocTabTest` 106건, 화면 34건 · 회귀 19건 통과, 전체 27벌 1,787건 · 실패 0. **커밋 안 함**(사용자 지시) | 아래 |
| 2026-09-28 | **시험 도구를 런처에 맞춤** — 새 플랫폼은 `UbisamBase.Launcher.exe` 가 셸을 같은 프로세스에 올려 창 주인 프로세스 이름이 바뀌었고, 예전 이름만 찾던 화면 시험 도구가 전부 "창이 안 뜬다" 로 멈춰 있었다. `a2lib.ps1` 에 `Shell-Procs` · `Shell-Proc` · `Stop-App` 을 두고 `Start-App` · `Get-Win` 이 그것을 쓰게 했다(런처가 제목 없는 진행 창을 먼저 띄우는 경우를 대비해 제목 있는 창을 먼저 고름). 스크립트 76개 · 156곳을 함께 고침. 문법 187개 통과, 끄기 → 켜기 → 창 찾기 → 네 탭 → 단추 → 끄기까지 실제로 돌았고 설정 · 창 자리 네 파일 해시 그대로 되돌림. **제품 코드 변경 없음** | 없음 |

View File

@ -430,7 +430,8 @@
| 상태 | 항목 |
|---|---|
| **완료** | 왼쪽 줄 번호가 밀린다(사용자 보고) — 12차 B-1 로 왼쪽에도 반영할 수 있게 됐는데 왼쪽 번호를 다시 매기는 자리가 없었다. 줄을 넣으면 그 줄은 번호가 비고 뒤 줄은 옛 번호(`01 02 · 03 04 05 · 06`), 저장하면 8줄인데 번호는 6까지. `RenumberBase` 를 두어 두 파일 비교에서만 왼쪽 줄을 1부터 다시 맨다. "머지 반영함" 표시가 BASE 번호를 열쇠로 쓰므로 그 열쇠도 함께 옮긴다. Git · 네 칸은 손대지 않는다 |
| **완료** | `[<]` 단추가 있는 줄만 글이 밀린다(사용자 보고, 그림으로 짚어 줌) — 단추 칸 폭이 `Auto` 라 단추가 보이는 줄만 뒤 칸이 밀렸다(BASE 는 번호까지, LOCAL 은 코드가). `GutterBothWaysWidth` 로 두 칸이 같은 자리를 늘 잡아 두고, 모드에 따라 0 ↔ 단추 폭으로 갈아 끼운다. 편집기 여백도 같이 민다. 값으로 확인: 한 칸의 모든 줄이 같은 x(BASE 137 · LOCAL 1283), 글자 크기 · Git 모드도 그대로 |
| **완료** | 왼쪽 줄 번호가 밀린다(위를 쫓다 따로 찾음) — 12차 B-1 로 왼쪽에도 반영할 수 있게 됐는데 왼쪽 번호를 다시 매기는 자리가 없었다. 줄을 넣으면 그 줄은 번호가 비고 뒤 줄은 옛 번호(`01 02 · 03 04 05 · 06`), 저장하면 8줄인데 번호는 6까지. `RenumberBase` 를 두어 두 파일 비교에서만 왼쪽 줄을 1부터 다시 맨다. "머지 반영함" 표시가 BASE 번호를 열쇠로 쓰므로 그 열쇠도 함께 옮긴다. Git · 네 칸은 손대지 않는다 |
| 볼 것 | 왼쪽에만 있는 줄을 `[<]` 로 뺄 때 줄 수가 실제로 주는지 — 따로 열면 되는데 다른 반영 뒤에는 시험 도구가 낡은 줄 칸을 집어 값이 흔들렸다. 번호 불변식은 지켜짐 |
### 시험 도구

View File

@ -34,6 +34,10 @@
<GridLength x:Key="GutterNarrowNumberWidth">40</GridLength>
<GridLength x:Key="GutterMarkWidth">6</GridLength>
<GridLength x:Key="GutterButtonWidth">20</GridLength>
<!-- 두 파일 비교에서만 나오는 [&lt;] 단추 자리(12차 B-1). Auto 로 두면 단추가 있는 줄만
글이 오른쪽으로 밀려, 같은 칸 안에서도 줄마다 글 시작 자리가 달랐다(2026-09-28 사용자 보고).
모드가 바뀔 때 화면이 0 ↔ 단추 폭으로 갈아 끼운다(ApplyGutterReserve). -->
<GridLength x:Key="GutterBothWaysWidth">0</GridLength>
<system:Double x:Key="GutterButtonHeight">14</system:Double>
<system:Double x:Key="GutterButtonFontSize">9</system:Double>
<system:Double x:Key="BookmarkHeight">10</system:Double>
@ -160,7 +164,8 @@
66 인 까닭은 LocalLineTemplate 에 적었다(6차 B-4). -->
<Grid MinHeight="{Binding BaseRowHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- [<] 자리. 단추가 없는 줄에서도 자리를 잡아 둔다 - Auto 로 두면 이 줄만 글이 밀린다. -->
<ColumnDefinition Width="{DynamicResource GutterBothWaysWidth}"/>
<ColumnDefinition Width="{DynamicResource GutterWidth}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@ -365,7 +370,8 @@
<ColumnDefinition Width="{DynamicResource GutterMarkWidth}"/>
<ColumnDefinition Width="{DynamicResource GutterNarrowNumberWidth}"/>
<ColumnDefinition Width="{DynamicResource GutterButtonWidth}"/>
<ColumnDefinition Width="Auto"/>
<!-- [<] 자리(12차 B-1). BASE 와 같은 값이라 두 칸의 글 시작 자리가 늘 같다. -->
<ColumnDefinition Width="{DynamicResource GutterBothWaysWidth}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

View File

@ -473,6 +473,14 @@ namespace MergeHub.Views
return;
}
// 두 파일 비교 ↔ Git 을 오가면 [<] 단추가 생기고 사라진다 - 잡아 둘 자리도 함께 바뀐다.
if (e.PropertyName == nameof(HomeViewModel.BothWays))
{
ApplyGutterReserve();
ScheduleResync();
return;
}
if (e.PropertyName != nameof(HomeViewModel.IsThreeWay)) return;
ScheduleResync();
@ -539,6 +547,28 @@ namespace MergeHub.Views
/// <see cref="CodeFontMetrics"/> 한 벌로 바꾼다 - 넷 가운데 하나라도 따로 놀면 행이 어긋난다.
/// 행 높이의 하한(<see cref="MergeLine.MinimumRowHeight"/>)도 같이 옮겨야 글자를 줄였을 때 행이 준다.
/// </summary>
/// <summary>마지막으로 건 크기 한 벌. 모드가 바뀔 때 여백만 다시 잡으려고 들고 있는다.</summary>
private CodeFontMetrics _metrics = CodeFontMetrics.For(CodeFontMetrics.Default);
/// <summary>
/// 두 파일 비교에서만 나오는 <c>[&lt;]</c> 단추 자리를 <b>줄마다 늘 잡아 둔다</b>(2026-09-28 사용자 보고).
///
/// 그 칸을 <c>Auto</c> 로 두었더니 단추가 보이는 줄에서만 칸이 벌어져, <b>같은 칸 안에서도
/// 줄마다 글이 시작하는 자리가 달랐다</b> - 화면에서는 03 · 04 줄만 오른쪽으로 밀려 보였다.
/// 모드에 따라 0 ↔ 단추 폭으로 갈아 끼우면, 한 모드 안에서는 모든 줄이 같은 자리에서 시작한다.
/// Git 모드는 왼쪽이 지난 커밋이라 이 단추가 아예 없으므로 0 - 그때는 여백이 예전 그대로다.
///
/// <para>편집기 여백(<c>EditorGutterMargin</c>)도 같은 값만큼 밀어야 한다 - 투명한 편집기와
/// 아래 표시층이 어긋나면 캐럿이 글자와 다른 자리에 선다.</para>
/// </summary>
private void ApplyGutterReserve()
{
var reserve = (DataContext as HomeViewModel)?.BothWays == true ? _metrics.ButtonWidth : 0;
Resources["GutterBothWaysWidth"] = new GridLength(reserve);
Resources["EditorGutterMargin"] = new Thickness(_metrics.GutterWidth + reserve, 0, 0, 0);
}
private void ApplyCodeFont(double size)
{
var m = CodeFontMetrics.For(size);
@ -553,7 +583,9 @@ namespace MergeHub.Views
Resources["GutterButtonHeight"] = m.ButtonHeight;
Resources["GutterButtonFontSize"] = m.ButtonFontSize;
Resources["BookmarkHeight"] = m.BookmarkHeight;
Resources["EditorGutterMargin"] = new Thickness(m.GutterWidth, 0, 0, 0);
_metrics = m;
ApplyGutterReserve();
if (MergeLine.MinimumRowHeight != m.LineHeight)
{