diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index c619abc..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"], @@ -437,7 +440,7 @@ ["backspace", "mini_mode_delete_backwards"] ] }, - "mini/move_to_char": { + "mini/get_char": { "press": [ ["ctrl+g", "mini_mode_cancel"], ["ctrl+c", "mini_mode_cancel"], 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/get_char.zig b/src/tui/mode/mini/get_char.zig new file mode 100644 index 0000000..379acd4 --- /dev/null +++ b/src/tui/mode/mini/get_char.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const tp = @import("thespian"); + +const input = @import("input"); +const key = @import("renderer").input.key; +const mod = @import("renderer").input.modifier; +const event_type = @import("renderer").input.event_type; +const keybind = @import("keybind"); +const command = @import("command"); +const EventHandler = @import("EventHandler"); + +const tui = @import("../../tui.zig"); + +const Allocator = @import("std").mem.Allocator; +const fmt = @import("std").fmt; + +pub fn Create(options: type) type { + return struct { + const Self = @This(); + + const Commands = command.Collection(cmds); + + const ValueType = if (@hasDecl(options, "ValueType")) options.ValueType else void; + + allocator: Allocator, + input: ?ValueType = null, + value: ValueType, + ctx: command.Context, + commands: Commands = undefined, + + pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tui.MiniMode } { + const self = try allocator.create(Self); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .ctx = .{ .args = try ctx.args.clone(allocator) }, + .value = if (@hasDecl(options, "start")) options.start(self) else {}, + }; + try self.commands.init(self); + var mode = try keybind.mode("mini/get_char", allocator, .{ + .insert_command = "mini_mode_insert_bytes", + }); + mode.event_handler = EventHandler.to_owned(self); + return .{ mode, .{ .name = options.name(self) } }; + } + + pub fn deinit(self: *Self) void { + if (@hasDecl(options, "deinit")) + options.deinit(self); + self.allocator.free(self.ctx.args.buf); + self.commands.deinit(); + self.allocator.destroy(self); + } + + pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { + return false; + } + + const cmds = struct { + pub const Target = Self; + const Ctx = command.Context; + const Meta = command.Metadata; + const Result = command.Result; + + pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { + var code_point: u32 = 0; + if (!try ctx.args.match(.{tp.extract(&code_point)})) + return error.InvalidMoveToCharInsertCodePointArgument; + var buf: [6]u8 = undefined; + const bytes = input.ucs32_to_utf8(&[_]u32{code_point}, &buf) catch return error.InvalidMoveToCharCodePoint; + return options.process_egc(self, buf[0..bytes]); + } + pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; + + pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { + var bytes: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&bytes)})) + return error.InvalidMoveToCharInsertBytesArgument; + const egc = tui.egc_last(bytes); + var buf: [6]u8 = undefined; + @memcpy(buf[0..egc.len], egc); + return options.process_egc(self, buf[0..egc.len]); + } + pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; + + pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { + command.executeName("exit_mini_mode", .{}) catch {}; + } + pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; + }; + }; +} diff --git a/src/tui/mode/mini/move_to_char.zig b/src/tui/mode/mini/move_to_char.zig index 1d76631..e9ef5f7 100644 --- a/src/tui/mode/mini/move_to_char.zig +++ b/src/tui/mode/mini/move_to_char.zig @@ -1,25 +1,17 @@ const std = @import("std"); -const tp = @import("thespian"); - -const input = @import("input"); -const keybind = @import("keybind"); +const cbor = @import("cbor"); const command = @import("command"); -const EventHandler = @import("EventHandler"); const tui = @import("../../tui.zig"); -const Allocator = @import("std").mem.Allocator; +pub const Type = @import("get_char.zig").Create(@This()); +pub const create = Type.create; -const Self = @This(); - -const Commands = command.Collection(cmds); - -allocator: Allocator, -key: [6]u8 = undefined, -direction: Direction, -operation_command: []const u8, -operation: Operation, -commands: Commands = undefined, +pub const ValueType = struct { + direction: Direction, + operation_command: []const u8, + operation: Operation, +}; const Direction = enum { left, @@ -32,9 +24,9 @@ const Operation = enum { extend, }; -pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tui.MiniMode } { - var operation_command: []const u8 = undefined; - _ = ctx.args.match(.{tp.extract(&operation_command)}) catch return error.InvalidMoveToCharArgument; +pub fn start(self: *Type) ValueType { + var operation_command: []const u8 = "move_to_char_left"; + _ = self.ctx.args.match(.{cbor.extract(&operation_command)}) catch {}; const direction: Direction = if (std.mem.indexOf(u8, operation_command, "_left")) |_| .left else .right; var operation: Operation = undefined; @@ -50,80 +42,31 @@ pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tu operation = .move; } - const self = try allocator.create(Self); - errdefer allocator.destroy(self); - self.* = .{ - .allocator = allocator, + return .{ .direction = direction, - .operation_command = try allocator.dupe(u8, operation_command), + .operation_command = operation_command, .operation = operation, }; - try self.commands.init(self); - var mode = try keybind.mode("mini/move_to_char", allocator, .{ - .insert_command = "mini_mode_insert_bytes", - }); - mode.event_handler = EventHandler.to_owned(self); - return .{ mode, .{ .name = self.name() } }; } -pub fn deinit(self: *Self) void { - self.commands.deinit(); - self.allocator.free(self.operation_command); - self.allocator.destroy(self); -} - -fn name(self: *Self) []const u8 { - return switch (self.operation) { - .move => switch (self.direction) { +pub fn name(self: *Type) []const u8 { + return switch (self.value.operation) { + .move => switch (self.value.direction) { .left => "↶ move", .right => "↷ move", }, - .select => switch (self.direction) { + .select => switch (self.value.direction) { .left => "󰒅 ↶ select", .right => "󰒅 ↷ select", }, - .extend => switch (self.direction) { + .extend => switch (self.value.direction) { .left => "󰒅 ↶ extend", .right => "󰒅 ↷ extend", }, }; } -pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { - return false; -} - -fn execute_operation(self: *Self, ctx: command.Context) command.Result { - try command.executeName(self.operation_command, ctx); +pub fn process_egc(self: *Type, egc: []const u8) command.Result { + try command.executeName(self.value.operation_command, command.fmt(.{egc})); try command.executeName("exit_mini_mode", .{}); } - -const cmds = struct { - pub const Target = Self; - const Ctx = command.Context; - const Meta = command.Metadata; - const Result = command.Result; - - pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { - var code_point: u32 = 0; - if (!try ctx.args.match(.{tp.extract(&code_point)})) - return error.InvalidMoveToCharInsertCodePointArgument; - var buf: [6]u8 = undefined; - const bytes = input.ucs32_to_utf8(&[_]u32{code_point}, &buf) catch return error.InvalidMoveToCharCodePoint; - return self.execute_operation(command.fmt(.{buf[0..bytes]})); - } - pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; - - pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { - var bytes: []const u8 = undefined; - if (!try ctx.args.match(.{tp.extract(&bytes)})) - return error.InvalidMoveToCharInsertBytesArgument; - return self.execute_operation(ctx); - } - pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; - - pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { - command.executeName("exit_mini_mode", .{}) catch {}; - } - pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; -}; 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);