Fix local editor raw mode and chrome

This commit is contained in:
slhx agent
2026-06-21 15:22:41 +02:00
parent f75ee1ddb4
commit 629605bcdc
2 changed files with 249 additions and 23 deletions
+208 -19
View File
@@ -319,23 +319,29 @@ pub const Client = struct {
const max_body_lines = self.viewport.height - 1;
const cursor_line = lineIndexAt(snap.bytes, snap.cursor_byte);
const cursor_col = columnAt(snap.bytes, snap.cursor_byte);
const total_lines = countDocumentLines(snap.bytes);
const gutter_digits = @max(@as(usize, 2), decimalDigits(total_lines));
const gutter_width = @min(self.viewport.width, gutter_digits + 2);
const content_width = if (self.viewport.width > gutter_width) self.viewport.width - gutter_width else 0;
var visible_line_index: usize = 0;
var body_lines_used: usize = 0;
var line_iter = std.mem.splitScalar(u8, snap.bytes, '\n');
while (line_iter.next()) |line| : (visible_line_index += 1) {
if (body_lines_used >= max_body_lines) break;
try appendVisibleCells(allocator, &out, line, self.viewport.width);
try out.append(allocator, '\n');
try appendEditorLine(
allocator,
&out,
line,
visible_line_index + 1,
gutter_digits,
content_width,
visible_line_index == cursor_line,
cursor_col,
);
body_lines_used += 1;
if (visible_line_index == cursor_line and body_lines_used < max_body_lines) {
try appendCursorLine(allocator, &out, @min(cursor_col, self.viewport.width - 1), self.viewport.width);
body_lines_used += 1;
}
}
while (body_lines_used < max_body_lines) : (body_lines_used += 1) {
try out.append(allocator, '~');
try out.append(allocator, '\n');
try appendVirtualLine(allocator, &out, gutter_digits, content_width);
}
const leader_status = self.leader.status();
@@ -360,7 +366,7 @@ pub const Client = struct {
.{ @tagName(self.effectiveMode()), cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
);
defer allocator.free(status);
try appendVisibleCells(allocator, &out, status, self.viewport.width);
try appendStatusLine(allocator, &out, status, self.viewport.width);
return out.toOwnedSlice(allocator);
}
@@ -445,6 +451,11 @@ pub const Client = struct {
return @tagName(self.effectiveMode());
}
pub fn enterInsertMode(self: *Client) void {
self.mode = .insert;
self.message = "insert";
}
pub fn pendingRailName(self: *const Client) []const u8 {
return @tagName(self.prefix);
}
@@ -2086,9 +2097,14 @@ pub const Client = struct {
}
fn insertText(self: *Client, text: []const u8) !void {
const line = try std.fmt.allocPrint(self.allocator, "insert {s}", .{text});
defer self.allocator.free(line);
try self.applyMutatingProtocolCommand(line);
try self.recordUndo();
self.clearRedo();
self.session.dispatch(.{ .insert = text }) catch |err| {
self.dropLastUndoSnapshot();
return err;
};
self.noteDocumentChanged();
self.message = null;
}
fn applyMutatingProtocolCommand(self: *Client, line: []const u8) !void {
@@ -2295,6 +2311,97 @@ pub const Client = struct {
}
};
const ansi_reset = "\x1b[0m";
const ansi_gutter = "\x1b[38;2;88;96;112m";
const ansi_virtual = "\x1b[38;2;64;70;86m";
const ansi_text = "\x1b[38;2;214;222;235m";
const ansi_current_line = "\x1b[48;2;26;31;43m";
const ansi_cursor = "\x1b[38;2;18;22;30;48;2;245;197;92m";
const ansi_status = "\x1b[38;2;18;22;30;48;2;126;231;135m";
fn appendEditorLine(
allocator: std.mem.Allocator,
out: *std.ArrayList(u8),
line: []const u8,
line_no: usize,
gutter_digits: usize,
content_width: usize,
is_cursor_line: bool,
cursor_col: usize,
) !void {
try appendLineNumber(allocator, out, line_no, gutter_digits);
if (content_width == 0) {
try out.append(allocator, '\n');
return;
}
if (is_cursor_line) try out.appendSlice(allocator, ansi_current_line);
try out.appendSlice(allocator, ansi_text);
try appendEditorCells(allocator, out, line, content_width, is_cursor_line, cursor_col);
try out.appendSlice(allocator, ansi_reset);
try out.append(allocator, '\n');
}
fn appendLineNumber(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line_no: usize, gutter_digits: usize) !void {
const text = try std.fmt.allocPrint(allocator, "{d}", .{line_no});
defer allocator.free(text);
try out.appendSlice(allocator, ansi_gutter);
var pad: usize = text.len;
while (pad < gutter_digits) : (pad += 1) try out.append(allocator, ' ');
try out.appendSlice(allocator, text);
try out.appendSlice(allocator, "");
try out.appendSlice(allocator, ansi_reset);
try out.append(allocator, ' ');
}
fn appendVirtualLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), gutter_digits: usize, content_width: usize) !void {
_ = content_width;
try out.appendSlice(allocator, ansi_gutter);
var pad: usize = 0;
while (pad < gutter_digits) : (pad += 1) try out.append(allocator, ' ');
try out.appendSlice(allocator, "");
try out.appendSlice(allocator, ansi_virtual);
try out.appendSlice(allocator, " ·");
try out.appendSlice(allocator, ansi_reset);
try out.append(allocator, '\n');
}
fn appendEditorCells(
allocator: std.mem.Allocator,
out: *std.ArrayList(u8),
bytes: []const u8,
max_cells: usize,
is_cursor_line: bool,
cursor_col: usize,
) !void {
_ = cursor_col;
var remaining = max_cells;
if (is_cursor_line and remaining > 0) {
try out.appendSlice(allocator, ansi_cursor);
try out.appendSlice(allocator, "");
try out.appendSlice(allocator, ansi_reset);
try out.appendSlice(allocator, ansi_current_line);
try out.appendSlice(allocator, ansi_text);
remaining -= 1;
}
var i: usize = 0;
var col: usize = 0;
while (i < bytes.len and col < remaining) {
const len = std.unicode.utf8ByteSequenceLength(bytes[i]) catch 1;
const end = @min(bytes.len, i + len);
const width = @max(@as(usize, 1), session_mod.cellWidth(bytes[i..end]));
if (col + width > remaining) break;
try out.appendSlice(allocator, bytes[i..end]);
col += width;
i = end;
}
}
fn appendStatusLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), status: []const u8, width: usize) !void {
try out.appendSlice(allocator, ansi_status);
try appendVisibleCells(allocator, out, status, width);
try out.appendSlice(allocator, ansi_reset);
}
fn appendVisibleCells(allocator: std.mem.Allocator, out: *std.ArrayList(u8), bytes: []const u8, max_cells: usize) !void {
var i: usize = 0;
while (i < bytes.len) {
@@ -2307,11 +2414,37 @@ fn appendVisibleCells(allocator: std.mem.Allocator, out: *std.ArrayList(u8), byt
}
}
fn appendCursorLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), cursor_col: usize, max_cells: usize) !void {
fn countDocumentLines(bytes: []const u8) usize {
var lines: usize = 1;
for (bytes) |byte| {
if (byte == '\n') lines += 1;
}
return lines;
}
fn decimalDigits(value: usize) usize {
var digits: usize = 1;
var n = value;
while (n >= 10) : (digits += 1) n /= 10;
return digits;
}
fn visibleCellWidth(bytes: []const u8) usize {
var i: usize = 0;
while (i < cursor_col and i + 1 < max_cells) : (i += 1) try out.append(allocator, ' ');
try out.append(allocator, '^');
try out.append(allocator, '\n');
var cells: usize = 0;
while (i < bytes.len) {
if (bytes[i] == 0x1b and i + 1 < bytes.len and bytes[i + 1] == '[') {
i += 2;
while (i < bytes.len and bytes[i] != 'm') : (i += 1) {}
if (i < bytes.len) i += 1;
continue;
}
const len = std.unicode.utf8ByteSequenceLength(bytes[i]) catch 1;
const end = @min(bytes.len, i + len);
cells += session_mod.cellWidth(bytes[i..end]);
i = end;
}
return cells;
}
fn lineIndexAt(bytes: []const u8, cursor_byte: usize) usize {
@@ -2340,7 +2473,7 @@ fn diagnoseFrame(viewport: Viewport, frame: []const u8) RenderDiagnostics {
var lines = std.mem.splitScalar(u8, frame, '\n');
while (lines.next()) |line| {
diagnostics.frame_lines += 1;
diagnostics.max_line_cells = @max(diagnostics.max_line_cells, session_mod.cellWidth(line));
diagnostics.max_line_cells = @max(diagnostics.max_line_cells, visibleCellWidth(line));
}
return diagnostics;
}
@@ -2377,7 +2510,7 @@ fn renderWarningAlloc(allocator: std.mem.Allocator, diagnostics: RenderDiagnosti
fn assertLinesFit(frame: []const u8, width: usize) !void {
var lines = std.mem.splitScalar(u8, frame, '\n');
while (lines.next()) |line| {
try std.testing.expect(session_mod.cellWidth(line) <= width);
try std.testing.expect(visibleCellWidth(line) <= width);
}
}
@@ -4712,3 +4845,59 @@ test "adversarial: unknown physical key names stay rejected and leader help stay
defer std.testing.allocator.free(frame);
try assertLinesFit(frame, 72);
}
test "regular: editor render has colorscheme gutter cursor and status chrome" {
var client = try Client.init(std.testing.allocator, .{ .width = 40, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open alpha\nbeta");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[38;2;") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, " 1│") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, " 2│") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[38;2;18;22;30;48;2;126;231;135m") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "^\n") == null);
try assertLinesFit(frame, 40);
}
test "regular: empty editor still shows first line gutter and cursor marker" {
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 5 });
defer client.deinit();
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, " 1│") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, " ·") != null);
try assertLinesFit(frame, 24);
}
test "regular: local launcher insert mode lets ordinary text with spaces insert" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open ");
client.enterInsertMode();
for ("what is up") |byte| {
const key_bytes = [_]u8{byte};
try client.handleInput(&key_bytes);
}
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("what is up", snap.bytes);
}
test "regular: normal mode Space leader still works after insert escape" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open ");
client.enterInsertMode();
for ("abc") |byte| {
const key_bytes = [_]u8{byte};
try client.handleInput(&key_bytes);
}
try client.handleTraceLine("key escape");
try client.handleInput(" ");
try client.handleInput("w");
try std.testing.expectEqualStrings("abc", try client.saved());
}