Skip to content

Commit b1e31fb

Browse files
sanohiroclaude
andcommitted
perf: Codex指摘事項の最適化実装
- config.Buffer定数を使用(buffer.zig) INITIAL_ADD_CAPACITY, INITIAL_PIECES_CAPACITYを活用 - Shell enums重複解消(editor.zig) shell_service.zigからインポート、重複定義を削除 - dirty状態による描画スキップ(editor.zig, view.zig) needsRedraw()でウィンドウ変更を確認、変更なしの場合は カーソル移動のみでレンダリングをスキップ - computeBlockCommentState最適化(view.zig) キャッシュされた行位置から増分スキャン、O(n)→O(k)へ - Cell差分の行番号対応(view.zig) 行番号部分を検出し、コンテンツ部分のみの差分更新を許可 全169テスト成功 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f9ede92 commit b1e31fb

3 files changed

Lines changed: 120 additions & 40 deletions

File tree

src/buffer.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,8 @@ pub const Buffer = struct {
297297
pub fn init(allocator: std.mem.Allocator) !Buffer {
298298
return Buffer{
299299
.original = &[_]u8{},
300-
.add_buffer = try std.ArrayList(u8).initCapacity(allocator, 0),
301-
.pieces = try std.ArrayList(Piece).initCapacity(allocator, 0),
300+
.add_buffer = try std.ArrayList(u8).initCapacity(allocator, config.Buffer.INITIAL_ADD_CAPACITY),
301+
.pieces = try std.ArrayList(Piece).initCapacity(allocator, config.Buffer.INITIAL_PIECES_CAPACITY),
302302
.allocator = allocator,
303303
.owns_original = false,
304304
.is_mmap = false,

src/editor.zig

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ const poller = @import("poller.zig");
4444

4545
const Minibuffer = @import("services/minibuffer.zig").Minibuffer;
4646
const SearchService = @import("services/search_service.zig").SearchService;
47-
const ShellService = @import("services/shell_service.zig").ShellService;
47+
const shell_service_mod = @import("services/shell_service.zig");
48+
const ShellService = shell_service_mod.ShellService;
49+
const ShellOutputDest = shell_service_mod.OutputDest;
50+
const ShellInputSource = shell_service_mod.InputSource;
4851
const BufferManager = @import("services/buffer_manager.zig").BufferManager;
4952
const BufferState = @import("services/buffer_manager.zig").BufferState;
5053
const WindowManager = @import("services/window_manager.zig").WindowManager;
@@ -81,21 +84,6 @@ const EditorMode = enum {
8184
mx_key_describe, // M-x key: 次のキー入力を待っている
8285
};
8386

84-
/// シェルコマンド出力先
85-
const ShellOutputDest = enum {
86-
command_buffer, // Command Bufferに表示(デフォルト)
87-
replace, // 入力元を置換 (>)
88-
insert, // カーソル位置に挿入 (+>)
89-
new_buffer, // 新規バッファ (n>)
90-
};
91-
92-
/// シェルコマンド入力元
93-
const ShellInputSource = enum {
94-
selection, // 選択範囲(なければ空)
95-
buffer_all, // バッファ全体 (%)
96-
current_line, // 現在行 (.)
97-
};
98-
9987
/// シェルコマンドの非同期実行状態
10088
///
10189
/// 【非同期実行の仕組み】
@@ -1199,6 +1187,25 @@ pub const Editor = struct {
11991187

12001188
/// 全ウィンドウをレンダリング
12011189
fn renderAllWindows(self: *Editor) !void {
1190+
// 描画が必要かチェック(全ウィンドウがdirtyでないならスキップ)
1191+
var needs_render = false;
1192+
for (self.window_manager.iterator()) |*window| {
1193+
if (window.view.needsRedraw()) {
1194+
needs_render = true;
1195+
break;
1196+
}
1197+
}
1198+
1199+
// 描画が不要でもカーソル位置は更新する
1200+
if (!needs_render) {
1201+
// アクティブウィンドウのカーソル位置を更新
1202+
const window = self.window_manager.getCurrentWindowConst();
1203+
const pos = window.view.getCursorScreenPosition(window.x, window.y, window.width);
1204+
try self.terminal.moveCursor(pos.row, pos.col);
1205+
try self.terminal.flush();
1206+
return;
1207+
}
1208+
12021209
// 描画中はカーソルを非表示(ちらつき防止)
12031210
try self.terminal.hideCursor();
12041211

@@ -2525,20 +2532,7 @@ pub const Editor = struct {
25252532
// コマンド完了 - 結果を処理
25262533
defer self.shell_service.finish();
25272534

2528-
// ShellServiceの型をエディタの型に変換
2529-
const input_source: ShellInputSource = switch (result.input_source) {
2530-
.selection => .selection,
2531-
.buffer_all => .buffer_all,
2532-
.current_line => .current_line,
2533-
};
2534-
const output_dest: ShellOutputDest = switch (result.output_dest) {
2535-
.command_buffer => .command_buffer,
2536-
.replace => .replace,
2537-
.insert => .insert,
2538-
.new_buffer => .new_buffer,
2539-
};
2540-
2541-
try self.processShellResult(result.stdout, result.stderr, result.exit_status, input_source, output_dest);
2535+
try self.processShellResult(result.stdout, result.stderr, result.exit_status, result.input_source, result.output_dest);
25422536
self.mode = .normal;
25432537
}
25442538
// まだ実行中の場合は何もしない

src/view.zig

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ pub const View = struct {
291291
}
292292

293293
// 行番号の表示幅を計算(999行まで固定、1000行以上で動的拡張)
294-
pub fn getLineNumberWidth(self: *View) usize {
294+
pub fn getLineNumberWidth(self: *const View) usize {
295295
if (!config.Editor.SHOW_LINE_NUMBERS) return 0;
296296

297297
const total_lines = self.buffer.lineCount();
@@ -372,8 +372,13 @@ pub const View = struct {
372372
self.prev_top_line = self.top_line;
373373
}
374374

375+
/// 再描画が必要かどうか
376+
pub fn needsRedraw(self: *const View) bool {
377+
return self.needs_full_redraw or self.dirty_start != null;
378+
}
379+
375380
/// 指定行より前の行をスキャンして、ブロックコメント内で開始するかを判定
376-
/// キャッシュを使用してO(n)からO(1)に最適化(top_lineが変わらない場合
381+
/// キャッシュを使用してO(n)からO(k)に最適化(kはキャッシュ位置からの差分行数
377382
fn computeBlockCommentState(self: *View, target_line: usize) bool {
378383
// ブロックコメントがない言語なら常にfalse
379384
if (self.language.block_comment == null) return false;
@@ -385,13 +390,31 @@ pub const View = struct {
385390
}
386391
}
387392

388-
// キャッシュミス: 再計算
389-
var in_block = false;
393+
// キャッシュからスタートできるか確認(キャッシュ行 <= target_line の場合のみ)
394+
var start_line: usize = 0;
395+
var in_block: bool = false;
396+
if (self.cached_block_state) |cached_state| {
397+
if (self.cached_block_top_line < target_line) {
398+
// キャッシュ位置からスキャン開始
399+
start_line = self.cached_block_top_line;
400+
in_block = cached_state;
401+
}
402+
}
403+
404+
// start_lineからtarget_lineまでスキャン
390405
var iter = PieceIterator.init(self.buffer);
391406
var line_buf = std.ArrayList(u8){};
392407
defer line_buf.deinit(self.allocator);
393408

409+
// start_lineまでスキップ
394410
var current_line: usize = 0;
411+
while (current_line < start_line) : (current_line += 1) {
412+
while (iter.next()) |ch| {
413+
if (ch == '\n') break;
414+
}
415+
}
416+
417+
// start_lineからtarget_lineまでスキャン
395418
while (current_line < target_line) : (current_line += 1) {
396419
// 行を読み取る
397420
line_buf.clearRetainingCapacity();
@@ -515,14 +538,77 @@ pub const View = struct {
515538
// 行番号の表示幅を取得
516539
const line_num_display_width = self.getLineNumberWidth();
517540

518-
// 行番号がある場合は常に行全体を再描画
519-
// (ANSIシーケンスの計算が複雑なため、シンプルな実装を優先)
520-
if (line_num_display_width > 0) {
541+
// 行番号がある場合、行番号部分のバイト範囲を推定してスキップ
542+
// 行番号フォーマット: "\x1b[90m N\x1b[m " (ANSIエスケープ付き)
543+
// 行番号がない、または差分が行番号より後から始まる場合は部分更新が可能
544+
const line_num_byte_end = if (line_num_display_width > 0) blk: {
545+
// 行番号部分のバイト終端を探す(最初のリセットエスケープ \x1b[m の後のスペース2つ)
546+
if (std.mem.indexOf(u8, new_line, "\x1b[m ")) |pos| {
547+
break :blk pos + 5; // "\x1b[m ".len
548+
}
549+
// フォールバック:行全体を再描画
550+
break :blk 0;
551+
} else 0;
552+
553+
// 差分が行番号部分にかかっている場合は行全体を再描画
554+
if (line_num_display_width > 0 and start_raw < line_num_byte_end) {
521555
// 行頭から全体を再描画
522556
try term.moveCursor(abs_row, viewport_x);
523557
try term.write(new_line);
524558
// 残りをスペースで埋める(CLEAR_LINEの代わり)
525559
try self.padToWidth(term, new_line, viewport_width);
560+
} else if (line_num_display_width > 0) {
561+
// 行番号があるが本文部分のみの差分
562+
// 差分開始位置から画面カラム位置を計算
563+
var start = start_raw;
564+
while (start > 0 and start < new_line.len and unicode.isUtf8Continuation(new_line[start])) {
565+
start -= 1;
566+
}
567+
568+
// バイト位置から画面カラム位置を計算(行番号部分をスキップ)
569+
var screen_col: usize = 0;
570+
var b: usize = line_num_byte_end;
571+
// 行番号分の表示幅を加算
572+
screen_col = line_num_display_width;
573+
574+
while (b < start) {
575+
const c = new_line[b];
576+
// ANSIエスケープシーケンスをスキップ
577+
if (b + 1 < new_line.len and unicode.isAnsiEscapeStart(c, new_line[b + 1])) {
578+
b += 2;
579+
while (b < new_line.len and new_line[b] != 'm') : (b += 1) {}
580+
if (b < new_line.len) b += 1;
581+
continue;
582+
}
583+
584+
if (unicode.isAsciiByte(c)) {
585+
b += 1;
586+
screen_col += 1;
587+
} else {
588+
const len = unicode.utf8SeqLen(c);
589+
if (b + len <= new_line.len) {
590+
const cp = std.unicode.utf8Decode(new_line[b..b + len]) catch {
591+
b += 1;
592+
screen_col += 1;
593+
continue;
594+
};
595+
screen_col += unicode.displayWidth(cp);
596+
b += len;
597+
} else {
598+
b += 1;
599+
screen_col += 1;
600+
}
601+
}
602+
}
603+
604+
// 差分部分のみ描画
605+
try term.moveCursor(abs_row, viewport_x + screen_col);
606+
try term.write(new_line[start..]);
607+
608+
// 古い行の方が長い場合は残りをスペースで埋める
609+
if (old_line.len > new_line.len) {
610+
try self.padToWidth(term, new_line, viewport_width);
611+
}
526612
} else {
527613
// UTF-8文字境界に調整(継続バイトの途中から始まらないように)
528614
var start = start_raw;
@@ -875,7 +961,7 @@ pub const View = struct {
875961
}
876962

877963
/// アクティブウィンドウ用のカーソル位置を計算
878-
pub fn getCursorScreenPosition(self: *View, viewport_x: usize, viewport_y: usize, viewport_width: usize) struct { row: usize, col: usize } {
964+
pub fn getCursorScreenPosition(self: *const View, viewport_x: usize, viewport_y: usize, viewport_width: usize) struct { row: usize, col: usize } {
879965
const line_num_width = self.getLineNumberWidth();
880966
var screen_cursor_x = line_num_width + (if (self.cursor_x >= self.top_col) self.cursor_x - self.top_col else 0);
881967
// viewport幅を超えないようにクリップ(幅0の場合も考慮)

0 commit comments

Comments
 (0)