From 1658c9e3b4f258bfe3fdc48109bb58f1aefd5b24 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:16:07 +0200 Subject: [PATCH 1/8] refactor: add crlf mode testcase for Buffer.Node.get_byte_pos --- test/tests_buffer.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/tests_buffer.zig b/test/tests_buffer.zig index 2ba6af4..87eef41 100644 --- a/test/tests_buffer.zig +++ b/test/tests_buffer.zig @@ -190,6 +190,14 @@ test "get_byte_pos" { try std.testing.expectEqual(33, try buffer.root.get_byte_pos(.{ .row = 4, .col = 0 }, metrics(), eol_mode)); try std.testing.expectEqual(66, try buffer.root.get_byte_pos(.{ .row = 8, .col = 0 }, metrics(), eol_mode)); try std.testing.expectEqual(97, try buffer.root.get_byte_pos(.{ .row = 11, .col = 2 }, metrics(), eol_mode)); + + eol_mode = .crlf; + try std.testing.expectEqual(0, try buffer.root.get_byte_pos(.{ .row = 0, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(10, try buffer.root.get_byte_pos(.{ .row = 1, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(12, try buffer.root.get_byte_pos(.{ .row = 1, .col = 2 }, metrics(), eol_mode)); + try std.testing.expectEqual(37, try buffer.root.get_byte_pos(.{ .row = 4, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(74, try buffer.root.get_byte_pos(.{ .row = 8, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(108, try buffer.root.get_byte_pos(.{ .row = 11, .col = 2 }, metrics(), eol_mode)); } test "delete_bytes" { From 935b178d89ce70561e5f4fc5326eab113b1a8fe7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:17:00 +0200 Subject: [PATCH 2/8] feat: add Buffer.Node.byte_offset_to_line_and_col and testcase --- src/buffer/Buffer.zig | 29 +++++++++++++++++++++++++++++ test/tests_buffer.zig | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 7d6c5d9..c736d82 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -794,6 +794,35 @@ const Node = union(enum) { return if (found) ctx.result else error.NotFound; } + pub fn byte_offset_to_line_and_col(self: *const Node, pos: usize, metrics: Metrics, eol_mode: EolMode) Cursor { + const ctx_ = struct { + pos: usize, + line: usize = 0, + col: usize = 0, + eol_mode: EolMode, + fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Metrics) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + if (egc[0] == '\n') { + ctx.pos -= switch (ctx.eol_mode) { + .lf => 1, + .crlf => @min(2, ctx.pos), + }; + if (ctx.pos == 0) return Walker.stop; + ctx.line += 1; + ctx.col = 0; + } else { + ctx.pos -= @min(egc.len, ctx.pos); + if (ctx.pos == 0) return Walker.stop; + ctx.col += wcwidth; + } + return Walker.keep_walking; + } + }; + var ctx: ctx_ = .{ .pos = pos + 1, .eol_mode = eol_mode }; + self.walk_egc_forward(0, ctx_.walker, &ctx, metrics) catch {}; + return .{ .row = ctx.line, .col = ctx.col }; + } + pub fn insert_chars( self_: *const Node, line_: usize, diff --git a/test/tests_buffer.zig b/test/tests_buffer.zig index 87eef41..95ab500 100644 --- a/test/tests_buffer.zig +++ b/test/tests_buffer.zig @@ -414,3 +414,44 @@ test "get_from_pos" { const result3 = buffer.root.get_from_pos(.{ .row = 1, .col = 5 }, &result_buf, metrics()); try std.testing.expectEqualDeep(result3[0 .. line1.len - 4], line1[4..]); } + +test "byte_offset_to_line_and_col" { + const doc: []const u8 = + \\All your + \\ropes + \\are belong to + \\us! + \\All your + \\ropes + \\are belong to + \\us! + \\All your + \\ropes + \\are belong to + \\us! + ; + var eol_mode: Buffer.EolMode = .lf; + var sanitized: bool = false; + const buffer = try Buffer.create(a); + defer buffer.deinit(); + buffer.update(try buffer.load_from_string(doc, &eol_mode, &sanitized)); + + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(8, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 0 }, buffer.root.byte_offset_to_line_and_col(9, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 2 }, buffer.root.byte_offset_to_line_and_col(11, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 4, .col = 0 }, buffer.root.byte_offset_to_line_and_col(33, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 8, .col = 0 }, buffer.root.byte_offset_to_line_and_col(66, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 11, .col = 2 }, buffer.root.byte_offset_to_line_and_col(97, metrics(), eol_mode)); + + eol_mode = .crlf; + + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(8, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(9, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 0 }, buffer.root.byte_offset_to_line_and_col(10, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 2 }, buffer.root.byte_offset_to_line_and_col(12, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 4, .col = 0 }, buffer.root.byte_offset_to_line_and_col(37, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 8, .col = 0 }, buffer.root.byte_offset_to_line_and_col(74, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 11, .col = 2 }, buffer.root.byte_offset_to_line_and_col(108, metrics(), eol_mode)); +} From 18cd62ba7e199e4f43ea0554ee605110f98b572a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:17:48 +0200 Subject: [PATCH 3/8] feat: add editor goto_byte_offset command --- src/tui/editor.zig | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e64168b..6932495 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -5477,6 +5477,28 @@ pub const Editor = struct { } pub const goto_line_and_column_meta: Meta = .{ .arguments = &.{ .integer, .integer } }; + pub fn goto_byte_offset(self: *Self, ctx: Context) Result { + try self.send_editor_jump_source(); + var offset: usize = 0; + if (try ctx.args.match(.{ + tp.extract(&offset), + })) { + // self.logger.print("goto: byte offset:{d}", .{ offset }); + } else return error.InvalidGotoByteOffsetArgument; + self.cancel_all_selections(); + const root = self.buf_root() catch return; + const eol_mode = self.buf_eol_mode() catch return; + const primary = self.get_primary(); + primary.cursor = root.byte_offset_to_line_and_col(offset, self.metrics, eol_mode); + if (self.view.is_visible(&primary.cursor)) + self.clamp() + else + try self.scroll_view_center(.{}); + try self.send_editor_jump_destination(); + self.need_render(); + } + pub const goto_byte_offset_meta: Meta = .{ .arguments = &.{.integer} }; + pub fn goto_definition(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); From 30a457158c2e693bee17c98dc095b9906074c09c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:18:20 +0200 Subject: [PATCH 4/8] feat: add goto_offset mini mode and command --- src/tui/mode/mini/goto_offset.zig | 58 +++++++++++++++++++++++++++++++ src/tui/tui.zig | 5 +++ 2 files changed, 63 insertions(+) create mode 100644 src/tui/mode/mini/goto_offset.zig diff --git a/src/tui/mode/mini/goto_offset.zig b/src/tui/mode/mini/goto_offset.zig new file mode 100644 index 0000000..221f8a1 --- /dev/null +++ b/src/tui/mode/mini/goto_offset.zig @@ -0,0 +1,58 @@ +const fmt = @import("std").fmt; +const command = @import("command"); + +const tui = @import("../../tui.zig"); +const Cursor = @import("../../editor.zig").Cursor; + +pub const Type = @import("numeric_input.zig").Create(@This()); +pub const create = Type.create; + +pub const ValueType = struct { + cursor: Cursor = .{}, + offset: usize = 0, +}; + +pub fn name(_: *Type) []const u8 { + return "#goto byte"; +} + +pub fn start(_: *Type) ValueType { + const editor = tui.get_active_editor() orelse return .{}; + return .{ .cursor = editor.get_primary().cursor }; +} + +pub fn process_digit(self: *Type, digit: u8) void { + switch (digit) { + 0...9 => { + if (self.input) |*input| { + input.offset = input.offset * 10 + digit; + } else { + self.input = .{ .offset = digit }; + } + }, + else => unreachable, + } +} + +pub fn delete(self: *Type, input: *ValueType) void { + const newval = if (input.offset < 10) 0 else input.offset / 10; + if (newval == 0) self.input = null else input.offset = newval; +} + +pub fn format_value(_: *Type, input_: ?ValueType, buf: []u8) []const u8 { + return if (input_) |input| + fmt.bufPrint(buf, "{d}", .{input.offset}) catch "" + else + ""; +} + +pub const preview = goto; +pub const apply = goto; +pub const cancel = goto; + +fn goto(self: *Type, _: command.Context) void { + if (self.input) |input| + command.executeName("goto_byte_offset", command.fmt(.{input.offset})) catch {} + else + command.executeName("goto_line_and_column", command.fmt(.{ self.start.cursor.row, self.start.cursor.col })) catch {}; +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 0577836..f2ecb77 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1038,6 +1038,11 @@ const cmds = struct { } pub const goto_meta: Meta = .{ .description = "Goto line" }; + pub fn goto_offset(self: *Self, ctx: Ctx) Result { + return enter_mini_mode(self, @import("mode/mini/goto_offset.zig"), ctx); + } + pub const goto_offset_meta: Meta = .{ .description = "Goto byte offset" }; + pub fn move_to_char(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx); } From 7c5a22c9591baa2a5f0d9e51eaf251ef46c5e4ba Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:18:45 +0200 Subject: [PATCH 5/8] feat: add goto_offset keybind "b" in goto mini mode This effectively makes `ctrl+g b` the goto_offset keybinding. --- src/keybind/builtin/flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index ca9a7c4..8f0a85a 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -341,6 +341,7 @@ }, "mini/numeric": { "press": [ + ["b", "goto_offset"], ["ctrl+q", "quit"], ["ctrl+v", "system_paste"], ["ctrl+u", "mini_mode_reset"], From 219b8cd00a3dcd1bd3bba994b97fd079ee183bed Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:42:25 +0200 Subject: [PATCH 6/8] feat: support byte offsets in file links This adds support for a 'b' prefix to the first file link argument to denote a byte offset. `{path/to/file.ext}:b{offset}` --- src/file_link.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/file_link.zig b/src/file_link.zig index ec53f39..cc3b817 100644 --- a/src/file_link.zig +++ b/src/file_link.zig @@ -13,6 +13,7 @@ pub const FileDest = struct { column: ?usize = null, end_column: ?usize = null, exists: bool = false, + offset: ?usize = null, }; pub const DirDest = struct { @@ -37,11 +38,17 @@ pub fn parse(link: []const u8) error{InvalidFileLink}!Dest { .{ .file = .{ .path = it.first() } }; switch (dest) { .file => |*file| { - if (it.next()) |line_| + if (it.next()) |line_| if (line_.len > 0 and line_[0] == 'b') { + file.offset = std.fmt.parseInt(usize, line_[1..], 10) catch blk: { + file.path = link; + break :blk null; + }; + } else { file.line = std.fmt.parseInt(usize, line_, 10) catch blk: { file.path = link; break :blk null; }; + }; if (file.line) |_| if (it.next()) |col_| { file.column = std.fmt.parseInt(usize, col_, 10) catch null; }; @@ -88,6 +95,9 @@ pub fn parse_bracket_link(link: []const u8) error{InvalidFileLink}!Dest { pub fn navigate(to: tp.pid_ref, link: *const Dest) anyerror!void { switch (link.*) { .file => |file| { + if (file.offset) |offset| { + return to.send(.{ "cmd", "navigate", .{ .file = file.path, .offset = offset } }); + } if (file.line) |l| { if (file.column) |col| { try to.send(.{ "cmd", "navigate", .{ .file = file.path, .line = l, .column = col } }); From 7228a604b0a8de6ff79c108dd6c308c2d38220db Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:46:35 +0200 Subject: [PATCH 7/8] feat: add byte offset support to vim style '+' cli arguments This adds support for using `+b{offset}` on the command line. --- src/main.zig | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index e914235..ec12528 100644 --- a/src/main.zig +++ b/src/main.zig @@ -245,19 +245,34 @@ pub fn main() anyerror!void { defer links.deinit(); var prev: ?*file_link.Dest = null; var line_next: ?usize = null; + var offset_next: ?usize = null; for (positional_args.items) |arg| { if (arg.len == 0) continue; if (!args.literal and arg[0] == '+') { - const line = try std.fmt.parseInt(usize, arg[1..], 10); - if (prev) |p| switch (p.*) { - .file => |*file| { - file.line = line; - continue; - }, - else => {}, - }; - line_next = line; + if (arg.len > 2 and arg[1] == 'b') { + const offset = try std.fmt.parseInt(usize, arg[2..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.offset = offset; + continue; + }, + else => {}, + }; + offset_next = offset; + line_next = null; + } else { + const line = try std.fmt.parseInt(usize, arg[1..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.line = line; + continue; + }, + else => {}, + }; + line_next = line; + offset_next = null; + } continue; } @@ -274,6 +289,15 @@ pub fn main() anyerror!void { else => {}, } } + if (offset_next) |offset| { + switch (curr.*) { + .file => |*file| { + file.offset = offset; + offset_next = null; + }, + else => {}, + } + } } var have_project = false; From 2461717f114e09bbf7bc7909e28cd6b9c0ff581e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 22:47:50 +0200 Subject: [PATCH 8/8] feat: add support for byte offsets in file links to navigate command --- src/tui/mainview.zig | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 6166b29..1243632 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -150,10 +150,10 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { }); return true; } else if (try m.match(.{ "navigate_complete", tp.extract(&same_file), tp.extract(&path), tp.extract(&goto_args), tp.extract(&line), tp.extract(&column) })) { - cmds.navigate_complete(self, same_file, path, goto_args, line, column) catch |e| return tp.exit_error(e, @errorReturnTrace()); + cmds.navigate_complete(self, same_file, path, goto_args, line, column, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); return true; } else if (try m.match(.{ "navigate_complete", tp.extract(&same_file), tp.extract(&path), tp.extract(&goto_args), tp.null_, tp.null_ })) { - cmds.navigate_complete(self, same_file, path, goto_args, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); + cmds.navigate_complete(self, same_file, path, goto_args, null, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); return true; } return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m); @@ -349,6 +349,7 @@ const cmds = struct { var file_name: []const u8 = undefined; var line: ?i64 = null; var column: ?i64 = null; + var offset: ?i64 = null; var goto_args: []const u8 = &.{}; var iter = ctx.args.buf; @@ -370,6 +371,9 @@ const cmds = struct { } else if (std.mem.eql(u8, field_name, "goto")) { if (!try cbor.matchValue(&iter, cbor.extract_cbor(&goto_args))) return error.InvalidNavigateGotoArgument; + } else if (std.mem.eql(u8, field_name, "offset")) { + if (!try cbor.matchValue(&iter, cbor.extract(&offset))) + return error.InvalidNavigateOffsetArgument; } else { try cbor.skipValue(&iter); } @@ -392,7 +396,8 @@ const cmds = struct { if (tui.config().restore_last_cursor_position and !same_file and !have_editor_metadata and - line == null) + line == null and + offset == null) { const ctx_: struct { allocator: std.mem.Allocator, @@ -424,11 +429,11 @@ const cmds = struct { return; } - return cmds.navigate_complete(self, same_file, f, goto_args, line, column); + return cmds.navigate_complete(self, same_file, f, goto_args, line, column, offset); } pub const navigate_meta: Meta = .{ .arguments = &.{.object} }; - fn navigate_complete(self: *Self, same_file: bool, f: []const u8, goto_args: []const u8, line: ?i64, column: ?i64) Result { + fn navigate_complete(self: *Self, same_file: bool, f: []const u8, goto_args: []const u8, line: ?i64, column: ?i64, offset: ?i64) Result { if (!same_file) { if (self.get_active_editor()) |editor| { editor.send_editor_jump_source() catch {}; @@ -444,6 +449,10 @@ const cmds = struct { try command.executeName("scroll_view_center", .{}); if (column) |col| try command.executeName("goto_column", command.fmt(.{col})); + } else if (offset) |o| { + try command.executeName("goto_byte_offset", command.fmt(.{o})); + if (!same_file) + try command.executeName("scroll_view_center", .{}); } tui.need_render(); }