From a8437d61391479c1f56ae2ced5238d6f6973788b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 09:52:31 +0200 Subject: [PATCH 1/6] feat(themes): add Kanso theme (zen, ink, mist, pearl variants) --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 095e111d..48e0c7d0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,8 +22,8 @@ .hash = "thespian-0.0.1-owFOjlgiBgC8w4XqkCOegxz5vMy6kNErcssWQWf2QHeE", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-c6c7f18cfb2e3945cd0b71dab24271465074dbc3/flow-themes.tar.gz", - .hash = "N-V-__8AAOKzJACguNxU76WX9M7RIhOYGuLnlasJ1-GDdhqT", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-750400d02ea8cacaabc869cd4d34dcebf04a53c8/flow-themes.tar.gz", + .hash = "N-V-__8AAEWxJQAyUV_rvRIWHB8EhIBxpQXqCB68SpilIjEt", }, .fuzzig = .{ .url = "https://github.com/fjebaker/fuzzig/archive/4251fe4230d38e721514394a485db62ee1667ff3.tar.gz", From ba840b72e0d48e4ea5dc6ab24ffa5fd3b336cf38 Mon Sep 17 00:00:00 2001 From: Paul Graydon Date: Sat, 14 Feb 2026 23:09:57 +0100 Subject: [PATCH 2/6] feat: [vim] Add word textobject actions --- src/keybind/builtin/vim.json | 12 ++++ src/tui/editor.zig | 12 +++- src/tui/mode/vim.zig | 128 ++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index a844be61..587f148f 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -81,14 +81,23 @@ ["dgg", "cut_buffer_begin"], ["\"_dd", "delete_line"], + ["diw", "cut_inside_word"], + ["daw", "cut_around_word"], + ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], ["D", "cut_to_end_vim"], ["cw", ["enter_mode", "insert"], ["cut_word_right_vim"]], ["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]], + ["ciw", ["enter_mode", "insert"], ["cut_inside_word"]], + ["caw", ["enter_mode", "insert"], ["cut_around_word"]], + ["yy", ["copy_line_internal_vim"], ["cancel"]], + ["yiw", ["copy_inside_word"], ["cancel"]], + ["yaw", ["copy_around_word"], ["cancel"]], + ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -159,6 +168,9 @@ ["B", "select_word_left"], ["e", "select_word_right_end_vim"], + ["iw", "select_inside_word"], + ["aw", "select_around_word"], + ["^", "smart_move_begin"], ["$", "select_end"], [":", "open_command_palette"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index bdac85ac..e65dd6d5 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2628,7 +2628,15 @@ pub const Editor = struct { return cursor.test_at(root, is_whitespace, metrics); } - fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + pub fn is_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, is_whitespace_or_eol, metrics); + } + + pub fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return !cursor.test_at(root, is_whitespace, metrics); + } + + pub fn is_non_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { return !cursor.test_at(root, is_whitespace_or_eol, metrics); } @@ -3745,7 +3753,7 @@ pub const Editor = struct { } pub fn move_cursor_right_until_non_whitespace(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { - move_cursor_right_until(root, cursor, is_non_whitespace_at_cursor, metrics); + move_cursor_right_until(root, cursor, is_non_whitespace_or_eol_at_cursor, metrics); } pub fn move_word_left(self: *Self, ctx: Context) Result { diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 38eacbee..f7067e50 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -2,6 +2,13 @@ const std = @import("std"); const command = @import("command"); const cmd = command.executeName; +const tui = @import("../tui.zig"); + +const Buffer = @import("Buffer"); +const Cursor = Buffer.Cursor; +const CurSel = @import("../editor.zig").CurSel; +const Editor = @import("../editor.zig").Editor; + var commands: Commands = undefined; pub fn init() !void { @@ -138,6 +145,125 @@ const cmds_ = struct { //TODO return undefined; } - pub const copy_line_meta: Meta = .{ .description = "Copies the current line" }; + + pub fn select_inside_word(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + } + pub const select_inside_word_meta: Meta = .{ .description = "Select inside word" }; + + pub fn select_around_word(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + } + pub const select_around_word_meta: Meta = .{ .description = "Select around word" }; + + pub fn cut_inside_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_word_meta: Meta = .{ .description = "Cut inside word" }; + + pub fn cut_around_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" }; + + pub fn copy_inside_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_word_meta: Meta = .{ .description = "Copy inside word" }; + + pub fn copy_around_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" }; }; + +fn is_tab_or_space(c: []const u8) bool { + return (c[0] == ' ') or (c[0] == '\t'); +} + +fn is_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, is_tab_or_space, metrics); +} +fn is_not_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return !cursor.test_at(root, is_tab_or_space, metrics); +} + +fn select_inside_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_word_textobject(root, cursel, metrics, .inside); +} + +fn select_around_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_word_textobject(root, cursel, metrics, .around); +} + +fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, scope: enum { inside, around }) !void { + var prev = cursel.cursor; + var next = cursel.cursor; + + if (cursel.cursor.test_at(root, Editor.is_non_word_char, metrics)) { + if (cursel.cursor.test_at(root, Editor.is_whitespace_or_eol, metrics)) { + Editor.move_cursor_left_until(root, &prev, Editor.is_non_whitespace_at_cursor, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_non_whitespace_at_cursor, metrics); + } else { + Editor.move_cursor_left_until(root, &prev, Editor.is_whitespace_or_eol_at_cursor, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_whitespace_or_eol_at_cursor, metrics); + } + prev.move_right(root, metrics) catch {}; + } else { + Editor.move_cursor_left_until(root, &prev, Editor.is_word_boundary_left_vim, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_word_boundary_right_vim, metrics); + next.move_right(root, metrics) catch {}; + } + + if (scope == .around) { + const inside_prev = prev; + const inside_next = next; + + if (next.test_at(root, is_tab_or_space, metrics)) { + Editor.move_cursor_right_until(root, &next, is_not_tab_or_space_at_cursor, metrics); + } else { + next = inside_next; + prev.move_left(root, metrics) catch {}; + if (prev.test_at(root, is_tab_or_space, metrics)) { + Editor.move_cursor_left_until(root, &prev, is_not_tab_or_space_at_cursor, metrics); + prev.move_right(root, metrics) catch {}; + } else { + prev = inside_prev; + } + } + } + + const sel = cursel.enable_selection(root, metrics); + sel.begin = prev; + sel.end = next; + cursel.*.cursor = next; +} From ce7cc48a722fc5410ae653dab31b3a56216f9113 Mon Sep 17 00:00:00 2001 From: Paul Graydon Date: Sun, 8 Mar 2026 16:23:21 +0100 Subject: [PATCH 3/6] feat: [vim] Add bracket textobject actions --- src/keybind/builtin/vim.json | 52 +++++++ src/tui/editor.zig | 2 +- src/tui/mode/vim.zig | 274 +++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 1 deletion(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 587f148f..07d82099 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -82,7 +82,20 @@ ["\"_dd", "delete_line"], ["diw", "cut_inside_word"], + ["di(", "cut_inside_parentheses"], + ["di)", "cut_inside_parentheses"], + ["di[", "cut_inside_square_brackets"], + ["di]", "cut_inside_square_brackets"], + ["di{", "cut_inside_braces"], + ["di}", "cut_inside_braces"], + ["daw", "cut_around_word"], + ["da(", "cut_around_parentheses"], + ["da)", "cut_around_parentheses"], + ["da[", "cut_around_square_brackets"], + ["da]", "cut_around_square_brackets"], + ["da{", "cut_around_braces"], + ["da}", "cut_around_braces"], ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], @@ -91,12 +104,38 @@ ["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]], ["ciw", ["enter_mode", "insert"], ["cut_inside_word"]], + ["ci(", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci)", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci[", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["caw", ["enter_mode", "insert"], ["cut_around_word"]], + ["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca)", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca[", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca{", ["enter_mode", "insert"], ["cut_around_braces"]], + ["ca}", ["enter_mode", "insert"], ["cut_around_braces"]], ["yy", ["copy_line_internal_vim"], ["cancel"]], ["yiw", ["copy_inside_word"], ["cancel"]], + ["yi(", ["copy_inside_parentheses"], ["cancel"]], + ["yi)", ["copy_inside_parentheses"], ["cancel"]], + ["yi[", ["copy_inside_square_brackets"], ["cancel"]], + ["yi]", ["copy_inside_square_brackets"], ["cancel"]], + ["yi{", ["copy_inside_braces"], ["cancel"]], + ["yi}", ["copy_inside_braces"], ["cancel"]], + ["yaw", ["copy_around_word"], ["cancel"]], + ["ya(", ["copy_around_parentheses"], ["cancel"]], + ["ya)", ["copy_around_parentheses"], ["cancel"]], + ["ya[", ["copy_around_square_brackets"], ["cancel"]], + ["ya]", ["copy_around_square_brackets"], ["cancel"]], + ["ya{", ["copy_around_braces"], ["cancel"]], + ["ya}", ["copy_around_braces"], ["cancel"]], ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -169,7 +208,20 @@ ["e", "select_word_right_end_vim"], ["iw", "select_inside_word"], + ["i(", "select_inside_parentheses"], + ["i)", "select_inside_parentheses"], + ["i[", "select_inside_square_brackets"], + ["i]", "select_inside_square_brackets"], + ["i{", "select_inside_braces"], + ["i}", "select_inside_braces"], + ["aw", "select_around_word"], + ["a(", "select_around_parentheses"], + ["a)", "select_around_parentheses"], + ["a[", "select_around_square_brackets"], + ["a]", "select_around_square_brackets"], + ["a{", "select_around_braces"], + ["a}", "select_around_braces"], ["^", "smart_move_begin"], ["$", "select_end"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e65dd6d5..74a99168 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -41,7 +41,7 @@ const double_click_time_ms = 350; const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental) const syntax_full_reparse_error_threshold = 3; // number of tree-sitter errors that trigger a full reparse -const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; +pub const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000; pub const max_match_lines = 15; diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index f7067e50..043ed35a 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -8,6 +8,7 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const CurSel = @import("../editor.zig").CurSel; const Editor = @import("../editor.zig").Editor; +const bracket_search_radius = @import("../editor.zig").bracket_search_radius; var commands: Commands = undefined; @@ -165,6 +166,60 @@ const cmds_ = struct { } pub const select_around_word_meta: Meta = .{ .description = "Select around word" }; + pub fn select_inside_parentheses(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + } + pub const select_inside_parentheses_meta: Meta = .{ .description = "Select inside ()" }; + + pub fn select_around_parentheses(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + } + pub const select_around_parentheses_meta: Meta = .{ .description = "Select around ()" }; + + pub fn select_inside_square_brackets(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + } + pub const select_inside_square_brackets_meta: Meta = .{ .description = "Select inside []" }; + + pub fn select_around_square_brackets(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + } + pub const select_around_square_brackets_meta: Meta = .{ .description = "Select around []" }; + + pub fn select_inside_braces(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + } + pub const select_inside_braces_meta: Meta = .{ .description = "Select inside {}" }; + + pub fn select_around_braces(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + } + pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" }; + pub fn cut_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -185,6 +240,66 @@ const cmds_ = struct { } pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" }; + pub fn cut_inside_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_parentheses_meta: Meta = .{ .description = "Cut inside ()" }; + + pub fn cut_around_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_parentheses_meta: Meta = .{ .description = "Cut around ()" }; + + pub fn cut_inside_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_square_brackets_meta: Meta = .{ .description = "Cut inside []" }; + + pub fn cut_around_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_square_brackets_meta: Meta = .{ .description = "Cut around []" }; + + pub fn cut_inside_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_braces_meta: Meta = .{ .description = "Cut inside {}" }; + + pub fn cut_around_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" }; + pub fn copy_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -204,6 +319,66 @@ const cmds_ = struct { try ed.copy_internal_vim(ctx); } pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" }; + + pub fn copy_inside_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_parentheses_meta: Meta = .{ .description = "Copy inside ()" }; + + pub fn copy_around_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_parentheses_meta: Meta = .{ .description = "Copy around ()" }; + + pub fn copy_inside_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_square_brackets_meta: Meta = .{ .description = "Copy inside []" }; + + pub fn copy_around_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_square_brackets_meta: Meta = .{ .description = "Copy around []" }; + + pub fn copy_inside_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_braces_meta: Meta = .{ .description = "Copy inside {}" }; + + pub fn copy_around_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" }; }; fn is_tab_or_space(c: []const u8) bool { @@ -267,3 +442,102 @@ fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Me sel.end = next; cursel.*.cursor = next; } + +fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside); +} + +fn select_around_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .around); +} + +fn select_inside_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .inside); +} + +fn select_around_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .around); +} + +fn select_inside_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .inside); +} + +fn select_around_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .around); +} + +fn select_bracket_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, opening_char: []const u8, closing_char: []const u8, scope: enum { inside, around }) !void { + const current = cursel.cursor; + var prev = cursel.cursor; + var next = cursel.cursor; + + const bracket_egc, _, _ = root.egc_at(current.row, current.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, bracket_egc, opening_char)) { + const closing_row, const closing_col = try Editor.match_bracket(root, current, metrics); + + prev = current; + next.row = closing_row; + next.col = closing_col; + } else if (std.mem.eql(u8, bracket_egc, closing_char)) { + const opening_row, const opening_col = try Editor.match_bracket(root, current, metrics); + + prev.row = opening_row; + prev.col = opening_col; + next = current; + } else { + const opening_pos, const closing_pos = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch try find_bracket_pair(root, cursel, metrics, .right, opening_char); + + prev.row = opening_pos[0]; + prev.col = opening_pos[1]; + next.row = closing_pos[0]; + next.col = closing_pos[1]; + } + + prev.move_right(root, metrics) catch {}; + + if (scope == .around) { + prev.move_left(root, metrics) catch {}; + next.move_right(root, metrics) catch {}; + } + + const sel = cursel.enable_selection(root, metrics); + sel.begin = prev; + sel.end = next; + cursel.*.cursor = next; +} + +fn find_bracket_pair(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, direction: enum { left, right }, char: []const u8) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } { + const start = cursel.cursor; + var moving_cursor = cursel.cursor; + + var i: usize = 0; + while (i < bracket_search_radius) : (i += 1) { + switch (direction) { + .left => try moving_cursor.move_left(root, metrics), + .right => try moving_cursor.move_right(root, metrics), + } + + const curr_egc, _, _ = root.egc_at(moving_cursor.row, moving_cursor.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, char, curr_egc)) { + const closing_row, const closing_col = try Editor.match_bracket(root, moving_cursor, metrics); + + switch (direction) { + .left => if (closing_row > start.row or (closing_row == start.row and closing_col > start.col)) { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + } else { + continue; + }, + .right => { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + }, + } + } + } + + return error.Stop; +} From d53d155c6d94bd6caec6655c88e0744f4092a95e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 14:01:22 +0200 Subject: [PATCH 4/6] feat: add V language support closes #509 --- build.zig.zon | 4 ++-- src/file_type_lsp.zig | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 48e0c7d0..71a537a6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .dependencies = .{ .syntax = .{ - .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#56929f0c523b59153e17919be2cd09d8bef32cd0", - .hash = "flow_syntax-0.7.2-X8jOoeFTAQBeP2Tn08Tw1jsMdifLEDBgPLqPqNelAupy", + .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#7b1fd3a97f00aba3a95cc65b95f34162347ed1ea", + .hash = "flow_syntax-0.7.2-X8jOoQhWAQBPt1rBRmttAGI0Z2QC-hCSZuoBZoZgr6Vv", }, .flags = .{ .url = "git+https://github.com/neurocyte/flags?ref=main#984b27948da3e4e40a253f76c85b51ec1a9ada11", diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index 9064fc3c..a359e8a7 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -232,6 +232,11 @@ pub const typst = .{ pub const uxntal = .{}; +pub const v = .{ + .language_server = .{"v-analyzer"}, + .formatter = .{ "v", "fmt", "-" }, +}; + pub const vim = .{}; pub const xml = .{ From 310221bb266ae6a0252541daf6ebc2dacedbf5b7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 20:58:00 +0200 Subject: [PATCH 5/6] feat: support adding entirely new themes via the config --- src/main.zig | 6 +- src/soft_root.zig | 5 ++ src/tui/Widget.zig | 110 ++++++++++++++++++++++++- src/tui/mode/overlay/theme_palette.zig | 3 +- src/tui/tui.zig | 73 ++-------------- 5 files changed, 126 insertions(+), 71 deletions(-) diff --git a/src/main.zig b/src/main.zig index b191faf5..a8de7100 100644 --- a/src/main.zig +++ b/src/main.zig @@ -895,15 +895,15 @@ pub fn write_theme(theme_name: []const u8, content: []const u8) !void { pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); defer dir.close(); - var result = std.ArrayList([]const u8).init(allocator); + var result: std.ArrayList([]const u8) = .empty; var iter = dir.iterateAssumeFirstIteration(); while (try iter.next()) |entry| { switch (entry.kind) { - .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))), else => continue, } } - return result.toOwnedSlice(); + return result.toOwnedSlice(allocator); } pub fn get_config_dir() ConfigDirError![]const u8 { diff --git a/src/soft_root.zig b/src/soft_root.zig index a792e730..d076e942 100644 --- a/src/soft_root.zig +++ b/src/soft_root.zig @@ -29,6 +29,7 @@ pub const root = struct { pub const read_theme = if (@hasDecl(hard_root, "read_theme")) hard_root.read_theme else dummy.read_theme; pub const write_theme = if (@hasDecl(hard_root, "write_theme")) hard_root.write_theme else dummy.write_theme; + pub const list_themes = if (@hasDecl(hard_root, "list_themes")) hard_root.list_themes else dummy.list_themes; pub const get_theme_file_name = if (@hasDecl(hard_root, "get_theme_file_name")) hard_root.get_theme_file_name else dummy.get_theme_file_name; pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit; @@ -109,6 +110,10 @@ const dummy = struct { pub fn write_theme(_: []const u8, _: []const u8) !void { @panic("dummy write_theme call"); } + pub fn list_themes(_: std.mem.Allocator) ![]const []const u8 { + @panic("dummy list_themes call"); + } + pub fn get_theme_file_name(_: []const u8) ![]const u8 { @panic("dummy get_theme_file_name call"); } diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 6db55a94..9a473ecf 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); +const root = @import("soft_root").root; const Plane = @import("renderer").Plane; const EventHandler = @import("EventHandler"); @@ -9,7 +10,6 @@ const tui = @import("tui.zig"); pub const Box = @import("Box.zig"); pub const Pos = struct { y: i32 = 0, x: i32 = 0 }; pub const Theme = @import("theme"); -pub const themes = @import("themes").themes; pub const scopes = @import("themes").scopes; pub const Type = @import("config").WidgetType; pub const StyleTag = @import("config").WidgetStyle; @@ -42,6 +42,114 @@ pub const Layout = union(enum) { } }; +pub const ThemeInfo = struct { + name: []const u8, + storage: ?std.json.Parsed(Theme) = null, + + pub fn get(self: *@This(), allocator: std.mem.Allocator) ?Theme { + if (load_theme_file(allocator, self.name) catch null) |parsed_theme| { + self.storage = parsed_theme; + return self.storage.?.value; + } + + for (static_themes) |theme_| { + if (std.mem.eql(u8, theme_.name, self.name)) + return theme_; + } + return null; + } + + fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { + return load_theme_file_internal(allocator, theme_name) catch |e| { + std.log.err("Error loading theme '{s}' from file: {t}", .{ theme_name, e }); + return e; + }; + } + fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { + const json_str = root.read_theme(allocator, theme_name) orelse return null; + defer allocator.free(json_str); + return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always }); + } +}; + +var themes_: ?std.StringHashMap(*ThemeInfo) = null; +var theme_names_: ?[]const []const u8 = null; +const static_themes = @import("themes").themes; + +fn get_themes(allocator: std.mem.Allocator) *std.StringHashMap(*ThemeInfo) { + if (themes_) |*themes__| return themes__; + + const theme_files = root.list_themes(allocator) catch @panic("OOM get_themes"); + var themes: std.StringHashMap(*ThemeInfo) = .init(allocator); + defer allocator.free(theme_files); + for (theme_files) |file| { + const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes"); + theme_info.* = .{ + .name = file, + }; + themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes"); + } + + for (static_themes) |theme_| if (!themes.contains(theme_.name)) { + const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes"); + theme_info.* = .{ + .name = theme_.name, + }; + themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes"); + }; + themes_ = themes; + return &themes_.?; +} + +fn get_theme_names() []const []const u8 { + if (theme_names_) |names_| return names_; + const themes = themes_ orelse return &.{}; + var i = get_themes(themes.allocator).iterator(); + var names: std.ArrayList([]const u8) = .empty; + while (i.next()) |theme_| names.append(themes.allocator, theme_.value_ptr.*.name) catch @panic("OOM get_theme_names"); + std.mem.sort([]const u8, names.items, {}, struct { + fn cmp(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; + } + }.cmp); + theme_names_ = names.toOwnedSlice(themes.allocator) catch @panic("OOM get_theme_names"); + return theme_names_.?; +} + +pub fn get_theme_by_name(allocator: std.mem.Allocator, name_: []const u8) ?Theme { + const themes = get_themes(allocator); + const theme = themes.get(name_) orelse return null; + return theme.get(allocator); +} + +pub fn get_next_theme_by_name(name_: []const u8) []const u8 { + const theme_names = get_theme_names(); + var next = false; + for (theme_names) |theme_name| { + if (next) + return theme_name; + if (std.mem.eql(u8, theme_name, name_)) + next = true; + } + return theme_names[0]; +} + +pub fn get_prev_theme_by_name(name_: []const u8) []const u8 { + const theme_names = get_theme_names(); + const last = theme_names[theme_names.len - 1]; + var prev: ?[]const u8 = null; + for (theme_names) |theme_name| { + if (std.mem.eql(u8, theme_name, name_)) + return prev orelse last; + prev = theme_name; + } + return last; +} + +pub fn list_themes() []const []const u8 { + return get_theme_names(); +} + pub const VTable = struct { deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool, diff --git a/src/tui/mode/overlay/theme_palette.zig b/src/tui/mode/overlay/theme_palette.zig index a58e9844..1f9396df 100644 --- a/src/tui/mode/overlay/theme_palette.zig +++ b/src/tui/mode/overlay/theme_palette.zig @@ -33,7 +33,8 @@ pub fn load_entries(palette: *Type) !usize { var longest_hint: usize = 0; var idx: usize = 0; try set_previous_theme(palette, tui.theme().name); - for (Widget.themes) |theme| { + for (Widget.list_themes()) |theme_name_| { + const theme = Widget.get_theme_by_name(palette.allocator, theme_name_) orelse continue; idx += 1; (try palette.entries.addOne(palette.allocator)).* = .{ .label = theme.description, diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 3d9a1ad6..6b04a30c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -62,9 +62,7 @@ logger: log.Logger, drag_source: ?Widget = null, drag_button: input.MouseType = 0, dark_theme: Widget.Theme, -dark_parsed_theme: ?std.json.Parsed(Widget.Theme), light_theme: Widget.Theme, -light_parsed_theme: ?std.json.Parsed(Widget.Theme), idle_frame_count: usize = 0, unrendered_input_events_count: usize = 0, init_timer: ?tp.timeout, @@ -160,9 +158,9 @@ fn init(allocator: Allocator) InitError!*Self { if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) renderer.jit_debugger_enabled = true; - const dark_theme, const dark_parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; + const dark_theme = Widget.get_theme_by_name(allocator, conf.theme) orelse Widget.get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; conf.theme = dark_theme.name; - const light_theme, const light_parsed_theme = get_theme_by_name(allocator, conf.light_theme) orelse get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme; + const light_theme = Widget.get_theme_by_name(allocator, conf.light_theme) orelse Widget.get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme; conf.light_theme = light_theme.name; if (build_options.gui) conf.enable_terminal_cursor = false; @@ -203,8 +201,6 @@ fn init(allocator: Allocator) InitError!*Self { .query_cache_ = try syntax.QueryCache.create(allocator, .{}), .dark_theme = dark_theme, .light_theme = light_theme, - .dark_parsed_theme = dark_parsed_theme, - .light_parsed_theme = light_parsed_theme, }; instance_ = self; defer instance_ = null; @@ -987,21 +983,13 @@ fn refresh_input_mode(self: *Self) command.Result { } fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !void { - const theme_, const parsed_theme = get_theme_by_name(self.allocator, name) orelse { + const theme_ = Widget.get_theme_by_name(self.allocator, name) orelse { self.logger.print("theme not found: {s}", .{name}); return; }; switch (self.color_scheme) { - .dark => { - if (self.dark_parsed_theme) |p| p.deinit(); - self.dark_parsed_theme = parsed_theme; - self.dark_theme = theme_; - }, - .light => { - if (self.light_parsed_theme) |p| p.deinit(); - self.light_parsed_theme = parsed_theme; - self.light_theme = theme_; - }, + .dark => self.dark_theme = theme_, + .light => self.light_theme = theme_, } self.set_terminal_style(&theme_); self.logger.print("theme: {s}", .{theme_.description}); @@ -1141,13 +1129,13 @@ const cmds = struct { pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub fn theme_next(self: *Self, _: Ctx) Result { - const name = get_next_theme_by_name(self.current_theme().name); + const name = Widget.get_next_theme_by_name(self.current_theme().name); return self.set_theme_by_name(name, .store); } pub const theme_next_meta: Meta = .{ .description = "Next color theme" }; pub fn theme_prev(self: *Self, _: Ctx) Result { - const name = get_prev_theme_by_name(self.current_theme().name); + const name = Widget.get_prev_theme_by_name(self.current_theme().name); return self.set_theme_by_name(name, .store); } pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" }; @@ -2020,40 +2008,6 @@ pub fn theme() *const Widget.Theme { return current().current_theme(); } -pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Widget.Theme, ?std.json.Parsed(Widget.Theme) } { - if (load_theme_file(allocator, name) catch null) |parsed_theme| { - std.log.info("loaded theme from file: {s}", .{name}); - return .{ parsed_theme.value, parsed_theme }; - } - - for (Widget.themes) |theme_| { - if (std.mem.eql(u8, theme_.name, name)) - return .{ theme_, null }; - } - return null; -} - -fn get_next_theme_by_name(name: []const u8) []const u8 { - var next = false; - for (Widget.themes) |theme_| { - if (next) - return theme_.name; - if (std.mem.eql(u8, theme_.name, name)) - next = true; - } - return Widget.themes[0].name; -} - -fn get_prev_theme_by_name(name: []const u8) []const u8 { - var prev: ?Widget.Theme = null; - for (Widget.themes) |theme_| { - if (std.mem.eql(u8, theme_.name, name)) - return (prev orelse Widget.themes[Widget.themes.len - 1]).name; - prev = theme_; - } - return Widget.themes[Widget.themes.len - 1].name; -} - pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token { return if (find_scope_fallback(scope)) |tm_scope| scope_to_theme_token(theme_, tm_scope) orelse @@ -2442,19 +2396,6 @@ fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const return try root.get_theme_file_name(theme_name); } -fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) { - return load_theme_file_internal(allocator, theme_name) catch |e| { - std.log.err("loaded theme from file failed: {}", .{e}); - return e; - }; -} -fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) { - _ = std.json.Scanner; - const json_str = root.read_theme(allocator, theme_name) orelse return null; - defer allocator.free(json_str); - return try std.json.parseFromSlice(Widget.Theme, allocator, json_str, .{ .allocate = .alloc_always }); -} - pub const WidgetType = @import("config").WidgetType; pub const ConfigWidgetStyle = @import("config").WidgetStyle; pub const WidgetStyle = @import("WidgetStyle.zig"); From cf7fc6af5455646a84c3b571cd6c8de85ccf4af1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 20:58:54 +0200 Subject: [PATCH 6/6] fix: allow loading custom theme files up to 512Kb in size closes #544 --- src/main.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index a8de7100..054340ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -882,7 +882,10 @@ pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const const file_name = get_theme_file_name(theme_name) catch return null; var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; defer file.close(); - return file.readToEndAlloc(allocator, 64 * 1024) catch null; + return file.readToEndAlloc(allocator, 512 * 1024) catch |e| { + std.log.err("Error reading theme file: {t}", .{e}); + return null; + }; } pub fn write_theme(theme_name: []const u8, content: []const u8) !void {