From 223260887db9f1e26123e6e04fa91095184fd15f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 2 Nov 2025 21:32:52 +0100 Subject: [PATCH] feat: add underlining keybinds ctrl+_, ctrl+= and ctrl+plus ctrl+_ => select char to underline with ctrl+= => underline with '=' ctrl+plus => underline with '=' preserving spaces closes #350 --- src/keybind/builtin/flow.json | 3 ++ src/tui/editor.zig | 54 +++++++++++++++++++++++++++++++++ src/tui/mode/mini/underline.zig | 17 +++++++++++ src/tui/tui.zig | 5 +++ 4 files changed, 79 insertions(+) create mode 100644 src/tui/mode/mini/underline.zig diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 5ff2896..f527c3b 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -136,6 +136,9 @@ ["alt+u", "to_upper"], ["alt+l", "to_lower"], ["alt+c", "switch_case"], + ["ctrl+_", "underline"], + ["ctrl+=", "underline_with_char", "=", "solid"], + ["ctrl+plus", "underline_with_char", "="], ["alt+b", "move_word_left"], ["alt+f", "move_word_right"], ["alt+s", "filter", "sort"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index dc89c6a..3cf398f 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3632,6 +3632,60 @@ pub const Editor = struct { } pub const dupe_down_meta: Meta = .{ .description = "Duplicate line or selection down/forwards", .arguments = &.{.integer} }; + fn underline_cursel_with_char(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator, ctx: command.Context) error{Stop}!Buffer.Root { + var symbol: []const u8 = undefined; + var mode: enum { solid, space } = .space; + if (!(ctx.args.match(.{tp.extract(&symbol)}) catch return error.Stop) and + !(ctx.args.match(.{ tp.extract(&symbol), tp.extract(&mode) }) catch return error.Stop)) + return error.Stop; + var root = root_; + const sel: Selection = if (cursel.selection) |sel_| sel_ else Selection.line_from_cursor(cursel.cursor, root, self.metrics); + var sfa = std.heap.stackFallback(4096, self.allocator); + const sfa_allocator = sfa.get(); + const text = copy_selection(root, sel, sfa_allocator, self.metrics) catch return error.Stop; + defer sfa_allocator.free(text); + + var underlined: std.Io.Writer.Allocating = .init(sfa_allocator); + defer underlined.deinit(); + + var text_egcs = text; + while (text_egcs.len > 0) { + var colcount: c_int = 1; + const egc_len = self.metrics.egc_length(self.metrics, text_egcs, &colcount, 0); + switch (text_egcs[0]) { + '\t' => underlined.writer.writeAll("\t") catch {}, + '\n' => underlined.writer.writeAll(text_egcs[0..egc_len]) catch {}, + ' ' => underlined.writer.writeAll(switch (mode) { + .space => text_egcs[0..egc_len], + .solid => symbol, + }) catch {}, + else => while (colcount > 0) : (colcount -= 1) underlined.writer.writeAll(symbol) catch {}, + } + text_egcs = text_egcs[egc_len..]; + } + + cursel.cursor = sel.end; + if (cursel.selection) |_| { + cursel.disable_selection(root, self.metrics); + } else { + var test_eof = sel.end; + test_eof.move_right(root, self.metrics) catch { // test for EOF + root = self.insert(root, cursel, "\n", allocator) catch return error.Stop; + }; + } + root = self.insert(root, cursel, underlined.written(), allocator) catch return error.Stop; + cursel.selection = .{ .begin = sel.end, .end = cursel.cursor }; + return root; + } + + pub fn underline_with_char(self: *Self, ctx: Context) Result { + const b = try self.buf_for_update(); + const root = try self.with_cursels_mut_once_arg(b.root, underline_cursel_with_char, b.allocator, ctx); + try self.update_buf(root); + self.clamp(); + } + pub const underline_with_char_meta: Meta = .{ .arguments = &.{.string} }; + fn toggle_cursel_prefix(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; diff --git a/src/tui/mode/mini/underline.zig b/src/tui/mode/mini/underline.zig new file mode 100644 index 0000000..c55cd23 --- /dev/null +++ b/src/tui/mode/mini/underline.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const command = @import("command"); + +const tui = @import("../../tui.zig"); + +pub const Type = @import("get_char.zig").Create(@This()); +pub const create = Type.create; + +pub fn name(_: *Type) []const u8 { + return "underline"; +} + +pub fn process_egc(_: *Type, egc: []const u8) command.Result { + try command.executeName("underline_with_char", command.fmt(.{ egc, "solid" })); + try command.executeName("exit_mini_mode", .{}); +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 45063d6..ac38613 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1189,6 +1189,11 @@ const cmds = struct { } pub const replace_meta: Meta = .{ .description = "Replace with character" }; + pub fn underline(self: *Self, ctx: Ctx) Result { + return enter_mini_mode(self, @import("mode/mini/underline.zig"), ctx); + } + pub const underline_meta: Meta = .{ .description = "Underline with character" }; + pub fn open_file(self: *Self, ctx: Ctx) Result { if (get_active_selection(self.allocator)) |text| { defer self.allocator.free(text);