Skip to content

Commit 47c1010

Browse files
sanohiroclaude
andcommitted
fix: コメントハイライトとC-j表示バグを修正
- コメント入力時に即座にグレー表示されるよう修正 - 差分描画でANSIエスケープが途切れる問題を回避 - グレーハイライトがある行は常に行全体を再描画 - C-j (newline-and-indent) で行シフト時の表示崩れを修正 - markDirty()をmarkFullRedraw()に変更 - 行結合/移動/複製等の行シフト操作も同様に修正 - テストを統合: test_basic_operations.shをrun_all_tests.shにマージ - テストハーネスに--expectオプションと\nエスケープ対応を追加 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ca4b4b5 commit 47c1010

7 files changed

Lines changed: 247 additions & 427 deletions

File tree

run_all_tests.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ run_test() {
4242
fi
4343
}
4444

45+
# 内容検証付きテスト実行ヘルパー(--expect使用)
46+
run_test_verify() {
47+
local test_name="$1"
48+
shift
49+
# test_data/ のパスを /tmp/ze_test_data/ に置換
50+
local args=()
51+
for arg in "$@"; do
52+
local replaced_arg=$(echo "$arg" | sed 's|test_data/|/tmp/ze_test_data/|g')
53+
args+=("$replaced_arg")
54+
done
55+
local output
56+
output=$($HARNESS "${args[@]}" 2>&1)
57+
if echo "$output" | grep -q "PASS: Content matches!"; then
58+
test_result "$test_name" "PASS"
59+
elif echo "$output" | grep -q "Child exited with status: 0"; then
60+
# --expectなしの場合は終了コードで判断
61+
test_result "$test_name" "PASS"
62+
else
63+
test_result "$test_name" "FAIL"
64+
fi
65+
}
66+
4567
echo "========================================="
4668
echo "ze エディタ 統合テストスイート"
4769
echo "========================================="
@@ -317,6 +339,20 @@ run_test "28.6 ウィンドウ切り替え (C-Tab)" --file=test_data/test_multiw
317339
run_test "28.7 C-v ページダウン" --file=test_data/test_page_scroll.txt "C-v" "C-x" "C-c"
318340
run_test "28.8 M-v ページアップ" --file=test_data/test_page_scroll.txt "C-v" "M-v" "C-x" "C-c"
319341

342+
echo
343+
echo "=== カテゴリ 29: 改行操作とC-j ==="
344+
run_test "29.1 行頭でEnter" --file=test_data/test_cursor_input.txt "Enter" "C-x" "C-c" "n"
345+
run_test "29.2 行途中でEnter" --file=test_data/test_cursor_input.txt "Right" "Right" "Enter" "C-x" "C-c" "n"
346+
run_test "29.3 行末でEnter" --file=test_data/test_cursor_input.txt "End" "Enter" "C-x" "C-c" "n"
347+
run_test "29.4 行末でC-j" --file=test_data/test_cursor_input.txt "End" "C-j" "C-x" "C-c" "n"
348+
run_test "29.5 行頭でC-j" --file=test_data/test_cursor_input.txt "C-j" "C-x" "C-c" "n"
349+
run_test "29.6 行途中でC-j" --file=test_data/test_cursor_input.txt "Right" "Right" "C-j" "C-x" "C-c" "n"
350+
run_test "29.7 C-j後に文字入力" --file=test_data/test_cursor_input.txt "End" "C-j" "new" "C-x" "C-c" "n"
351+
# 内容検証テスト
352+
run_test_verify "29.8 C-j内容検証" --file=test_data/test_newline.txt "End" "C-j" "inserted" "C-x" "C-s" "C-x" "C-c" '--expect=line1\ninserted'
353+
run_test_verify "29.9 行頭C-j内容検証" --file=test_data/test_newline.txt "C-j" "C-x" "C-s" "C-x" "C-c" '--expect=\nline1'
354+
run_test_verify "29.10 行複製内容検証 (C-c d)" --file=test_data/test_newline.txt "C-c" "d" "C-x" "C-s" "C-x" "C-c" '--expect=line1\nline1'
355+
320356
echo
321357
echo "========================================="
322358
echo "統合テスト完了"

src/commands/edit.zig

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ pub fn joinLine(e: *Editor) !void {
374374
}
375375

376376
buffer_state.editing_ctx.modified = true;
377-
markDirtyAll(e, current_line - 1, null);
377+
// joinLineは行数が減るため全画面再描画が必要
378+
markFullRedrawAll(e);
378379

379380
e.setCursorToPos(delete_start);
380381

