From 67b214675f76511a042a1912f3ff5f866af89321 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 13 Sep 2025 20:05:28 +0200 Subject: [PATCH 01/50] refactor: log issues in LSP completion item messages --- src/Project.zig | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index f78db3a..69dec67 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1005,10 +1005,17 @@ fn send_completion_items(to: tp.pid_ref, file_path: []const u8, row: usize, col: var item: []const u8 = ""; while (len > 0) : (len -= 1) { if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&item)))) return error.InvalidMessageField; - send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete) catch return error.ClientFailed; + try send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete); } } +fn invalid_field(field: []const u8) error{InvalidMessage} { + const logger = log.logger("lsp"); + defer logger.deinit(); + logger.print("invalid completion field '{s}'", .{field}); + return error.InvalidMessage; +} + fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, item: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void { var label: []const u8 = ""; var label_detail: []const u8 = ""; @@ -1029,53 +1036,53 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: var field_name: []const u8 = undefined; if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; if (std.mem.eql(u8, field_name, "label")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label)))) return invalid_field("label"); } else if (std.mem.eql(u8, field_name, "labelDetails")) { var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("labelDetails"); if (std.mem.eql(u8, field_name, "detail")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label_detail)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label_detail)))) return invalid_field("labelDetails.detail"); } else if (std.mem.eql(u8, field_name, "description")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label_description)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label_description)))) return invalid_field("labelDetails.description"); } else { try cbor.skipValue(&iter); } } } else if (std.mem.eql(u8, field_name, "kind")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&kind)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&kind)))) return invalid_field("kind"); } else if (std.mem.eql(u8, field_name, "detail")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&detail)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&detail)))) return invalid_field("detail"); } else if (std.mem.eql(u8, field_name, "documentation")) { var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("documentation"); if (std.mem.eql(u8, field_name, "kind")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&documentation_kind)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&documentation_kind)))) return invalid_field("documentation.kind"); } else if (std.mem.eql(u8, field_name, "value")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&documentation)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&documentation)))) return invalid_field("documentation.value"); } else { try cbor.skipValue(&iter); } } } else if (std.mem.eql(u8, field_name, "sortText")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&sortText)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&sortText)))) return invalid_field("sortText"); } else if (std.mem.eql(u8, field_name, "insertTextFormat")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&insertTextFormat)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&insertTextFormat)))) return invalid_field("insertTextFormat"); } else if (std.mem.eql(u8, field_name, "textEdit")) { // var textEdit: []const u8 = ""; // { "newText": "wait_expired(${1:timeout_ns: isize})", "insert": Range, "replace": Range }, var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("textEdit"); if (std.mem.eql(u8, field_name, "newText")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&textEdit_newText)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&textEdit_newText)))) return invalid_field("textEdit.newText"); } else if (std.mem.eql(u8, field_name, "insert")) { var range_: []const u8 = undefined; - if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return invalid_field("textEdit.insert"); textEdit_insert = try read_range(range_); } else if (std.mem.eql(u8, field_name, "replace")) { var range_: []const u8 = undefined; - if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return invalid_field("textEdit.replace"); textEdit_replace = try read_range(range_); } else { try cbor.skipValue(&iter); From 76600bc6bd8c6ed8a3883b1812717bb57dd305a1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 13 Sep 2025 20:06:04 +0200 Subject: [PATCH 02/50] fix: handle completion items with no insert and/or replace coordinates superhtml fix --- src/Project.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 69dec67..92ba4d4 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1092,8 +1092,8 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: try cbor.skipValue(&iter); } } - const insert = textEdit_insert orelse return error.InvalidMessageField; - const replace = textEdit_replace orelse return error.InvalidMessageField; + const insert = textEdit_insert orelse Range{ .start = .{ .line = 0, .character = 0 }, .end = .{ .line = 0, .character = 0 } }; + const replace = textEdit_replace orelse Range{ .start = .{ .line = 0, .character = 0 }, .end = .{ .line = 0, .character = 0 } }; return to.send(.{ "cmd", "add_completion", .{ file_path, From 9bdc3e0a0aff07496c37cfabfa62d5d748edc289 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 13 Sep 2025 20:06:44 +0200 Subject: [PATCH 03/50] fix: handle completion items with no type icon superhtml fix --- src/tui/mode/overlay/completion_palette.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index eac3856..defc212 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -117,6 +117,7 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { } const CompletionItemKind = enum(u8) { + None = 0, Text = 1, Method = 2, Function = 3, @@ -146,6 +147,7 @@ const CompletionItemKind = enum(u8) { fn kind_icon(kind: CompletionItemKind) []const u8 { return switch (kind) { + .None => " ", .Text => "󰊄", .Method => "", .Function => "󰊕", From 59921d8e1073aebff7854b014d3bff4f2f4a5598 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 10:04:27 +0200 Subject: [PATCH 04/50] feat: restore cursor column when cancelling goto mini mode This commit refactors the numeric_input mini mode to make the input value type generic. This allows the goto mini mode to store the origin column along with the row. Also, this will allow more complex numeric_input modes, for example a goto mini mode that supports column and row. --- src/tui/mode/mini/goto.zig | 43 +++++++++++++++++++-- src/tui/mode/mini/numeric_input.zig | 59 +++++++++++++++++++---------- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/src/tui/mode/mini/goto.zig b/src/tui/mode/mini/goto.zig index 6607dc6..e8bc12a 100644 --- a/src/tui/mode/mini/goto.zig +++ b/src/tui/mode/mini/goto.zig @@ -1,3 +1,4 @@ +const fmt = @import("std").fmt; const command = @import("command"); const tui = @import("../../tui.zig"); @@ -5,13 +6,43 @@ const tui = @import("../../tui.zig"); pub const Type = @import("numeric_input.zig").Create(@This()); pub const create = Type.create; +pub const ValueType = @import("../../editor.zig").Cursor; + pub fn name(_: *Type) []const u8 { return "#goto"; } -pub fn start(_: *Type) usize { - const editor = tui.get_active_editor() orelse return 1; - return editor.get_primary().cursor.row + 1; +pub fn start(_: *Type) ValueType { + const editor = tui.get_active_editor() orelse return .{}; + return editor.get_primary().cursor; +} + +pub fn process_digit(self: *Type, digit: u8) void { + switch (digit) { + 0 => { + if (self.input) |*x| x.row = x.row * 10; + }, + 1...9 => { + if (self.input) |*x| { + x.row = x.row * 10 + digit; + } else { + self.input = .{ .row = digit }; + } + }, + else => unreachable, + } +} + +pub fn delete(self: *Type, input: *ValueType) void { + const newval = if (input.row < 10) 0 else input.row / 10; + if (newval == 0) self.input = null else input.row = newval; +} + +pub fn format_value(_: *Type, input: ?ValueType, buf: []u8) []const u8 { + return if (input) |value| + (fmt.bufPrint(buf, "{d}", .{value.row}) catch "") + else + ""; } pub const preview = goto; @@ -19,5 +50,9 @@ pub const apply = goto; pub const cancel = goto; fn goto(self: *Type, _: command.Context) void { - command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {}; + if (self.input) |input| { + command.executeName("goto_line", command.fmt(.{input.row})) catch {}; + } else { + command.executeName("goto_line_and_column", command.fmt(.{ self.start.row, self.start.col })) catch {}; + } } diff --git a/src/tui/mode/mini/numeric_input.zig b/src/tui/mode/mini/numeric_input.zig index 5b6be0f..ad32849 100644 --- a/src/tui/mode/mini/numeric_input.zig +++ b/src/tui/mode/mini/numeric_input.zig @@ -18,10 +18,12 @@ pub fn Create(options: type) type { const Commands = command.Collection(cmds); + const ValueType = if (@hasDecl(options, "ValueType")) options.ValueType else usize; + allocator: Allocator, buf: [30]u8 = undefined, - input: ?usize = null, - start: usize, + input: ?ValueType = null, + start: ValueType, ctx: command.Context, commands: Commands = undefined, @@ -31,7 +33,7 @@ pub fn Create(options: type) type { self.* = .{ .allocator = allocator, .ctx = .{ .args = try ctx.args.clone(allocator) }, - .start = 0, + .start = if (@hasDecl(options, "ValueType")) ValueType{} else 0, }; self.start = options.start(self); try self.commands.init(self); @@ -55,27 +57,42 @@ pub fn Create(options: type) type { fn update_mini_mode_text(self: *Self) void { if (tui.mini_mode()) |mini_mode| { - mini_mode.text = if (self.input) |linenum| - (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") - else - ""; + if (@hasDecl(options, "format_value")) { + mini_mode.text = options.format_value(self, self.input, &self.buf); + } else { + mini_mode.text = if (self.input) |linenum| + (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") + else + ""; + } mini_mode.cursor = tui.egc_chunk_width(mini_mode.text, 0, 1); } } fn insert_char(self: *Self, char: u8) void { - switch (char) { - '0' => { - if (self.input) |linenum| self.input = linenum * 10; - }, - '1'...'9' => { - const digit: usize = @intCast(char - '0'); - self.input = if (self.input) |x| x * 10 + digit else digit; - }, - else => {}, + const process_digit_ = if (@hasDecl(options, "process_digit")) options.process_digit else process_digit; + if (@hasDecl(options, "Separator")) { + switch (char) { + '0'...'9' => process_digit_(@intCast(char - '0')), + options.Separator => options.process_separator(self), + else => {}, + } + } else { + switch (char) { + '0'...'9' => process_digit_(self, @intCast(char - '0')), + else => {}, + } } } + fn process_digit(self: *Self, digit: u8) void { + self.input = switch (digit) { + 0 => if (self.input) |value| value * 10 else 0, + 1...9 => if (self.input) |x| x * 10 + digit else digit, + else => unreachable, + }; + } + fn insert_bytes(self: *Self, bytes: []const u8) void { for (bytes) |c| self.insert_char(c); } @@ -101,9 +118,13 @@ pub fn Create(options: type) type { pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { - if (self.input) |linenum| { - const newval = if (linenum < 10) 0 else linenum / 10; - self.input = if (newval == 0) null else newval; + if (self.input) |*input| { + if (@hasDecl(options, "delete")) { + options.delete(self, input); + } else { + const newval = if (input.* < 10) 0 else input.* / 10; + self.input = if (newval == 0) null else newval; + } self.update_mini_mode_text(); options.preview(self, self.ctx); } From 933126e2a002b64cc15043b09ecdc6732f139678 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 20:39:45 +0200 Subject: [PATCH 05/50] feat: add support for {row}:{column} syntax in goto mini mode --- src/tui/mode/mini/goto.zig | 80 ++++++++++++++++++++--------- src/tui/mode/mini/numeric_input.zig | 2 +- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/tui/mode/mini/goto.zig b/src/tui/mode/mini/goto.zig index e8bc12a..963ec7f 100644 --- a/src/tui/mode/mini/goto.zig +++ b/src/tui/mode/mini/goto.zig @@ -2,11 +2,16 @@ 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 = @import("../../editor.zig").Cursor; +pub const ValueType = struct { + cursor: Cursor = .{}, + part: enum { row, col } = .row, +}; +pub const Separator = ':'; pub fn name(_: *Type) []const u8 { return "#goto"; @@ -14,35 +19,64 @@ pub fn name(_: *Type) []const u8 { pub fn start(_: *Type) ValueType { const editor = tui.get_active_editor() orelse return .{}; - return editor.get_primary().cursor; + return .{ .cursor = editor.get_primary().cursor }; } pub fn process_digit(self: *Type, digit: u8) void { - switch (digit) { - 0 => { - if (self.input) |*x| x.row = x.row * 10; + const part = if (self.input) |input| input.part else .row; + switch (part) { + .row => switch (digit) { + 0 => { + if (self.input) |*input| input.cursor.row = input.cursor.row * 10; + }, + 1...9 => { + if (self.input) |*input| { + input.cursor.row = input.cursor.row * 10 + digit; + } else { + self.input = .{ .cursor = .{ .row = digit } }; + } + }, + else => unreachable, }, - 1...9 => { - if (self.input) |*x| { - x.row = x.row * 10 + digit; - } else { - self.input = .{ .row = digit }; - } + .col => if (self.input) |*input| { + input.cursor.col = input.cursor.col * 10 + digit; }, - else => unreachable, } } +pub fn process_separator(self: *Type) void { + if (self.input) |*input| switch (input.part) { + .row => input.part = .col, + else => {}, + }; +} + pub fn delete(self: *Type, input: *ValueType) void { - const newval = if (input.row < 10) 0 else input.row / 10; - if (newval == 0) self.input = null else input.row = newval; + switch (input.part) { + .row => { + const newval = if (input.cursor.row < 10) 0 else input.cursor.row / 10; + if (newval == 0) self.input = null else input.cursor.row = newval; + }, + .col => { + const newval = if (input.cursor.col < 10) 0 else input.cursor.col / 10; + if (newval == 0) { + input.part = .row; + input.cursor.col = 0; + } else input.cursor.col = newval; + }, + } } pub fn format_value(_: *Type, input: ?ValueType, buf: []u8) []const u8 { - return if (input) |value| - (fmt.bufPrint(buf, "{d}", .{value.row}) catch "") - else - ""; + return if (input) |value| blk: { + switch (value.part) { + .row => break :blk fmt.bufPrint(buf, "{d}", .{value.cursor.row}) catch "", + .col => if (value.cursor.col == 0) + break :blk fmt.bufPrint(buf, "{d}:", .{value.cursor.row}) catch "" + else + break :blk fmt.bufPrint(buf, "{d}:{d}", .{ value.cursor.row, value.cursor.col }) catch "", + } + } else ""; } pub const preview = goto; @@ -50,9 +84,9 @@ pub const apply = goto; pub const cancel = goto; fn goto(self: *Type, _: command.Context) void { - if (self.input) |input| { - command.executeName("goto_line", command.fmt(.{input.row})) catch {}; - } else { - command.executeName("goto_line_and_column", command.fmt(.{ self.start.row, self.start.col })) catch {}; - } + send_goto(if (self.input) |input| input.cursor else self.start.cursor); +} + +fn send_goto(cursor: Cursor) void { + command.executeName("goto_line_and_column", command.fmt(.{ cursor.row, cursor.col })) catch {}; } diff --git a/src/tui/mode/mini/numeric_input.zig b/src/tui/mode/mini/numeric_input.zig index ad32849..7b6c30f 100644 --- a/src/tui/mode/mini/numeric_input.zig +++ b/src/tui/mode/mini/numeric_input.zig @@ -73,7 +73,7 @@ pub fn Create(options: type) type { const process_digit_ = if (@hasDecl(options, "process_digit")) options.process_digit else process_digit; if (@hasDecl(options, "Separator")) { switch (char) { - '0'...'9' => process_digit_(@intCast(char - '0')), + '0'...'9' => process_digit_(self, @intCast(char - '0')), options.Separator => options.process_separator(self), else => {}, } 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 06/50] 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 07/50] 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 08/50] 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 09/50] 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 10/50] 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 11/50] 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 12/50] 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 13/50] 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(); } From 4035cefcaf83a18076dd23c3b556c2cdb37f186a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 17 Sep 2025 23:05:21 +0200 Subject: [PATCH 14/50] feat: add optional integer arguments to goto and goto_offset commands --- src/tui/tui.zig | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index f2ecb77..9955208 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1034,14 +1034,31 @@ const cmds = struct { pub const find_in_files_meta: Meta = .{ .description = "Find in files" }; pub fn goto(self: *Self, ctx: Ctx) Result { - return enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx); + var line: usize = undefined; + var column: usize = undefined; + return if (try ctx.args.match(.{tp.extract(&line)})) + command.executeName("goto_line", command.fmt(.{line})) + else if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column) })) + command.executeName("goto_line_and_column", command.fmt(.{ line, column })) + else + enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx); } - pub const goto_meta: Meta = .{ .description = "Goto line" }; + pub const goto_meta: Meta = .{ + .description = "Goto line", + .arguments = &.{ .integer, .integer }, + }; pub fn goto_offset(self: *Self, ctx: Ctx) Result { - return enter_mini_mode(self, @import("mode/mini/goto_offset.zig"), ctx); + var offset: usize = undefined; + return if (try ctx.args.match(.{tp.extract(&offset)})) + command.executeName("goto_byte_offset", command.fmt(.{offset})) + else + enter_mini_mode(self, @import("mode/mini/goto_offset.zig"), ctx); } - pub const goto_offset_meta: Meta = .{ .description = "Goto byte offset" }; + pub const goto_offset_meta: Meta = .{ + .description = "Goto byte offset", + .arguments = &.{.integer}, + }; pub fn move_to_char(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx); From 60016a3d039f9cd17604683503edcb1e36cb0eee Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 11:52:42 +0200 Subject: [PATCH 15/50] feat: improve expand_selection by selecting top selection matching node --- src/tui/editor.zig | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 6932495..6e43585 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -170,9 +170,8 @@ pub const CurSel = struct { return sel; } - fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { - const range = node.getRange(); - self.selection = .{ + fn selection_from_range(range: syntax.Range, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return .{ .begin = .{ .row = range.start_point.row, .col = try root.pos_to_width(range.start_point.row, range.start_point.column, metrics), @@ -182,7 +181,23 @@ pub const CurSel = struct { .col = try root.pos_to_width(range.end_point.row, range.end_point.column, metrics), }, }; - self.cursor = self.selection.?.end; + } + + fn selection_from_node(node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return selection_from_range(node.getRange(), root, metrics); + } + + fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { + const sel = try selection_from_node(node, root, metrics); + self.selection = sel; + self.cursor = sel.end; + } + + fn select_parent_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!syntax.Node { + const parent = node.getParent(); + if (parent.isNull()) return error.NotFound; + try self.select_node(parent, root, metrics); + return parent; } fn write(self: *const Self, writer: Buffer.MetaWriter) !void { @@ -4297,19 +4312,36 @@ pub const Editor = struct { return node; } - fn select_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + fn top_node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + var node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return node; + var parent = node.getParent(); + if (parent.isNull()) return node; + const node_sel = CurSel.selection_from_node(node, root, metrics) catch return node; + var parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + while (parent_sel.eql(node_sel)) { + node = parent; + parent = parent.getParent(); + parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + } + return node; + } + + fn select_top_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { cursel.disable_selection(root, self.metrics); const sel = (try cursel.enable_selection(root, self.metrics)).*; - return cursel.select_node(try self.node_at_selection(sel, root, metrics), root, metrics); + return cursel.select_node(try self.top_node_at_selection(sel, root, metrics), root, metrics); } fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + var node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; - const parent = node.getParent(); - if (parent.isNull()) return error.Stop; - return cursel.select_node(parent, root, metrics); + var node_sel = try CurSel.selection_from_node(node, root, metrics); + if (!node_sel.eql(sel)) return cursel.select_node(node, root, metrics); + node = try cursel.select_parent_node(node, root, metrics); + while (cursel.selection.?.eql(sel)) + node = try cursel.select_parent_node(node, root, metrics); } pub fn expand_selection(self: *Self, _: Context) Result { @@ -4320,7 +4352,7 @@ pub const Editor = struct { try if (cursel.selection) |_| self.expand_selection_to_parent_node(root, cursel, self.metrics) else - self.select_node_at_cursor(root, cursel, self.metrics); + self.select_top_node_at_cursor(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } @@ -4363,7 +4395,7 @@ pub const Editor = struct { fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4372,7 +4404,7 @@ pub const Editor = struct { fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_named_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4398,7 +4430,7 @@ pub const Editor = struct { fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4407,7 +4439,7 @@ pub const Editor = struct { fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_named_sibling(node); if (sibling.isNull()) return error.Stop; From 99dc805817033d5eddd1830697335491f5ef6114 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:25:13 +0200 Subject: [PATCH 16/50] feat: add flow mode keybinds for unnamed AST sibling movement --- src/keybind/builtin/flow.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 8f0a85a..e975af7 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -132,6 +132,10 @@ ["alt+shift+right", "expand_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], + ["alt+]", "shrink_selection", true], + ["alt+[", "expand_selection"], + ["alt+{", "select_prev_sibling", true], + ["alt+}", "select_next_sibling", true], ["alt+shift+e", "move_parent_node_end"], ["alt+shift+b", "move_parent_node_start"], ["alt+a", "select_all_siblings"], From 30af629a1ab770b36340e585eea8ddd59b973401 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:50:14 +0200 Subject: [PATCH 17/50] refactor: expose CurSel.to_selection method --- src/tui/editor.zig | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 6e43585..92ba6ba 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -102,37 +102,41 @@ pub const CurSel = struct { } pub fn enable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return switch (tui.get_selection_style()) { - .normal => self.enable_selection_normal(), - .inclusive => try self.enable_selection_inclusive(root, metrics), - }; + self.selection = try self.to_selection(root, metrics); + return if (self.selection) |*sel| sel else unreachable; } pub fn enable_selection_normal(self: *Self) *Selection { - return if (self.selection) |*sel| - sel - else cod: { - self.selection = Selection.from_cursor(&self.cursor); - break :cod &self.selection.?; + self.selection = self.to_selection_normal(); + return if (self.selection) |*sel| sel else unreachable; + } + + pub fn to_selection(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return switch (tui.get_selection_style()) { + .normal => self.to_selection_normal(), + .inclusive => try self.to_selection_inclusive(root, metrics), }; } - fn enable_selection_inclusive(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return if (self.selection) |*sel| + fn to_selection_normal(self: *const Self) Selection { + return if (self.selection) |sel| sel else Selection.from_cursor(&self.cursor); + } + + fn to_selection_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return if (self.selection) |sel| sel else cod: { - self.selection = Selection.from_cursor(&self.cursor); - try self.selection.?.end.move_right(root, metrics); - try self.cursor.move_right(root, metrics); - break :cod &self.selection.?; + var sel = Selection.from_cursor(&self.cursor); + try sel.end.move_right(root, metrics); + break :cod sel; }; } - fn to_inclusive_cursor(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { - var res = self.cursor; + fn to_cursor_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { + var cursor = self.cursor; if (self.selection) |sel| if (!sel.is_reversed()) - try res.move_left(root, metrics); - return res; + try cursor.move_left(root, metrics); + return cursor; } pub fn disable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) void { @@ -1207,7 +1211,7 @@ pub const Editor = struct { fn get_rendered_cursor(self: *Self, style: anytype, cursel: anytype) !Cursor { return switch (style) { .normal => cursel.cursor, - .inclusive => try cursel.to_inclusive_cursor(try self.buf_root(), self.metrics), + .inclusive => try cursel.to_cursor_inclusive(try self.buf_root(), self.metrics), }; } From 8100e7d52b55fd670c39c4ce469900df057b4f01 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:58:10 +0200 Subject: [PATCH 18/50] refactor: improve const correctness in AST navigation functions --- src/tui/editor.zig | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 92ba6ba..088cdf8 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4298,7 +4298,7 @@ pub const Editor = struct { } pub const selections_reverse_meta: Meta = .{ .description = "Reverse selection" }; - fn node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + fn node_at_selection(self: *const Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { const syn = self.syntax orelse return error.Stop; const node = try syn.node_at_point_range(.{ .start_point = .{ @@ -4316,7 +4316,7 @@ pub const Editor = struct { return node; } - fn top_node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + fn top_node_at_selection(self: *const Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { var node = try self.node_at_selection(sel, root, metrics); if (node.isNull()) return node; var parent = node.getParent(); @@ -4331,10 +4331,9 @@ pub const Editor = struct { return node; } - fn select_top_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - cursel.disable_selection(root, self.metrics); - const sel = (try cursel.enable_selection(root, self.metrics)).*; - return cursel.select_node(try self.top_node_at_selection(sel, root, metrics), root, metrics); + fn top_node_at_cursel(self: *const Self, cursel: *const CurSel, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + const sel = try cursel.to_selection(root, metrics); + return try self.top_node_at_selection(sel, root, metrics); } fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { @@ -4356,7 +4355,7 @@ pub const Editor = struct { try if (cursel.selection) |_| self.expand_selection_to_parent_node(root, cursel, self.metrics) else - self.select_top_node_at_cursor(root, cursel, self.metrics); + cursel.select_node(try self.top_node_at_cursel(cursel, root, self.metrics), root, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } From 1ef77601e36e0521b7b4aba4a405646fccd51f4d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:58:42 +0200 Subject: [PATCH 19/50] feat: allow next/previous sibling functions to work with no selection --- src/tui/editor.zig | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 088cdf8..8252578 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4397,8 +4397,7 @@ pub const Editor = struct { pub const shrink_selection_meta: Meta = .{ .description = "Shrink selection to first AST child node" }; fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4406,8 +4405,7 @@ pub const Editor = struct { } fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_named_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4421,19 +4419,17 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - if (cursel.selection) |_| - try if (unnamed) - self.select_next_sibling_node(root, cursel, self.metrics) - else - self.select_next_named_sibling_node(root, cursel, self.metrics); + try if (unnamed) + self.select_next_sibling_node(root, cursel, self.metrics) + else + self.select_next_named_sibling_node(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } pub const select_next_sibling_meta: Meta = .{ .description = "Move selection to next AST sibling node" }; fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4441,8 +4437,7 @@ pub const Editor = struct { } fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_named_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4456,11 +4451,10 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - if (cursel.selection) |_| - try if (unnamed) - self.select_prev_sibling_node(root, cursel, self.metrics) - else - self.select_prev_named_sibling_node(root, cursel, self.metrics); + try if (unnamed) + self.select_prev_sibling_node(root, cursel, self.metrics) + else + self.select_prev_named_sibling_node(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } From 52996ed57dd1f9d0a763f2019f4b120951022f86 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 13:07:03 +0200 Subject: [PATCH 20/50] feat: make AST keybindings more intuitive --- src/keybind/builtin/flow.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index e975af7..c139a3a 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -128,14 +128,14 @@ ["alt+shift+s", "filter", "sort", "-u"], ["alt+shift+v", "paste"], ["alt+shift+i", "add_cursors_to_line_ends"], - ["alt+shift+left", "shrink_selection"], - ["alt+shift+right", "expand_selection"], + ["alt+shift+left", "expand_selection"], + ["alt+shift+right", "shrink_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], - ["alt+]", "shrink_selection", true], - ["alt+[", "expand_selection"], - ["alt+{", "select_prev_sibling", true], - ["alt+}", "select_next_sibling", true], + ["alt+{", "expand_selection"], + ["alt+}", "shrink_selection", true], + ["alt+[", "select_prev_sibling", true], + ["alt+]", "select_next_sibling", true], ["alt+shift+e", "move_parent_node_end"], ["alt+shift+b", "move_parent_node_start"], ["alt+a", "select_all_siblings"], From e92f4fe9b1d3af69d695e4d7d9845524a28fe052 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:18:10 +0200 Subject: [PATCH 21/50] build: add nightly build helper script --- contrib/make_nightly_build | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100755 contrib/make_nightly_build diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build new file mode 100755 index 0000000..7f910f7 --- /dev/null +++ b/contrib/make_nightly_build @@ -0,0 +1,102 @@ +#!/bin/bash +set -e + +builddir="nightly-build" + +DESTDIR="$(pwd)/$builddir" +BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" +APPNAME="$(basename "$BASEDIR")" +title="$APPNAME nightly build" +repo="neurocyte/$APPNAME-nightly" + +release_notes="$BASEDIR/$builddir-release-notes" + +cd "$BASEDIR" + +if [ -e "$DESTDIR" ]; then + echo directory \"$builddir\" already exists + exit 1 +fi + +if [ -e "$release_notes" ]; then + echo file \"$release_notes\" already exists + exit 1 +fi + +DIFF="$(git diff --stat --patch HEAD)" + +if [ -n "$DIFF" ] ; then + echo there are outstanding changes: + echo "$DIFF" + exit 1 +fi + +UNPUSHED="$(git log --pretty=oneline @{u}...)" + +if [ -n "$UNPUSHED" ] ; then + echo there are unpushed commits: + echo "$UNPUSHED" + exit 1 +fi + +# get latest version tag from github releases api +last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) +[ -z "last_nightly_version" ] && { + echo "failed to fetch $title latest version" + exit 1 +} + +echo running tests... + +./zig build test + +echo building... + +./zig build -Dpackage_release --prefix "$DESTDIR/build" + +cd "$DESTDIR/build" + +VERSION=$(/bin/cat version) +TARGETS=$(/bin/ls) + +for target in $TARGETS; do + if [ -d "$target" ]; then + cd "$target" + if [ "${target:0:8}" == "windows-" ]; then + echo packing zip "$target"... + zip -r "../../${APPNAME}-${VERSION}-${target}.zip" ./* + cd .. + else + echo packing tar "$target"... + tar -czf "../../${APPNAME}-${VERSION}-${target}.tar.gz" -- * + cd .. + fi + fi +done + +cd .. +rm -r build + +TARFILES=$(/bin/ls) + +for tarfile in $TARFILES; do + echo signing "$tarfile"... + gpg --local-user 4E6CF7234FFC4E14531074F98EB1E1BB660E3FB9 --detach-sig "$tarfile" + sha256sum -b "$tarfile" >"${tarfile}.sha256" +done + +echo "done making release $VERSION @ $DESTDIR" +echo + +/bin/ls -lah + +cd .. + +echo $title > "$release_notes" +echo > "$release_notes" +echo "changes:" > "$release_notes" +echo > "$release_notes" +git log (git describe --tags --abbrev=0)..HEAD --pretty="format:%al %s" >> "$release_notes" +cat "$release_notes" + +gh release create (git --git-dir $BASEDIR/.git describe) --notes-file "$release_notes" $DESTDIR/* From d611f74cfbc61cc74bde9d606b84865cfb990526 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:31:06 +0200 Subject: [PATCH 22/50] build: fix git log call in make_nightly_build --- contrib/make_nightly_build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 7f910f7..b24b241 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -96,7 +96,7 @@ echo $title > "$release_notes" echo > "$release_notes" echo "changes:" > "$release_notes" echo > "$release_notes" -git log (git describe --tags --abbrev=0)..HEAD --pretty="format:%al %s" >> "$release_notes" +git log "$(git describe --tags --abbrev=0)..HEAD" --pretty="format:%al %s" >> "$release_notes" cat "$release_notes" gh release create (git --git-dir $BASEDIR/.git describe) --notes-file "$release_notes" $DESTDIR/* From 0006a056dbd6394adb93e9497f92f4d1ca720a56 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:37:29 +0200 Subject: [PATCH 23/50] build: add version check to make_nightly_build --- contrib/make_nightly_build | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index b24b241..4552c2b 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -46,6 +46,15 @@ last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/late exit 1 } +local_version="$(git --git-dir $BASEDIR/.git describe)" +if [ "$local_version" == "$last_nightly_version" ] ; then + echo "$title is already at version $last_nightly_version" + exit 1 +fi + +echo +echo "building $title version $local_version..." +echo echo running tests... ./zig build test From 7c6712d7a405ee45a5116907e0da5ab6e6e42232 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:37:47 +0200 Subject: [PATCH 24/50] build: add explicit repo parameter to gh release create in make_nightly_build --- contrib/make_nightly_build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 4552c2b..b6aaf05 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -108,4 +108,4 @@ echo > "$release_notes" git log "$(git describe --tags --abbrev=0)..HEAD" --pretty="format:%al %s" >> "$release_notes" cat "$release_notes" -gh release create (git --git-dir $BASEDIR/.git describe) --notes-file "$release_notes" $DESTDIR/* +gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* From 7555331c1fbc2478059c8a283bce199348b670d3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:41:44 +0200 Subject: [PATCH 25/50] build: fix make_nightly_build release notes query --- contrib/make_nightly_build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index b6aaf05..896b6dc 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -105,7 +105,7 @@ echo $title > "$release_notes" echo > "$release_notes" echo "changes:" > "$release_notes" echo > "$release_notes" -git log "$(git describe --tags --abbrev=0)..HEAD" --pretty="format:%al %s" >> "$release_notes" +git log "${last_nightly_version}..HEAD" --pretty="format:%al %s" >> "$release_notes" cat "$release_notes" gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* From 87a72195d7dec999bca39142e2ae2afdd9935ca4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:48:55 +0200 Subject: [PATCH 26/50] build: misc clean-ups in make_nightly_build --- contrib/make_nightly_build | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 896b6dc..0ce52d9 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -19,21 +19,21 @@ if [ -e "$DESTDIR" ]; then fi if [ -e "$release_notes" ]; then - echo file \"$release_notes\" already exists + echo file \""$release_notes"\" already exists exit 1 fi DIFF="$(git diff --stat --patch HEAD)" -if [ -n "$DIFF" ] ; then +if [ -n "$DIFF" ]; then echo there are outstanding changes: echo "$DIFF" exit 1 fi -UNPUSHED="$(git log --pretty=oneline @{u}...)" +UNPUSHED="$(git log --pretty=oneline '@{u}...')" -if [ -n "$UNPUSHED" ] ; then +if [ -n "$UNPUSHED" ]; then echo there are unpushed commits: echo "$UNPUSHED" exit 1 @@ -41,13 +41,13 @@ fi # get latest version tag from github releases api last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) -[ -z "last_nightly_version" ] && { +[ -z "$last_nightly_version" ] && { echo "failed to fetch $title latest version" exit 1 } -local_version="$(git --git-dir $BASEDIR/.git describe)" -if [ "$local_version" == "$last_nightly_version" ] ; then +local_version="$(git --git-dir "$BASEDIR/.git" describe)" +if [ "$local_version" == "$last_nightly_version" ]; then echo "$title is already at version $last_nightly_version" exit 1 fi @@ -101,11 +101,13 @@ echo cd .. -echo $title > "$release_notes" -echo > "$release_notes" -echo "changes:" > "$release_notes" -echo > "$release_notes" -git log "${last_nightly_version}..HEAD" --pretty="format:%al %s" >> "$release_notes" +{ + echo "$title" + echo "$release_notes" + echo "changes:" + echo +} >"$release_notes" +git log "${last_nightly_version}..HEAD" --pretty="format:%al %s" >>"$release_notes" cat "$release_notes" gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* From 733c24ca16c561972eac0c0719869b6b7cc64e50 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:50:50 +0200 Subject: [PATCH 27/50] build: add version to nightly build release notes --- contrib/make_nightly_build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 0ce52d9..a8eb482 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -94,7 +94,7 @@ for tarfile in $TARFILES; do sha256sum -b "$tarfile" >"${tarfile}.sha256" done -echo "done making release $VERSION @ $DESTDIR" +echo "done making $title $VERSION @ $DESTDIR" echo /bin/ls -lah @@ -102,7 +102,7 @@ echo cd .. { - echo "$title" + echo "$title" "$VERSION" echo "$release_notes" echo "changes:" echo From 099444f84d4c88c1ef7e11cd9ea7a3ef73c33c8e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 21:57:24 +0200 Subject: [PATCH 28/50] build: use commit hash in nightly release notes --- contrib/make_nightly_build | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index a8eb482..95e2540 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -103,11 +103,11 @@ cd .. { echo "$title" "$VERSION" - echo "$release_notes" - echo "changes:" + echo + echo "commits:" echo } >"$release_notes" -git log "${last_nightly_version}..HEAD" --pretty="format:%al %s" >>"$release_notes" +git log "${last_nightly_version}..HEAD" --pretty="format:%h %s" >>"$release_notes" cat "$release_notes" gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* From a870254166a74ca6c284e5df87652d0b864c0234 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 22:02:15 +0200 Subject: [PATCH 29/50] build: improve nightly release notes commit references --- contrib/make_nightly_build | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 95e2540..780ac5a 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -53,7 +53,9 @@ if [ "$local_version" == "$last_nightly_version" ]; then fi echo -echo "building $title version $local_version..." +echo "building $title version $local_version... (previous $last_nightly_version)" +echo +git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" echo echo running tests... @@ -107,7 +109,7 @@ cd .. echo "commits:" echo } >"$release_notes" -git log "${last_nightly_version}..HEAD" --pretty="format:%h %s" >>"$release_notes" +git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" >>"$release_notes" cat "$release_notes" gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* From 366dde01441716e5eff008ec2565fd101dd9da27 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:04:40 +0200 Subject: [PATCH 30/50] build: read github tag name with jq in make_nightly_build --- contrib/make_nightly_build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 780ac5a..3223438 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -39,8 +39,8 @@ if [ -n "$UNPUSHED" ]; then exit 1 fi -# get latest version tag from github releases api -last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name":' | cut -d'"' -f4) +# get latest version tag +last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name) [ -z "$last_nightly_version" ] && { echo "failed to fetch $title latest version" exit 1 From db16c26f0c047f3a97805c8036764222378998c8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:07:24 +0200 Subject: [PATCH 31/50] build: add nightly build uploads to codeberg.org and git.flow-control.dev --- contrib/make_nightly_build | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 3223438..e8932e4 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -1,6 +1,14 @@ #!/bin/bash set -e +for arg in "$@"; do + case "$arg" in + --no-github) NO_GITHUB=1 ;; + --no-codeberg) NO_CODEBERG=1 ;; + --no-flowcontrol) NO_FLOWCONTROL=1 ;; + esac +done + builddir="nightly-build" DESTDIR="$(pwd)/$builddir" @@ -47,9 +55,11 @@ last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/late } local_version="$(git --git-dir "$BASEDIR/.git" describe)" -if [ "$local_version" == "$last_nightly_version" ]; then - echo "$title is already at version $last_nightly_version" - exit 1 +if [ "$1" != "--no-github" ]; then + if [ "$local_version" == "$last_nightly_version" ]; then + echo "$title is already at version $last_nightly_version" + exit 1 + fi fi echo @@ -106,10 +116,39 @@ cd .. { echo "$title" "$VERSION" echo + echo "commits:" echo + git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" + echo + + echo "contributors to this release:" + git shortlog -s -n "${last_nightly_version}..HEAD" + echo + + echo "also available at:" + echo "[git.flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION)" + echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION)" + echo "[flow-control.dev](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION)" } >"$release_notes" -git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" >>"$release_notes" + cat "$release_notes" -gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* +if [ -z "$NO_GITHUB" ]; then + echo uploading to github.com + gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* +fi + +if [ -z "$NO_CODEBERG" ]; then + ASSETS="" + for a in $DESTDIR/*; do + ASSETS="$ASSETS --asset $a" + done + echo uploading to codeberg.org + tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" --prerelease $ASSETS +fi + +if [ -z "$NO_FLOWCONTROL" ]; then + echo uploading to git.flow-control.dev + tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" --prerelease +fi From 54456517769a7639c9b6a8aba6094138a5bb14a9 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:21:31 +0200 Subject: [PATCH 32/50] build: fix typo in nightly build release notes --- contrib/make_nightly_build | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index e8932e4..d96063c 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -121,15 +121,17 @@ cd .. echo git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" echo + echo echo "contributors to this release:" - git shortlog -s -n "${last_nightly_version}..HEAD" + git shortlog -s -n "${last_nightly_version}..HEAD" | cut -b 8- + echo echo echo "also available at:" - echo "[git.flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION)" + echo "[flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION)" echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION)" - echo "[flow-control.dev](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION)" + echo "[codeberg.org](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION)" } >"$release_notes" cat "$release_notes" From f2b1451b3e6e6423e0213ca9d22da6566e904c41 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:22:13 +0200 Subject: [PATCH 33/50] build: do not mark nightly builds as pre-release on codeberg --- contrib/make_nightly_build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index d96063c..bde8414 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -147,10 +147,10 @@ if [ -z "$NO_CODEBERG" ]; then ASSETS="$ASSETS --asset $a" done echo uploading to codeberg.org - tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" --prerelease $ASSETS + tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" $ASSETS fi if [ -z "$NO_FLOWCONTROL" ]; then echo uploading to git.flow-control.dev - tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" --prerelease + tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" fi From 6a84c222d018e394387ce6fbe38994ed80f45b32 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:23:56 +0200 Subject: [PATCH 34/50] build: reverse upload order of nightly builds --- contrib/make_nightly_build | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index bde8414..95be193 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -136,9 +136,9 @@ cd .. cat "$release_notes" -if [ -z "$NO_GITHUB" ]; then - echo uploading to github.com - gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* +if [ -z "$NO_FLOWCONTROL" ]; then + echo uploading to git.flow-control.dev + tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" fi if [ -z "$NO_CODEBERG" ]; then @@ -150,7 +150,7 @@ if [ -z "$NO_CODEBERG" ]; then tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" $ASSETS fi -if [ -z "$NO_FLOWCONTROL" ]; then - echo uploading to git.flow-control.dev - tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" +if [ -z "$NO_GITHUB" ]; then + echo uploading to github.com + gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* fi From bfba9ab81033f154171824934ef52cc9a84123e9 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:33:50 +0200 Subject: [PATCH 35/50] build: get latest nightly build version from git.flow-control.dev --- contrib/make_nightly_build | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 95be193..eda0a1e 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -48,7 +48,14 @@ if [ -n "$UNPUSHED" ]; then fi # get latest version tag -last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name) + +if [ -z "$NO_FLOWCONTROL" ]; then + last_nightly_version=$(curl -s https://git.flow-control.dev/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) +elif [ -z "$NO_GITHUB" ]; then + last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name) +elif [ -z "$NO_CODEBERG" ]; then + last_nightly_version=$(curl -s https://codeberg.org/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) +fi [ -z "$last_nightly_version" ] && { echo "failed to fetch $title latest version" exit 1 From 15e27a6104b6a54e2da8577cb50dfb458999c290 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 13:36:04 +0200 Subject: [PATCH 36/50] build: add option to allow uploading dirty nightly builds --- contrib/make_nightly_build | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index eda0a1e..05a4850 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -6,6 +6,7 @@ for arg in "$@"; do --no-github) NO_GITHUB=1 ;; --no-codeberg) NO_CODEBERG=1 ;; --no-flowcontrol) NO_FLOWCONTROL=1 ;; + --allow-dirty) ALLOW_DIRTY=1 ;; esac done @@ -33,18 +34,20 @@ fi DIFF="$(git diff --stat --patch HEAD)" -if [ -n "$DIFF" ]; then - echo there are outstanding changes: - echo "$DIFF" - exit 1 -fi +if [ -z "$ALLOW_DIRTY" ]; then + if [ -n "$DIFF" ]; then + echo there are outstanding changes: + echo "$DIFF" + exit 1 + fi -UNPUSHED="$(git log --pretty=oneline '@{u}...')" + UNPUSHED="$(git log --pretty=oneline '@{u}...')" -if [ -n "$UNPUSHED" ]; then - echo there are unpushed commits: - echo "$UNPUSHED" - exit 1 + if [ -n "$UNPUSHED" ]; then + echo there are unpushed commits: + echo "$UNPUSHED" + exit 1 + fi fi # get latest version tag From 34594942c78b1df8b9beb98588f2961a4c484e7d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 15:19:45 +0200 Subject: [PATCH 37/50] build: add source tarballs to nightly builds --- contrib/make_nightly_build | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 05a4850..9d36a6f 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -85,9 +85,13 @@ echo building... ./zig build -Dpackage_release --prefix "$DESTDIR/build" +VERSION=$(/bin/cat "$DESTDIR/build/version") + +git archive --format=tar.gz --output="$DESTDIR/flow-$VERSION-source.tar.gz" HEAD +git archive --format=zip --output="$DESTDIR/flow-$VERSION-source.zip" HEAD + cd "$DESTDIR/build" -VERSION=$(/bin/cat version) TARGETS=$(/bin/ls) for target in $TARGETS; do @@ -146,18 +150,27 @@ cd .. cat "$release_notes" +ASSETS="" + if [ -z "$NO_FLOWCONTROL" ]; then + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sig" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sha256" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sig" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sha256" echo uploading to git.flow-control.dev - tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" + tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ + $ASSETS fi if [ -z "$NO_CODEBERG" ]; then - ASSETS="" for a in $DESTDIR/*; do ASSETS="$ASSETS --asset $a" done echo uploading to codeberg.org - tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" $ASSETS + tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ + $ASSETS fi if [ -z "$NO_GITHUB" ]; then From 024eb8b43b824f3faf97930c3a48c06a5d8cf909 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 15:20:13 +0200 Subject: [PATCH 38/50] build: improve nightly build release notes --- contrib/make_nightly_build | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build index 9d36a6f..65949c5 100755 --- a/contrib/make_nightly_build +++ b/contrib/make_nightly_build @@ -128,24 +128,20 @@ echo cd .. { - echo "$title" "$VERSION" - echo - - echo "commits:" + echo "## commits in this build" echo git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" echo echo - echo "contributors to this release:" + echo "## contributors" git shortlog -s -n "${last_nightly_version}..HEAD" | cut -b 8- echo - echo - echo "also available at:" - echo "[flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION)" - echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION)" - echo "[codeberg.org](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION)" + echo "## downloads" + echo "[flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION) (source only)" + echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" + echo "[codeberg.org](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" } >"$release_notes" cat "$release_notes" @@ -175,5 +171,5 @@ fi if [ -z "$NO_GITHUB" ]; then echo uploading to github.com - gh release create "$VERSION" --repo "$repo" --notes-file "$release_notes" $DESTDIR/* + gh release create "$VERSION" --repo "$repo" --title "$title $VERSION" --notes-file "$release_notes" $DESTDIR/* fi From be758be087a20193d926abccb5c1dacffe20e87e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 15:51:27 +0200 Subject: [PATCH 39/50] feat: make delete_buffer command with no argument delete the current buffer --- src/tui/mainview.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 1243632..b8fc38b 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -617,8 +617,10 @@ const cmds = struct { pub fn delete_buffer(self: *Self, ctx: Ctx) Result { var file_path: []const u8 = undefined; - if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false)) - return error.InvalidDeleteBufferArgument; + if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false)) { + const editor = self.get_active_editor() orelse return error.InvalidDeleteBufferArgument; + file_path = editor.file_path orelse return error.InvalidDeleteBufferArgument; + } const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return; if (buffer.is_dirty()) return tp.exit("unsaved changes"); From f7496654ae3982fb39e766d87eec90d03a99525a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 15:52:18 +0200 Subject: [PATCH 40/50] feat: add vim mode aliases for buffer commands This adds these vim mode specific commands: :bd (Close file) :bw (Delete buffer) :bnext (Next buffer/tab) :bprevious (Previous buffer/tab) :ls (List/switch buffers) closes #296 --- src/tui/mode/vim.zig | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index e88cd7d..81839b9 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -51,6 +51,31 @@ const cmds_ = struct { } pub const @"e!_meta": Meta = .{ .description = "e! (force reload current file)" }; + pub fn bd(_: *void, _: Ctx) Result { + try cmd("close_file", .{}); + } + pub const bd_meta: Meta = .{ .description = "bd (Close file)" }; + + pub fn bw(_: *void, _: Ctx) Result { + try cmd("delete_buffer", .{}); + } + pub const bw_meta: Meta = .{ .description = "bw (Delete buffer)" }; + + pub fn bnext(_: *void, _: Ctx) Result { + try cmd("next_tab", .{}); + } + pub const bnext_meta: Meta = .{ .description = "bnext (Next buffer/tab)" }; + + pub fn bprevious(_: *void, _: Ctx) Result { + try cmd("next_tab", .{}); + } + pub const bprevious_meta: Meta = .{ .description = "bprevious (Previous buffer/tab)" }; + + pub fn ls(_: *void, _: Ctx) Result { + try cmd("switch_buffers", .{}); + } + pub const ls_meta: Meta = .{ .description = "ls (List/switch buffers)" }; + pub fn move_begin_or_add_integer_argument_zero(_: *void, _: Ctx) Result { return if (@import("keybind").current_integer_argument()) |_| command.executeName("add_integer_argument_digit", command.fmt(.{0})) From a9d4fed205d2158a779cee7395505abf39fb5f99 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 18:03:45 +0200 Subject: [PATCH 41/50] feat: support wide characters in win32 gui closes #132 --- src/win32/GlyphIndexCache.zig | 19 +++++++++++++------ src/win32/d3d11.zig | 11 +++++++---- src/win32/gui.zig | 23 +++++++++++++++-------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/win32/GlyphIndexCache.zig b/src/win32/GlyphIndexCache.zig index 02bc281..85fb686 100644 --- a/src/win32/GlyphIndexCache.zig +++ b/src/win32/GlyphIndexCache.zig @@ -5,9 +5,15 @@ const Node = struct { prev: ?u32, next: ?u32, codepoint: ?u21, + right_half: ?bool, }; -map: std.AutoHashMapUnmanaged(u21, u32) = .{}, +const MapKey = struct { + codepoint: u21, + right_half: bool, +}; + +map: std.AutoHashMapUnmanaged(MapKey, u32) = .{}, nodes: []Node, front: u32, back: u32, @@ -25,13 +31,14 @@ pub fn init(allocator: std.mem.Allocator, capacity: u32) error{OutOfMemory}!Glyp pub fn clearRetainingCapacity(self: *GlyphIndexCache) void { self.map.clearRetainingCapacity(); - self.nodes[0] = .{ .prev = null, .next = 1, .codepoint = null }; - self.nodes[self.nodes.len - 1] = .{ .prev = @intCast(self.nodes.len - 2), .next = null, .codepoint = null }; + self.nodes[0] = .{ .prev = null, .next = 1, .codepoint = null, .right_half = null }; + self.nodes[self.nodes.len - 1] = .{ .prev = @intCast(self.nodes.len - 2), .next = null, .codepoint = null, .right_half = null }; for (self.nodes[1 .. self.nodes.len - 1], 1..) |*node, index| { node.* = .{ .prev = @intCast(index - 1), .next = @intCast(index + 1), .codepoint = null, + .right_half = null, }; } self.front = 0; @@ -51,12 +58,12 @@ const Reserved = struct { index: u32, replaced: ?u21, }; -pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: u21) error{OutOfMemory}!union(enum) { +pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: u21, right_half: bool) error{OutOfMemory}!union(enum) { newly_reserved: Reserved, already_reserved: u32, } { { - const entry = try self.map.getOrPut(allocator, codepoint); + const entry = try self.map.getOrPut(allocator, .{ .codepoint = codepoint, .right_half = right_half }); if (entry.found_existing) { self.moveToBack(entry.value_ptr.*); return .{ .already_reserved = entry.value_ptr.* }; @@ -69,7 +76,7 @@ pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: const replaced = self.nodes[self.front].codepoint; self.nodes[self.front].codepoint = codepoint; if (replaced) |r| { - const removed = self.map.remove(r); + const removed = self.map.remove(.{ .codepoint = r, .right_half = self.nodes[self.front].right_half orelse false }); std.debug.assert(removed); } const save_front = self.front; diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index 7dbe105..529ba1c 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -149,7 +149,7 @@ pub const WindowState = struct { } // TODO: this should take a utf8 graphme instead - pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21) u32 { + pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21, right_half: bool) u32 { // for now we'll just use 1 texture and leverage the entire thing const texture_cell_count: XY(u16) = getD3d11TextureMaxCellCount(font.cell_size); const texture_cell_count_total: u32 = @@ -187,12 +187,15 @@ pub const WindowState = struct { switch (glyph_index_cache.reserve( global.glyph_cache_arena.allocator(), codepoint, + right_half, ) catch |e| oom(e)) { .newly_reserved => |reserved| { // var render_success = false; // defer if (!render_success) state.glyph_index_cache.remove(reserved.index); const pos: XY(u16) = cellPosFromIndex(reserved.index, texture_cell_count.x); const coord = coordFromCellPos(font.cell_size, pos); + var staging_size = font.cell_size; + staging_size.x = if (right_half) staging_size.x * 2 else staging_size.x; const staging = global.staging_texture.update(font.cell_size); var utf8_buf: [7]u8 = undefined; const utf8_len: u3 = std.unicode.utf8Encode(codepoint, &utf8_buf) catch |e| std.debug.panic( @@ -204,10 +207,10 @@ pub const WindowState = struct { utf8_buf[0..utf8_len], ); const box: win32.D3D11_BOX = .{ - .left = 0, + .left = if (right_half) font.cell_size.x else 0, .top = 0, .front = 0, - .right = font.cell_size.x, + .right = if (right_half) font.cell_size.x * 2 else font.cell_size.x, .bottom = font.cell_size.y, .back = 1, }; @@ -289,7 +292,7 @@ pub fn paint( } const copy_col_count: u16 = @min(col_count, shader_col_count); - const blank_space_glyph_index = state.generateGlyph(font, ' '); + const blank_space_glyph_index = state.generateGlyph(font, ' ', false); const cell_count: u32 = @as(u32, shader_col_count) * @as(u32, shader_row_count); state.shader_cells.updateCount(cell_count); diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 6d49cec..485d1b4 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1081,19 +1081,26 @@ fn WndProc( global.render_cells_arena.allocator(), global.screen.buf.len, ) catch |e| oom(e); + var prev_width: usize = 1; + var prev_cell: render.Cell = undefined; for (global.screen.buf, global.render_cells.items) |*screen_cell, *render_cell| { + const width = screen_cell.char.width; const codepoint = if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) std.unicode.wtf8Decode(screen_cell.char.grapheme) catch std.unicode.replacement_character else std.unicode.replacement_character; - render_cell.* = .{ - .glyph_index = state.render_state.generateGlyph( - font, - codepoint, - ), - .background = renderColorFromVaxis(screen_cell.style.bg), - .foreground = renderColorFromVaxis(screen_cell.style.fg), - }; + if (prev_width > 1) { + render_cell.* = prev_cell; + render_cell.glyph_index = state.render_state.generateGlyph(font, codepoint, true); + } else { + render_cell.* = .{ + .glyph_index = state.render_state.generateGlyph(font, codepoint, false), + .background = renderColorFromVaxis(screen_cell.style.bg), + .foreground = renderColorFromVaxis(screen_cell.style.fg), + }; + } + prev_width = width; + prev_cell = render_cell.*; } render.paint( &state.render_state, From 8278a080aff87ff0c8b41e65d33519f2c06abe6f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 18:09:09 +0200 Subject: [PATCH 42/50] fix: actually use staging_size in WindowState.generateGlyph --- src/win32/d3d11.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index 529ba1c..b1b88cf 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -196,7 +196,7 @@ pub const WindowState = struct { const coord = coordFromCellPos(font.cell_size, pos); var staging_size = font.cell_size; staging_size.x = if (right_half) staging_size.x * 2 else staging_size.x; - const staging = global.staging_texture.update(font.cell_size); + const staging = global.staging_texture.update(staging_size); var utf8_buf: [7]u8 = undefined; const utf8_len: u3 = std.unicode.utf8Encode(codepoint, &utf8_buf) catch |e| std.debug.panic( "todo: handle invalid codepoint {} (0x{0x}) ({s})", From 05b87b1406d10a839a8864e13930f486f647fde4 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 23 Sep 2025 13:32:13 -0600 Subject: [PATCH 43/50] finish win32 gui support for double-wide characters --- src/win32/DwriteRenderer.zig | 6 +++++- src/win32/d3d11.zig | 20 ++++++++++++++++---- src/win32/gui.zig | 6 ++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/win32/DwriteRenderer.zig b/src/win32/DwriteRenderer.zig index 8ad34a0..39abee5 100644 --- a/src/win32/DwriteRenderer.zig +++ b/src/win32/DwriteRenderer.zig @@ -75,6 +75,7 @@ pub fn render( self: *const DwriteRenderer, font: Font, utf8: []const u8, + double_width: bool, ) void { var utf16_buf: [10]u16 = undefined; const utf16_len = std.unicode.utf8ToUtf16Le(&utf16_buf, utf8) catch unreachable; @@ -85,7 +86,10 @@ pub fn render( const rect: win32.D2D_RECT_F = .{ .left = 0, .top = 0, - .right = @floatFromInt(font.cell_size.x), + .right = if (double_width) + @as(f32, @floatFromInt(font.cell_size.x)) * @as(f32, @floatFromInt(font.cell_size.x)) + else + @as(f32, @floatFromInt(font.cell_size.x)), .bottom = @floatFromInt(font.cell_size.y), }; self.render_target.BeginDraw(); diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index b1b88cf..dd1c5a2 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -149,7 +149,7 @@ pub const WindowState = struct { } // TODO: this should take a utf8 graphme instead - pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21, right_half: bool) u32 { + pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21, kind: enum { single, left, right }) u32 { // for now we'll just use 1 texture and leverage the entire thing const texture_cell_count: XY(u16) = getD3d11TextureMaxCellCount(font.cell_size); const texture_cell_count_total: u32 = @@ -184,6 +184,11 @@ pub const WindowState = struct { break :blk &(state.glyph_index_cache.?); }; + const right_half: bool = switch (kind) { + .single, .left => false, + .right => true, + }; + switch (glyph_index_cache.reserve( global.glyph_cache_arena.allocator(), codepoint, @@ -194,8 +199,11 @@ pub const WindowState = struct { // defer if (!render_success) state.glyph_index_cache.remove(reserved.index); const pos: XY(u16) = cellPosFromIndex(reserved.index, texture_cell_count.x); const coord = coordFromCellPos(font.cell_size, pos); - var staging_size = font.cell_size; - staging_size.x = if (right_half) staging_size.x * 2 else staging_size.x; + const staging_size: XY(u16) = .{ + // twice the width to handle double-wide glyphs + .x = font.cell_size.x * 2, + .y = font.cell_size.y, + }; const staging = global.staging_texture.update(staging_size); var utf8_buf: [7]u8 = undefined; const utf8_len: u3 = std.unicode.utf8Encode(codepoint, &utf8_buf) catch |e| std.debug.panic( @@ -205,6 +213,10 @@ pub const WindowState = struct { staging.text_renderer.render( font, utf8_buf[0..utf8_len], + switch (kind) { + .single => false, + .left, .right => true, + }, ); const box: win32.D3D11_BOX = .{ .left = if (right_half) font.cell_size.x else 0, @@ -292,7 +304,7 @@ pub fn paint( } const copy_col_count: u16 = @min(col_count, shader_col_count); - const blank_space_glyph_index = state.generateGlyph(font, ' ', false); + const blank_space_glyph_index = state.generateGlyph(font, ' ', .single); const cell_count: u32 = @as(u32, shader_col_count) * @as(u32, shader_row_count); state.shader_cells.updateCount(cell_count); diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 485d1b4..bab2196 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1083,6 +1083,7 @@ fn WndProc( ) catch |e| oom(e); var prev_width: usize = 1; var prev_cell: render.Cell = undefined; + var prev_codepoint: u21 = undefined; for (global.screen.buf, global.render_cells.items) |*screen_cell, *render_cell| { const width = screen_cell.char.width; const codepoint = if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) @@ -1091,16 +1092,17 @@ fn WndProc( std.unicode.replacement_character; if (prev_width > 1) { render_cell.* = prev_cell; - render_cell.glyph_index = state.render_state.generateGlyph(font, codepoint, true); + render_cell.glyph_index = state.render_state.generateGlyph(font, prev_codepoint, .right); } else { render_cell.* = .{ - .glyph_index = state.render_state.generateGlyph(font, codepoint, false), + .glyph_index = state.render_state.generateGlyph(font, codepoint, if (width == 1) .single else .left), .background = renderColorFromVaxis(screen_cell.style.bg), .foreground = renderColorFromVaxis(screen_cell.style.fg), }; } prev_width = width; prev_cell = render_cell.*; + prev_codepoint = codepoint; } render.paint( &state.render_state, From 2790dcfd11bbb7c4b8bb0247a5fd051bba34ba91 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 23 Sep 2025 14:03:34 -0600 Subject: [PATCH 44/50] add some new text to the font test --- src/tui/fonts.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/tui/fonts.zig b/src/tui/fonts.zig index 35ef2d6..1c78176 100644 --- a/src/tui/fonts.zig +++ b/src/tui/fonts.zig @@ -224,6 +224,17 @@ pub const font_test_text: []const u8 = \\🙂‍↔ \\ \\ + \\你好世界 "Hello World" + \\一二三四五六七八九十 "123456789" + \\龍鳳麟龜 (dragon, phoenix, qilin, turtle) + \\Fullwidth numbers: 1234567890 + \\Fullwidth letters: ABCDEFG abcdefg + \\Fullwidth punctuation: !@#$%^&*() + \\Half-width (normal): ABC 123 + \\Full-width (double): ABC 123 + \\Punctuation: 。,、;:「」『』 + \\Symbols: ○●□■△▲☆★◇◆ + \\ \\ recommended fonts for terminals with no nerdfont fallback support (e.g. flow-gui): \\ \\ "IosevkaTerm Nerd Font" => https://github.com/ryanoasis/nerd-fonts/releases/download/v3.3.0/IosevkaTerm.zip From 921f09450924936625df6c1c855e3cfeac47cf7a Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 23 Sep 2025 14:04:58 -0600 Subject: [PATCH 45/50] workaround crash when rendering some utf8 on win32 gui closes #194 Ignores cells that have graphemes with more than 1 codepoint rather than crash. --- src/win32/gui.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/win32/gui.zig b/src/win32/gui.zig index bab2196..d1bcaed 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1086,7 +1086,10 @@ fn WndProc( var prev_codepoint: u21 = undefined; for (global.screen.buf, global.render_cells.items) |*screen_cell, *render_cell| { const width = screen_cell.char.width; - const codepoint = if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) + // temporary workaround, ignore multi-codepoint graphemes + const codepoint = if (screen_cell.char.grapheme.len > 4) + std.unicode.replacement_character + else if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) std.unicode.wtf8Decode(screen_cell.char.grapheme) catch std.unicode.replacement_character else std.unicode.replacement_character; From 5cc6724a079ca87f769636d19fb04b0dd79d1137 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 23 Sep 2025 14:05:24 -0600 Subject: [PATCH 46/50] win32 gui: center double-wide characters --- src/win32/DwriteRenderer.zig | 4 +-- src/win32/dwrite.zig | 47 ++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/win32/DwriteRenderer.zig b/src/win32/DwriteRenderer.zig index 39abee5..0a9b20d 100644 --- a/src/win32/DwriteRenderer.zig +++ b/src/win32/DwriteRenderer.zig @@ -87,7 +87,7 @@ pub fn render( .left = 0, .top = 0, .right = if (double_width) - @as(f32, @floatFromInt(font.cell_size.x)) * @as(f32, @floatFromInt(font.cell_size.x)) + @as(f32, @floatFromInt(font.cell_size.x)) * 2 else @as(f32, @floatFromInt(font.cell_size.x)), .bottom = @floatFromInt(font.cell_size.y), @@ -100,7 +100,7 @@ pub fn render( self.render_target.DrawText( @ptrCast(utf16.ptr), @intCast(utf16.len), - font.text_format, + if (double_width) font.text_format_double else font.text_format_single, &rect, &self.white_brush.ID2D1Brush, .{}, diff --git a/src/win32/dwrite.zig b/src/win32/dwrite.zig index 0bd85f2..23129e7 100644 --- a/src/win32/dwrite.zig +++ b/src/win32/dwrite.zig @@ -23,11 +23,13 @@ pub fn init() void { } pub const Font = struct { - text_format: *win32.IDWriteTextFormat, + text_format_single: *win32.IDWriteTextFormat, + text_format_double: *win32.IDWriteTextFormat, cell_size: XY(u16), pub fn init(dpi: u32, size: f32, face: *const FontFace) Font { - var text_format: *win32.IDWriteTextFormat = undefined; + var text_format_single: *win32.IDWriteTextFormat = undefined; + { const hr = global.dwrite_factory.CreateTextFormat( face.ptr(), @@ -37,14 +39,43 @@ pub const Font = struct { .NORMAL, // stretch win32.scaleDpi(f32, size, dpi), win32.L(""), // locale - &text_format, + &text_format_single, ); if (hr < 0) std.debug.panic( "CreateTextFormat '{}' height {d} failed, hresult=0x{x}", .{ std.unicode.fmtUtf16Le(face.slice()), size, @as(u32, @bitCast(hr)) }, ); } - errdefer _ = text_format.IUnknown.Release(); + errdefer _ = text_format_single.IUnknown.Release(); + + var text_format_double: *win32.IDWriteTextFormat = undefined; + { + const hr = global.dwrite_factory.CreateTextFormat( + face.ptr(), + null, + .NORMAL, //weight + .NORMAL, // style + .NORMAL, // stretch + win32.scaleDpi(f32, size, dpi), + win32.L(""), // locale + &text_format_double, + ); + if (hr < 0) std.debug.panic( + "CreateTextFormat '{}' height {d} failed, hresult=0x{x}", + .{ std.unicode.fmtUtf16Le(face.slice()), size, @as(u32, @bitCast(hr)) }, + ); + } + errdefer _ = text_format_double.IUnknown.Release(); + + { + const hr = text_format_double.SetTextAlignment(win32.DWRITE_TEXT_ALIGNMENT_CENTER); + if (hr < 0) fatalHr("SetTextAlignment", hr); + } + + { + const hr = text_format_double.SetParagraphAlignment(win32.DWRITE_PARAGRAPH_ALIGNMENT_CENTER); + if (hr < 0) fatalHr("SetParagraphAlignment", hr); + } const cell_size: XY(u16) = blk: { var text_layout: *win32.IDWriteTextLayout = undefined; @@ -52,7 +83,7 @@ pub const Font = struct { const hr = global.dwrite_factory.CreateTextLayout( win32.L("█"), 1, - text_format, + text_format_single, std.math.floatMax(f32), std.math.floatMax(f32), &text_layout, @@ -73,13 +104,15 @@ pub const Font = struct { }; return .{ - .text_format = text_format, + .text_format_single = text_format_single, + .text_format_double = text_format_double, .cell_size = cell_size, }; } pub fn deinit(self: *Font) void { - _ = self.text_format.IUnknown.Release(); + _ = self.text_format_single.IUnknown.Release(); + _ = self.text_format_double.IUnknown.Release(); self.* = undefined; } From 14dbc08bcf9db9578f812e856ee6961751766980 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 22:31:12 +0200 Subject: [PATCH 47/50] feat: add string mappings for keypad key events --- src/renderer/vaxis/input.zig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/renderer/vaxis/input.zig b/src/renderer/vaxis/input.zig index c8bdcf2..c21b7ba 100644 --- a/src/renderer/vaxis/input.zig +++ b/src/renderer/vaxis/input.zig @@ -231,6 +231,25 @@ pub const utils = struct { vaxis.Key.f33 => "f33", vaxis.Key.f34 => "f34", vaxis.Key.f35 => "f35", + vaxis.Key.kp_decimal => "kp_decimal", + vaxis.Key.kp_divide => "kp_divide", + vaxis.Key.kp_multiply => "kp_multiply", + vaxis.Key.kp_subtract => "kp_subtract", + vaxis.Key.kp_add => "kp_add", + vaxis.Key.kp_enter => "kp_enter", + vaxis.Key.kp_equal => "kp_equal", + vaxis.Key.kp_separator => "kp_separator", + vaxis.Key.kp_left => "kp_left", + vaxis.Key.kp_right => "kp_right", + vaxis.Key.kp_up => "kp_up", + vaxis.Key.kp_down => "kp_down", + vaxis.Key.kp_page_up => "kp_page_up", + vaxis.Key.kp_page_down => "kp_page_down", + vaxis.Key.kp_home => "kp_home", + vaxis.Key.kp_end => "kp_end", + vaxis.Key.kp_insert => "kp_insert", + vaxis.Key.kp_delete => "kp_delete", + vaxis.Key.kp_begin => "kp_begin", vaxis.Key.media_play => "media_play", vaxis.Key.media_pause => "media_pause", vaxis.Key.media_play_pause => "media_play_pause", From 82c11c64f3839e3ae3955942d2759f2764f3f853 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 22:52:21 +0200 Subject: [PATCH 48/50] feat: add keybindings for keypad navigation keys --- src/keybind/builtin/flow.json | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index c139a3a..dbed511 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -77,12 +77,20 @@ ["ctrl+enter", "smart_insert_line_after"], ["ctrl+end", "move_buffer_end"], ["ctrl+home", "move_buffer_begin"], + ["ctrl+kp_end", "move_buffer_end"], + ["ctrl+kp_home", "move_buffer_begin"], ["ctrl+up", "move_scroll_up"], ["ctrl+down", "move_scroll_down"], + ["ctrl+kp_up", "move_scroll_up"], + ["ctrl+kp_down", "move_scroll_down"], ["ctrl+page_up", "move_scroll_page_up"], ["ctrl+page_down", "move_scroll_page_down"], + ["ctrl+kp_page_up", "move_scroll_page_up"], + ["ctrl+kp_page_down", "move_scroll_page_down"], ["ctrl+left", "move_word_left"], ["ctrl+right", "move_word_right"], + ["ctrl+kp_left", "move_word_left"], + ["ctrl+kp_right", "move_word_right"], ["ctrl+backspace", "delete_word_left"], ["ctrl+delete", "delete_word_right"], ["ctrl+f5", "toggle_inspector_view"], @@ -98,10 +106,16 @@ ["ctrl+shift+enter", "smart_insert_line_before"], ["ctrl+shift+end", "select_buffer_end"], ["ctrl+shift+home", "select_buffer_begin"], + ["ctrl+shift+kp_end", "select_buffer_end"], + ["ctrl+shift+kp_home", "select_buffer_begin"], ["ctrl+shift+up", "select_scroll_up"], ["ctrl+shift+down", "select_scroll_down"], + ["ctrl+shift+kp_up", "select_scroll_up"], + ["ctrl+shift+kp_down", "select_scroll_down"], ["ctrl+shift+left", "select_word_left"], ["ctrl+shift+right", "select_word_right"], + ["ctrl+shift+kp_left", "select_word_left"], + ["ctrl+shift+kp_right", "select_word_right"], ["ctrl+shift+space", "selections_reverse"], ["alt+o", "open_previous_file"], ["alt+j", "join_next_line"], @@ -117,8 +131,12 @@ ["alt+R", ["shell_execute_insert", "openssl", "rand", "-hex", "4"]], ["alt+left", "jump_back"], ["alt+right", "jump_forward"], + ["alt+kp_left", "jump_back"], + ["alt+kp_right", "jump_forward"], ["alt+up", "pull_up"], ["alt+down", "pull_down"], + ["alt+kp_up", "pull_up"], + ["alt+kp_down", "pull_down"], ["alt+enter", "insert_line"], ["alt+f10", "gutter_mode_next"], ["alt+shift+f10", "gutter_style_next"], @@ -130,8 +148,12 @@ ["alt+shift+i", "add_cursors_to_line_ends"], ["alt+shift+left", "expand_selection"], ["alt+shift+right", "shrink_selection"], + ["alt+shift+kp_left", "expand_selection"], + ["alt+shift+kp_right", "shrink_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], + ["alt+kp_home", "select_prev_sibling"], + ["alt+kp_end", "select_next_sibling"], ["alt+{", "expand_selection"], ["alt+}", "shrink_selection", true], ["alt+[", "select_prev_sibling", true], @@ -141,20 +163,32 @@ ["alt+a", "select_all_siblings"], ["alt+shift+home", "move_scroll_left"], ["alt+shift+end", "move_scroll_right"], + ["alt+shift+kp_home", "move_scroll_left"], + ["alt+shift+kp_end", "move_scroll_right"], ["alt+shift+up", "add_cursor_up"], ["alt+shift+down", "add_cursor_down"], + ["alt+shift+kp_up", "add_cursor_up"], + ["alt+shift+kp_down", "add_cursor_down"], ["alt+shift+f12", "goto_type_definition"], ["shift+f3", "goto_prev_match"], ["shift+f10", "toggle_syntax_highlighting"], ["shift+f12", "references"], ["shift+left", "select_left"], ["shift+right", "select_right"], + ["shift+kp_left", "select_left"], + ["shift+kp_right", "select_right"], ["shift+up", "select_up"], ["shift+down", "select_down"], + ["shift+kp_up", "select_up"], + ["shift+kp_down", "select_down"], ["shift+home", "smart_select_begin"], ["shift+end", "select_end"], + ["shift+kp_home", "smart_select_begin"], + ["shift+kp_end", "select_end"], ["shift+page_up", "select_page_up"], ["shift+page_down", "select_page_down"], + ["shift+kp_page_up", "select_page_up"], + ["shift+kp_page_down", "select_page_down"], ["shift+enter", "smart_insert_line_before"], ["shift+backspace", "delete_backward"], ["shift+tab", "unindent"], @@ -177,12 +211,20 @@ ["backspace", "smart_delete_backward"], ["left", "move_left"], ["right", "move_right"], + ["kp_left", "move_left"], + ["kp_right", "move_right"], ["up", "move_up"], ["down", "move_down"], + ["kp_up", "move_up"], + ["kp_down", "move_down"], ["home", "smart_move_begin"], ["end", "move_end"], + ["kp_home", "smart_move_begin"], + ["kp_end", "move_end"], ["page_up", "move_page_up"], ["page_down", "move_page_down"], + ["kp_page_up", "move_page_up"], + ["kp_page_down", "move_page_down"], ["tab", "indent"], ["ctrl+space", "enter_mode", "select"], @@ -235,16 +277,30 @@ ["right", "select_right"], ["ctrl+left", "select_word_left"], ["ctrl+right", "select_word_right"], + ["kp_left", "select_left"], + ["kp_right", "select_right"], + ["ctrl+kp_left", "select_word_left"], + ["ctrl+kp_right", "select_word_right"], ["up", "select_up"], ["down", "select_down"], + ["kp_up", "select_up"], + ["kp_down", "select_down"], ["home", "select_begin"], ["end", "select_end"], + ["kp_home", "select_begin"], + ["kp_end", "select_end"], ["ctrl+home", "select_buffer_begin"], ["ctrl+end", "select_buffer_end"], + ["ctrl+kp_home", "select_buffer_begin"], + ["ctrl+kp_end", "select_buffer_end"], ["page_up", "select_page_up"], ["page_down", "select_page_down"], ["ctrl+page_up", "select_scroll_page_up"], ["ctrl+page_down", "select_scroll_page_down"], + ["kp_page_up", "select_page_up"], + ["kp_page_down", "select_page_down"], + ["ctrl+kp_page_up", "select_scroll_page_up"], + ["ctrl+kp_page_down", "select_scroll_page_down"], ["ctrl+b", "move_to_char", "select_to_char_left"], ["ctrl+t", "move_to_char", "select_to_char_right"], ["ctrl+space", "enter_mode", "normal"], @@ -286,6 +342,8 @@ ["q", "quit"], ["up", "home_menu_up"], ["down", "home_menu_down"], + ["kp_up", "home_menu_up"], + ["kp_down", "home_menu_down"], ["enter", "home_menu_activate"] ] }, @@ -308,8 +366,12 @@ ["ctrl+escape", "palette_menu_cancel"], ["ctrl+up", "palette_menu_up"], ["ctrl+down", "palette_menu_down"], + ["ctrl+kp_up", "palette_menu_up"], + ["ctrl+kp_down", "palette_menu_down"], ["ctrl+page_up", "palette_menu_pageup"], ["ctrl+page_down", "palette_menu_pagedown"], + ["ctrl+kp_page_up", "palette_menu_pageup"], + ["ctrl+kp_page_down", "palette_menu_pagedown"], ["ctrl+enter", "palette_menu_activate"], ["ctrl+backspace", "overlay_delete_word_left"], ["ctrl+shift+e", "palette_menu_up"], @@ -330,10 +392,16 @@ ["escape", "palette_menu_cancel"], ["up", "palette_menu_up"], ["down", "palette_menu_down"], + ["kp_up", "palette_menu_up"], + ["kp_down", "palette_menu_down"], ["page_up", "palette_menu_pageup"], ["page_down", "palette_menu_pagedown"], + ["kp_page_up", "palette_menu_pageup"], + ["kp_page_down", "palette_menu_pagedown"], ["home", "palette_menu_top"], ["end", "palette_menu_bottom"], + ["kp_home", "palette_menu_top"], + ["kp_end", "palette_menu_bottom"], ["enter", "palette_menu_activate"], ["delete", "palette_menu_delete_item"], ["backspace", "overlay_delete_backwards"] @@ -404,8 +472,12 @@ ["shift+tab", "mini_mode_reverse_complete_file"], ["up", "mini_mode_reverse_complete_file"], ["down", "mini_mode_try_complete_file"], - ["right", "mini_mode_try_complete_file_forward"], + ["kp_up", "mini_mode_reverse_complete_file"], + ["kp_down", "mini_mode_try_complete_file"], ["left", "mini_mode_delete_to_previous_path_segment"], + ["right", "mini_mode_try_complete_file_forward"], + ["kp_left", "mini_mode_delete_to_previous_path_segment"], + ["kp_right", "mini_mode_try_complete_file_forward"], ["tab", "mini_mode_try_complete_file"], ["escape", "mini_mode_cancel"], ["enter", "mini_mode_select"], @@ -435,6 +507,8 @@ ["shift+f3", "goto_prev_match"], ["up", "select_prev_file"], ["down", "select_next_file"], + ["kp_up", "select_prev_file"], + ["kp_down", "select_next_file"], ["f3", "goto_next_match"], ["f15", "goto_prev_match"], ["f9", "theme_prev"], @@ -467,6 +541,8 @@ ["shift+f3", "goto_prev_match"], ["up", "mini_mode_history_prev"], ["down", "mini_mode_history_next"], + ["kp_up", "mini_mode_history_prev"], + ["kp_down", "mini_mode_history_next"], ["f3", "goto_next_match"], ["f15", "goto_prev_match"], ["f9", "theme_prev"], From 622d65497aca9849712b2323bafbcec9dfa9aa24 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Sep 2025 22:58:52 +0200 Subject: [PATCH 49/50] feat: add helix mode keybindings for keypad keys --- src/keybind/builtin/helix.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/keybind/builtin/helix.json b/src/keybind/builtin/helix.json index 2129459..c9226cd 100644 --- a/src/keybind/builtin/helix.json +++ b/src/keybind/builtin/helix.json @@ -111,6 +111,8 @@ ["home", "move_begin"], ["end", "move_end"], + ["kp_home", "move_begin"], + ["kp_end", "move_end"], ["w","move_next_word_start"], ["b","move_prev_word_start"], @@ -201,6 +203,8 @@ ["page_up", "move_scroll_page_up"], ["page_down", "move_scroll_page_down"], + ["kp_page_up", "move_scroll_page_up"], + ["kp_page_down", "move_scroll_page_down"], ["space F", "find_file"], ["space S", "workspace_symbol_picker"], @@ -293,6 +297,10 @@ ["alt+down", "shrink_selection"], ["alt+left", "select_prev_sibling"], ["alt+right", "select_next_sibling"], + ["alt+kp_up", "expand_selection"], + ["alt+kp_down", "shrink_selection"], + ["alt+kp_left", "select_prev_sibling"], + ["alt+kp_right", "select_next_sibling"], ["alt+e", "extend_parent_node_end"], ["alt+b", "extend_parent_node_start"], @@ -378,6 +386,10 @@ ["down", "select_down"], ["up", "select_up"], ["right", "select_right"], + ["kp_left", "select_left"], + ["kp_down", "select_down"], + ["kp_up", "select_up"], + ["kp_right", "select_right"], ["t", "extend_till_char"], ["f", "move_to_char", "select_to_char_right_helix"], @@ -386,6 +398,8 @@ ["home", "extend_to_line_start"], ["end", "extend_to_line_end"], + ["kp_home", "extend_to_line_start"], + ["kp_end", "extend_to_line_end"], ["w", "extend_next_word_start"], ["b", "extend_pre_word_start"], From bcef17a4664659a997cdef48a855664f4c481f8a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 24 Sep 2025 13:59:56 +0200 Subject: [PATCH 50/50] fix: make sure we don't destroy file_path before navigating in open_file mini mode --- src/tui/mode/mini/open_file.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/mini/open_file.zig b/src/tui/mode/mini/open_file.zig index 07f70e4..fc13d55 100644 --- a/src/tui/mode/mini/open_file.zig +++ b/src/tui/mode/mini/open_file.zig @@ -37,9 +37,9 @@ pub fn select(self: *Type) void { var buf = std.ArrayList(u8).init(self.allocator); defer buf.deinit(); const file_path = project_manager.expand_home(&buf, self.file_path.items); - command.executeName("exit_mini_mode", .{}) catch {}; if (root.is_directory(file_path)) tp.self_pid().send(.{ "cmd", "change_project", .{file_path} }) catch {} else if (file_path.len > 0) tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch {}; + command.executeName("exit_mini_mode", .{}) catch {}; }