diff --git a/src/buffer/unicode.zig b/src/buffer/unicode.zig index eca8f5e8..fc78be54 100644 --- a/src/buffer/unicode.zig +++ b/src/buffer/unicode.zig @@ -201,6 +201,24 @@ pub fn switch_case(allocator: std.mem.Allocator, text: []const u8) TransformErro to_upper(allocator, text); } +pub fn toggle_case(allocator: std.mem.Allocator, text: []const u8) TransformError![]u8 { + var result: std.Io.Writer.Allocating = .init(allocator); + defer result.deinit(); + const writer = &result.writer; + const view: Utf8View = .initUnchecked(text); + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + const cp_ = if (uucode.get(.changes_when_lowercased, cp)) + uucode.get(.simple_lowercase_mapping, cp) orelse cp + else + uucode.get(.simple_uppercase_mapping, cp) orelse cp; + var utf8_buf: [6]u8 = undefined; + const size = try utf8Encode(cp_, &utf8_buf); + try writer.writeAll(utf8_buf[0..size]); + } + return result.toOwnedSlice(); +} + pub fn is_lowercase(text: []const u8) bool { return utf8_predicate_all(.is_lowercase, text); } diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 4425d51d..db80dde4 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -150,6 +150,7 @@ ["alt+l", "to_lower"], ["alt+q", "reflow"], ["alt+c", "switch_case"], + ["ctrl+k c", "toggle_case"], ["ctrl+_", "underline"], ["ctrl+=", "underline_with_char", "=", "solid"], ["ctrl+plus", "underline_with_char", "="], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 67441531..347c13a5 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -7257,27 +7257,43 @@ pub const Editor = struct { .arguments = &.{.integer}, }; - fn to_upper_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { + fn get_selection_or_select_word(self: *Self, root: Buffer.Root, cursel: *CurSel) error{Stop}!*Selection { + if (cursel.selection) |*sel| { + return sel; + } else { + var sel = cursel.enable_selection(root, self.metrics); + try move_cursor_word_begin(root, &sel.begin, self.metrics); + try move_cursor_word_end(root, &sel.end, self.metrics); + return sel; + } + } + + fn transform_cursel( + comptime transform: fn (std.mem.Allocator, []const u8) Buffer.unicode.TransformError![]u8, + self: *Self, + root_: Buffer.Root, + cursel: *CurSel, + allocator: Allocator, + ) error{Stop}!Buffer.Root { var root = root_; const saved = cursel.*; - const sel = if (cursel.selection) |*sel| sel else ret: { - var sel = cursel.enable_selection(root, self.metrics); - move_cursor_word_begin(root, &sel.begin, self.metrics) catch return error.Stop; - move_cursor_word_end(root, &sel.end, self.metrics) catch return error.Stop; - break :ret sel; - }; + const sel = try self.get_selection_or_select_word(root, cursel); var sfa = std.heap.stackFallback(4096, self.allocator); const sfa_allocator = sfa.get(); const cut_text = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; defer sfa_allocator.free(cut_text); - const ucased = Buffer.unicode.to_upper(sfa_allocator, cut_text) catch return error.Stop; - defer sfa_allocator.free(ucased); + const transformed = transform(sfa_allocator, cut_text) catch return error.Stop; + defer sfa_allocator.free(transformed); root = try self.delete_selection(root, cursel, allocator); - root = self.insert(root, cursel, ucased, allocator) catch return error.Stop; + root = self.insert(root, cursel, transformed, allocator) catch return error.Stop; cursel.* = saved; return root; } + fn to_upper_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { + return transform_cursel(Buffer.unicode.to_upper, self, root_, cursel, allocator); + } + pub fn to_upper(self: *Self, _: Context) Result { const b = try self.buf_for_update(); const root = try self.with_cursels_mut_once(b.root, to_upper_cursel, b.allocator); @@ -7286,25 +7302,8 @@ pub const Editor = struct { } pub const to_upper_meta: Meta = .{ .description = "Convert selection or word to upper case" }; - fn to_lower_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, buffer_allocator: Allocator) error{Stop}!Buffer.Root { - var root = root_; - const saved = cursel.*; - const sel = if (cursel.selection) |*sel| sel else ret: { - var sel = cursel.enable_selection(root, self.metrics); - move_cursor_word_begin(root, &sel.begin, self.metrics) catch return error.Stop; - move_cursor_word_end(root, &sel.end, self.metrics) catch return error.Stop; - break :ret sel; - }; - var sfa = std.heap.stackFallback(4096, self.allocator); - const sfa_allocator = sfa.get(); - const cut_text = copy_selection(root, sel.*, sfa_allocator, self.metrics) catch return error.Stop; - defer sfa_allocator.free(cut_text); - const ucased = Buffer.unicode.to_lower(sfa_allocator, cut_text) catch return error.Stop; - defer sfa_allocator.free(ucased); - root = try self.delete_selection(root, cursel, buffer_allocator); - root = self.insert(root, cursel, ucased, buffer_allocator) catch return error.Stop; - cursel.* = saved; - return root; + fn to_lower_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { + return transform_cursel(Buffer.unicode.to_lower, self, root_, cursel, allocator); } pub fn to_lower(self: *Self, _: Context) Result { @@ -7346,6 +7345,18 @@ pub const Editor = struct { } pub const switch_case_meta: Meta = .{ .description = "Switch the case of selection or character at cursor" }; + fn toggle_case_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { + return transform_cursel(Buffer.unicode.toggle_case, self, root_, cursel, allocator); + } + + pub fn toggle_case(self: *Self, _: Context) Result { + const b = try self.buf_for_update(); + const root = try self.with_cursels_mut_once(b.root, toggle_case_cursel, b.allocator); + try self.update_buf(root); + self.clamp(); + } + pub const toggle_case_meta: Meta = .{ .description = "Toggle the case of each character in selection or character at cursor" }; + pub fn forced_mark_clean(self: *Self, _: Context) Result { if (self.buffer) |b| { b.mark_clean();