@@ -466,7 +467,8 @@ pub fn moveLineUp(e: *Editor) !void {
466467
try e.recordInsert(prev_line_start, line_content, view.getCursorBufferPos());
467468

468469
buffer_state.editing_ctx.modified = true;
469-
markDirtyAll(e, current_line - 1, null);
470+
// moveLineUpは行の入れ替えのため全画面再描画が必要
471+
markFullRedrawAll(e);
470472

471473
view.moveCursorUp();
472474
}
@@ -500,7 +502,8 @@ pub fn moveLineDown(e: *Editor) !void {
500502
try e.recordInsert(new_insert_pos, line_content, view.getCursorBufferPos());
501503

502504
buffer_state.editing_ctx.modified = true;
503-
markDirtyAll(e, current_line, null);
505+
// moveLineDownは行の入れ替えのため全画面再描画が必要
506+
markFullRedrawAll(e);
504507

505508
view.moveCursorDown();
506509
}
@@ -608,7 +611,8 @@ pub fn duplicateLine(e: *Editor) !void {
608611
try e.recordInsert(insert_pos, to_insert, view.getCursorBufferPos());
609612

610613
buffer_state.editing_ctx.modified = true;
611-
markDirtyAll(e, current_line, null);
614+
// duplicateLineは行数が増えるため全画面再描画が必要
615+
markFullRedrawAll(e);
612616

613617
// カーソルを複製した行に移動
614618
view.moveCursorDown();

src/editor.zig

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,12 +2405,13 @@ pub const Editor = struct {
24052405
const max_screen_line = if (view.viewport_height >= 2) view.viewport_height - 2 else 0;
24062406
if (view.cursor_y < max_screen_line) {
24072407
view.cursor_y += 1;
2408-
view.markDirty(current_line, null); // EOF まで再描画
24092408
} else {
24102409
// 画面の最下部の場合はスクロール
24112410
view.top_line += 1;
2412-
view.markFullRedraw(); // スクロール時は全画面再描画
24132411
}
2412+
// 改行は行シフトを起こすため、prev_screenキャッシュが無効になる
2413+
// markDirtyでは差分描画が壊れるので全画面再描画が必要
2414+
view.markFullRedraw();
24142415
view.cursor_x = 0;
24152416
view.top_col = 0; // 水平スクロールもリセット
24162417
} else {
@@ -2593,9 +2594,16 @@ pub const Editor = struct {
25932594
// カーソルを置換後の位置に移動
25942595
self.setCursorToPos(match_pos + replacement.len);
25952596

2596-
// 置換した行から再描画(差分レンダリング)
2597-
const current_line = buffer.findLineByPos(match_pos);
2598-
self.getCurrentView().markDirty(current_line, null);
2597+
// 置換した行から再描画
2598+
// 検索文字列・置換文字列に改行が含まれる場合は行数が変わるため全画面再描画
2599+
const has_newline = std.mem.indexOf(u8, search, "\n") != null or
2600+
std.mem.indexOf(u8, replacement, "\n") != null;
2601+
if (has_newline) {
2602+
self.getCurrentView().markFullRedraw();
2603+
} else {
2604+
const current_line = buffer.findLineByPos(match_pos);
2605+
self.getCurrentView().markDirty(current_line, current_line);
2606+
}
25992607
}
26002608

26012609
// ========================================
@@ -2870,7 +2878,8 @@ pub const Editor = struct {
28702878
self.getCurrentBuffer().editing_ctx.modified = true;
28712879
self.setCursorToPos(start);
28722880
self.window_manager.getCurrentWindow().mark_pos = null; // マークをクリア
2873-
self.getCurrentView().markDirty(0, null);
2881+
// シェル出力は行数が変わる可能性があるため全画面再描画
2882+
self.getCurrentView().markFullRedraw();
28742883
}
28752884
} else {
28762885
// 選択なしの場合はカーソル位置に挿入(+> と同じ動作)
@@ -2880,7 +2889,8 @@ pub const Editor = struct {
28802889
try buf.insertSlice(pos, stdout);
28812890
try self.recordInsert(pos, stdout, pos);
28822891
self.getCurrentBuffer().editing_ctx.modified = true;
2883-
self.getCurrentView().markDirty(0, null);
2892+
// シェル出力は行数が変わる可能性があるため全画面再描画
2893+
self.getCurrentView().markFullRedraw();
28842894
}
28852895
}
28862896
},
@@ -2900,7 +2910,8 @@ pub const Editor = struct {
29002910
}
29012911
self.getCurrentBuffer().editing_ctx.modified = true;
29022912
self.setCursorToPos(0);
2903-
self.getCurrentView().markDirty(0, null);
2913+
// シェル出力は行数が変わる可能性があるため全画面再描画
2914+
self.getCurrentView().markFullRedraw();
29042915
},
29052916
.current_line => {
29062917
// 現在行を置換
@@ -2921,7 +2932,8 @@ pub const Editor = struct {
29212932
}
29222933
self.getCurrentBuffer().editing_ctx.modified = true;
29232934
self.setCursorToPos(line_start);
2924-
self.getCurrentView().markDirty(line_num, null);
2935+
// シェル出力は行数が変わる可能性があるため全画面再描画
2936+
self.getCurrentView().markFullRedraw();
29252937
},
29262938
}
29272939
},
@@ -2933,7 +2945,8 @@ pub const Editor = struct {
29332945
try buf.insertSlice(pos, stdout);
29342946
try self.recordInsert(pos, stdout, pos);
29352947
self.getCurrentBuffer().editing_ctx.modified = true;
2936-
self.getCurrentView().markDirty(0, null);
2948+
// シェル出力は行数が変わる可能性があるため全画面再描画
2949+
self.getCurrentView().markFullRedraw();
29372950
}
29382951
},
29392952
.new_buffer => {

src/view.zig

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ fn calculateScreenColumn(line: []const u8, start_byte: usize, end_byte: usize, i
137137
return screen_col;
138138
}
139139

140+
/// 行の本文部分(行番号の後)にANSIグレーコード(\x1b[90m)が含まれているかチェック
141+
/// コメントハイライトの有無を判定するために使用
142+
fn hasAnsiGray(line: []const u8, content_start: usize) bool {
143+
// 行番号の後ろの部分のみチェック
144+
if (content_start >= line.len) return false;
145+
const content = line[content_start..];
146+
// グレーのANSIコード \x1b[90m を探す
147+
return std.mem.indexOf(u8, content, "\x1b[90m") != null;
148+
}
149+
140150
pub const View = struct {
141151
buffer: *Buffer,
142152
top_line: usize,
@@ -691,84 +701,97 @@ pub const View = struct {
691701
}
692702
}
693703

694-
// 差分を検出
695-
const min_len = @min(old_line.len, new_line.len);
696-
var diff_start: ?usize = null;
697-
var diff_end: usize = 0;
704+
// 行番号の表示幅を取得
705+
const line_num_display_width = self.getLineNumberWidth();
698706

699-
// 前方から差分を探す
700-
for (0..min_len) |i| {
701-
if (old_line[i] != new_line[i]) {
702-
if (diff_start == null) diff_start = i;
703-
diff_end = i + 1;
707+
// 行番号がある場合、行番号部分のバイト範囲を推定
708+
// 行番号フォーマット: "\x1b[90m N\x1b[m " (ANSIエスケープ付き)
709+
const line_num_byte_end = if (line_num_display_width > 0) blk: {
710+
// 行番号部分のバイト終端を探す(最初のリセットエスケープ \x1b[m の後のスペース2つ)
711+
if (std.mem.indexOf(u8, new_line, "\x1b[m ")) |pos| {
712+
break :blk pos + 5; // "\x1b[m ".len
704713
}
705-
}
706-
707-
// 長さが違う場合は後ろも差分
708-
if (old_line.len != new_line.len) {
709-
if (diff_start == null) diff_start = min_len;
710-
diff_end = @max(old_line.len, new_line.len);
711-
}
712-
713-
if (diff_start) |start_raw| {
714-
// 行番号の表示幅を取得
715-
const line_num_display_width = self.getLineNumberWidth();
716-
717-
// 行番号がある場合、行番号部分のバイト範囲を推定してスキップ
718-
// 行番号フォーマット: "\x1b[90m N\x1b[m " (ANSIエスケープ付き)
719-
// 行番号がない、または差分が行番号より後から始まる場合は部分更新が可能
720-
const line_num_byte_end = if (line_num_display_width > 0) blk: {
721-
// 行番号部分のバイト終端を探す(最初のリセットエスケープ \x1b[m の後のスペース2つ)
722-
if (std.mem.indexOf(u8, new_line, "\x1b[m ")) |pos| {
723-
break :blk pos + 5; // "\x1b[m ".len
724-
}
725-
// フォールバック:行全体を再描画
726-
break :blk 0;
727-
} else 0;
728-
729-
// 差分が行番号部分にかかっている場合は行全体を再描画
730-
if (line_num_display_width > 0 and start_raw < line_num_byte_end) {
731-
// 行頭から全体を再描画
714+
// フォールバック:行全体を再描画
715+
break :blk 0;
716+
} else 0;
717+
718+
// シンタックスハイライト(ANSIエスケープ)の変化をチェック
719+
// コメント色のANSIコードがある場合は行全体を再描画(差分描画ではカラーが正しく適用されない)
720+
const old_has_gray = hasAnsiGray(old_line, line_num_byte_end);
721+
const new_has_gray = hasAnsiGray(new_line, line_num_byte_end);
722+
// グレーがある場合は行全体を再描画(差分描画だとカラーが途切れる問題を回避)
723+
if (old_has_gray or new_has_gray) {
724+
// 内容が異なる場合のみ再描画
725+
if (!std.mem.eql(u8, old_line, new_line)) {
732726
try term.moveCursor(abs_row, viewport_x);
733727
try term.write(new_line);
734-
// 残りをスペースで埋める(CLEAR_LINEの代わり)
735728
try self.padToWidth(term, new_line, viewport_width);
736-
} else if (line_num_display_width > 0) {
737-
// 行番号があるが本文部分のみの差分
738-
// UTF-8文字境界に調整
739-
var start = start_raw;
740-
while (start > 0 and start < new_line.len and unicode.isUtf8Continuation(new_line[start])) {
741-
start -= 1;
729+
}
730+
} else {
731+
// 差分を検出
732+
const min_len = @min(old_line.len, new_line.len);
733+
var diff_start: ?usize = null;
734+
var diff_end: usize = 0;
735+
736+
// 前方から差分を探す
737+
for (0..min_len) |i| {
738+
if (old_line[i] != new_line[i]) {
739+
if (diff_start == null) diff_start = i;
740+
diff_end = i + 1;
742741
}
742+
}
743743

744-
// バイト位置から画面カラム位置を計算(行番号部分をスキップ)
745-
const screen_col = calculateScreenColumn(new_line, line_num_byte_end, start, line_num_display_width);
746-
747-
// 差分部分のみ描画
748-
try term.moveCursor(abs_row, viewport_x + screen_col);
749-
try term.write(new_line[start..]);
744+
// 長さが違う場合は後ろも差分
745+
if (old_line.len != new_line.len) {
746+
if (diff_start == null) diff_start = min_len;
747+
diff_end = @max(old_line.len, new_line.len);
748+
}
750749

751-
// 古い行の方が長い場合は残りをスペースで埋める
752-
if (old_line.len > new_line.len) {
750+
if (diff_start) |start_raw| {
751+
// 差分が行番号部分にかかっている場合は行全体を再描画
752+
if (line_num_display_width > 0 and start_raw < line_num_byte_end) {
753+
// 行頭から全体を再描画
754+
try term.moveCursor(abs_row, viewport_x);
755+
try term.write(new_line);
756+
// 残りをスペースで埋める(CLEAR_LINEの代わり)
753757
try self.padToWidth(term, new_line, viewport_width);
754-
}
755-
} else {
756-
// UTF-8文字境界に調整(継続バイトの途中から始まらないように)
757-
var start = start_raw;
758-
while (start > 0 and start < new_line.len and unicode.isUtf8Continuation(new_line[start])) {
759-
start -= 1;
760-
}
758+
} else if (line_num_display_width > 0) {
759+
// 行番号があるが本文部分のみの差分
760+
// UTF-8文字境界に調整
761+
var start = start_raw;
762+
while (start > 0 and start < new_line.len and unicode.isUtf8Continuation(new_line[start])) {
763+
start -= 1;
764+
}
761765

762-
// バイト位置から画面カラム位置を計算(ANSIエスケープシーケンスをスキップ
763-
const screen_col = calculateScreenColumn(new_line, 0, start, 0);
766+
// バイト位置から画面カラム位置を計算(行番号部分をスキップ
767+
const screen_col = calculateScreenColumn(new_line, line_num_byte_end, start, line_num_display_width);
764768

765-
// 差分部分のみ描画(viewport_xオフセットを加算)
766-
try term.moveCursor(abs_row, viewport_x + screen_col);
767-
try term.write(new_line[start..]);
769+
// 差分部分のみ描画
770+
try term.moveCursor(abs_row, viewport_x + screen_col);
771+
try term.write(new_line[start..]);
768772

769-
// 古い行の方が長い場合は残りをスペースで埋める
770-
if (old_line.len > new_line.len) {
771-
try self.padToWidth(term, new_line, viewport_width);
773+
// 古い行の方が長い場合は残りをスペースで埋める
774+
if (old_line.len > new_line.len) {
775+
try self.padToWidth(term, new_line, viewport_width);
776+
}
777+
} else {
778+
// UTF-8文字境界に調整(継続バイトの途中から始まらないように)
779+
var start = start_raw;
780+
while (start > 0 and start < new_line.len and unicode.isUtf8Continuation(new_line[start])) {
781+
start -= 1;
782+
}
783+
784+
// バイト位置から画面カラム位置を計算(ANSIエスケープシーケンスをスキップ)
785+
const screen_col = calculateScreenColumn(new_line, 0, start, 0);
786+
787+
// 差分部分のみ描画(viewport_xオフセットを加算)
788+
try term.moveCursor(abs_row, viewport_x + screen_col);
789+
try term.write(new_line[start..]);
790+
791+
// 古い行の方が長い場合は残りをスペースで埋める
792+
if (old_line.len > new_line.len) {
793+
try self.padToWidth(term, new_line, viewport_width);
794+
}
772795
}
773796
}
774797
}

0 commit comments

Comments
 (0)