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/config.zig b/src/config.zig index d10d4c67..f3845cdb 100644 --- a/src/config.zig +++ b/src/config.zig @@ -87,6 +87,8 @@ hint_window_style: WidgetStyle = .thick_boxed, info_box_style: WidgetStyle = .bar_left_spacious, info_box_width_limit: usize = 80, +palette_placement: PalettePlacement = .top_center, + centered_view: bool = false, centered_view_width: usize = 145, centered_view_min_screen_width: usize = 145, @@ -256,3 +258,10 @@ pub const TerminalOnExit = enum { close, hold, }; + +pub const PalettePlacement = enum { + top_center, + top_left, + top_right, + center, +}; diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 6c5ab7de..30d5f4e5 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -157,6 +157,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", "="], @@ -187,8 +188,8 @@ ["alt+[", "select_prev_sibling", true], ["alt+]", "select_next_sibling", true], ["alt+a", "select_all_siblings"], - ["alt+shift+home", "move_scroll_left"], - ["alt+shift+end", "move_scroll_right"], + ["alt+shift+home", "scroll_left"], + ["alt+shift+end", "scroll_right"], ["alt+shift+up", "add_cursor_up"], ["alt+shift+down", "add_cursor_down"], ["alt+shift+f12", "goto_type_definition"], diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 07d82099..8b3d9106 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -88,6 +88,8 @@ ["di]", "cut_inside_square_brackets"], ["di{", "cut_inside_braces"], ["di}", "cut_inside_braces"], + ["di'", "cut_inside_single_quotes"], + ["di\"", "cut_inside_double_quotes"], ["daw", "cut_around_word"], ["da(", "cut_around_parentheses"], @@ -96,6 +98,8 @@ ["da]", "cut_around_square_brackets"], ["da{", "cut_around_braces"], ["da}", "cut_around_braces"], + ["da'", "cut_around_single_quotes"], + ["da\"", "cut_around_double_quotes"], ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], @@ -110,6 +114,8 @@ ["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], ["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]], ["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["ci'", ["enter_mode", "insert"], ["cut_inside_single_quotes"]], + ["ci\"", ["enter_mode", "insert"], ["cut_inside_double_quotes"]], ["caw", ["enter_mode", "insert"], ["cut_around_word"]], ["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]], @@ -118,6 +124,8 @@ ["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]], ["ca{", ["enter_mode", "insert"], ["cut_around_braces"]], ["ca}", ["enter_mode", "insert"], ["cut_around_braces"]], + ["ca'", ["enter_mode", "insert"], ["cut_around_single_quotes"]], + ["ca\"", ["enter_mode", "insert"], ["cut_around_double_quotes"]], ["yy", ["copy_line_internal_vim"], ["cancel"]], @@ -128,6 +136,8 @@ ["yi]", ["copy_inside_square_brackets"], ["cancel"]], ["yi{", ["copy_inside_braces"], ["cancel"]], ["yi}", ["copy_inside_braces"], ["cancel"]], + ["yi'", ["copy_inside_single_quotes"], ["cancel"]], + ["yi\"", ["copy_inside_double_quotes"], ["cancel"]], ["yaw", ["copy_around_word"], ["cancel"]], ["ya(", ["copy_around_parentheses"], ["cancel"]], @@ -136,6 +146,8 @@ ["ya]", ["copy_around_square_brackets"], ["cancel"]], ["ya{", ["copy_around_braces"], ["cancel"]], ["ya}", ["copy_around_braces"], ["cancel"]], + ["ya'", ["copy_around_single_quotes"], ["cancel"]], + ["ya\"", ["copy_around_double_quotes"], ["cancel"]], ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -214,6 +226,8 @@ ["i]", "select_inside_square_brackets"], ["i{", "select_inside_braces"], ["i}", "select_inside_braces"], + ["i'", "select_inside_single_quotes"], + ["i\"", "select_inside_double_quotes"], ["aw", "select_around_word"], ["a(", "select_around_parentheses"], @@ -222,6 +236,8 @@ ["a]", "select_around_square_brackets"], ["a{", "select_around_braces"], ["a}", "select_around_braces"], + ["a'", "select_around_single_quotes"], + ["a\"", "select_around_double_quotes"], ["^", "smart_move_begin"], ["$", "select_end"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 23bd9f60..d09c311a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -1673,23 +1673,15 @@ pub const Editor = struct { const style_ = style_cache_lookup(ctx.theme, ctx.cache, scope, id); const style = if (style_) |sty| sty.style else return; - if (sel.end.row < ctx.self.view.row) return; - if (sel.begin.row > ctx.self.view.row + ctx.self.view.rows) return; - if (sel.begin.row < ctx.self.view.row) sel.begin.row = ctx.self.view.row; - if (sel.end.row > ctx.self.view.row + ctx.self.view.rows) sel.end.row = ctx.self.view.row + ctx.self.view.rows; - - if (sel.end.col < ctx.self.view.col) return; - if (sel.begin.col > ctx.self.view.col + ctx.self.view.cols) return; - if (sel.begin.col < ctx.self.view.col) sel.begin.col = ctx.self.view.col; - if (sel.end.col > ctx.self.view.col + ctx.self.view.cols) sel.end.col = ctx.self.view.col + ctx.self.view.cols; + ctx.clamp_to_view(&sel.begin); + ctx.clamp_to_view(&sel.end); for (sel.begin.row..sel.end.row + 1) |row| { - const begin_col = if (row == sel.begin.row) sel.begin.col else 0; + const begin_col = if (row == sel.begin.row) sel.begin.col else ctx.self.view.col; const end_col = if (row == sel.end.row) sel.end.col else ctx.self.view.col + ctx.self.view.cols; - const y = @max(ctx.self.view.row, row) - ctx.self.view.row; - const x = @max(ctx.self.view.col, begin_col) - ctx.self.view.col; - const end_x = @max(ctx.self.view.col, end_col) - ctx.self.view.col; - if (x >= end_x) return; + const y = row - ctx.self.view.row; + const x = begin_col - ctx.self.view.col; + const end_x = end_col - ctx.self.view.col; for (x..end_x) |x_| try ctx.render_cell(y, x_, style); } @@ -1701,6 +1693,19 @@ pub const Editor = struct { cell.set_style(style); _ = ctx.self.plane.putc(&cell) catch {}; } + fn clamp_to_view(ctx: *const @This(), cursor: *Cursor) void { + const row_off: u32 = @intCast(ctx.self.view.row); + const col_off: u32 = @intCast(ctx.self.view.col); + if (cursor.row < row_off) { + cursor.row = row_off; + cursor.col = col_off; + } + if (cursor.row >= row_off + ctx.self.view.rows) { + cursor.row = row_off + ctx.self.view.rows; + cursor.col = col_off + ctx.self.view.cols; + } + cursor.col = std.math.clamp(cursor.col, col_off, col_off + ctx.self.view.cols); + } }; var ctx: Ctx = .{ .self = self, @@ -3002,17 +3007,15 @@ pub const Editor = struct { self.update_animation_step(dest); } - fn scroll_up(self: *Self) void { - const scroll_step_vertical = tui.config().scroll_step_vertical; + fn scroll_up_internal(self: *Self, count: usize) void { var dest_row = self.scroll_dest; - dest_row = if (dest_row > scroll_step_vertical) dest_row - scroll_step_vertical else 0; + dest_row -|= count; self.update_scroll_dest_abs(dest_row); } - fn scroll_down(self: *Self) void { - const scroll_step_vertical = tui.config().scroll_step_vertical; + fn scroll_down_internal(self: *Self, count: usize) void { var dest_row = self.scroll_dest; - dest_row += scroll_step_vertical; + dest_row += count; self.update_scroll_dest_abs(dest_row); } @@ -3035,7 +3038,7 @@ pub const Editor = struct { else if (tui.fast_scroll()) self.scroll_pageup() else - self.scroll_up(); + self.scroll_up_internal(tui.config().scroll_step_vertical); } pub fn mouse_scroll_down(self: *Self) void { @@ -3044,7 +3047,7 @@ pub const Editor = struct { else if (tui.fast_scroll()) self.scroll_pagedown() else - self.scroll_down(); + self.scroll_down_internal(tui.config().scroll_step_vertical); } pub fn scroll_to(self: *Self, row: usize) void { @@ -3969,6 +3972,132 @@ pub const Editor = struct { } pub const goto_bracket_meta: Meta = .{ .description = "Goto matching bracket" }; + const QuoteRole = enum { opening, closing }; + + fn row_start_cursor(root: Buffer.Root, cursor: Cursor, metrics: Buffer.Metrics) Cursor { + var c = cursor; + + while (true) { + var prev = c; + prev.move_left(root, metrics) catch break; + if (prev.row != c.row) break; + c = prev; + } + + return c; + } + + fn quote_is_escaped(root: Buffer.Root, quote_cursor: Cursor, metrics: Buffer.Metrics) bool { + var cursor = quote_cursor; + var backslashes: usize = 0; + + while (true) { + var prev = cursor; + prev.move_left(root, metrics) catch break; + if (prev.row != cursor.row) break; + + const egc, _, _ = root.egc_at(prev.row, prev.col, metrics) catch break; + if (!std.mem.eql(u8, egc, "\\")) break; + + backslashes += 1; + cursor = prev; + } + + return (backslashes % 2) == 1; + } + + fn find_unescaped_quote( + root: Buffer.Root, + start: Cursor, + metrics: Buffer.Metrics, + direction: enum { left, right }, + quote: []const u8, + ) error{Stop}!Cursor { + var cursor = start; + var i: usize = 0; + + while (i < bracket_search_radius) : (i += 1) { + switch (direction) { + .left => cursor.move_left(root, metrics) catch return error.Stop, + .right => cursor.move_right(root, metrics) catch return error.Stop, + } + + const egc, _, _ = root.egc_at(cursor.row, cursor.col, metrics) catch { + return error.Stop; + }; + + if (!std.mem.eql(u8, egc, quote)) continue; + if (quote_is_escaped(root, cursor, metrics)) continue; + + return cursor; + } + + return error.Stop; + } + + fn quote_role_on_row( + root: Buffer.Root, + quote_cursor: Cursor, + metrics: Buffer.Metrics, + quote: []const u8, + ) error{Stop}!QuoteRole { + var cursor = row_start_cursor(root, .{ .row = quote_cursor.row, .col = 0 }, metrics); + var opening = true; + + while (cursor.row == quote_cursor.row and cursor.col <= quote_cursor.col) { + const egc, _, _ = root.egc_at(cursor.row, cursor.col, metrics) catch { + return error.Stop; + }; + + if (std.mem.eql(u8, egc, quote) and !quote_is_escaped(root, cursor, metrics)) { + if (cursor.col == quote_cursor.col) { + return if (opening) .opening else .closing; + } + opening = !opening; + } + + cursor.move_right(root, metrics) catch break; + } + + return error.Stop; + } + + pub fn find_quote_pair( + root: Buffer.Root, + original_cursor: Cursor, + metrics: Buffer.Metrics, + quote: []const u8, + ) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } { + + // If the cursor is already on a quote, use it directly as the anchor. + // Otherwise find the nearest quote, preferring rightward. + const cursor_egc, _, _ = root.egc_at(original_cursor.row, original_cursor.col, metrics) catch return error.Stop; + const anchor = if (std.mem.eql(u8, cursor_egc, quote)) + original_cursor + else + find_unescaped_quote(root, original_cursor, metrics, .right, quote) catch + find_unescaped_quote(root, original_cursor, metrics, .left, quote) catch + return error.Stop; + + const role = try quote_role_on_row(root, anchor, metrics, quote); + + const other = switch (role) { + .opening => try find_unescaped_quote(root, anchor, metrics, .right, quote), + .closing => try find_unescaped_quote(root, anchor, metrics, .left, quote), + }; + + return switch (role) { + .opening => .{ + .{ anchor.row, anchor.col }, + .{ other.row, other.col }, + }, + .closing => .{ + .{ other.row, other.col }, + .{ anchor.row, anchor.col }, + }, + }; + } + pub fn move_or_select_to_char_right(self: *Self, ctx: Context) Result { const selected = if (self.get_primary().selection) |_| true else false; if (selected) try self.select_to_char_right(ctx) else try self.move_to_char_right(ctx); @@ -4411,6 +4540,20 @@ pub const Editor = struct { } pub const unindent_meta: Meta = .{ .description = "Unindent current line", .arguments = &.{.integer} }; + pub fn scroll_up(self: *Self, ctx: Context) Result { + var count: usize = 1; + _ = ctx.args.match(.{tp.extract(&count)}) catch false; + self.scroll_up_internal(count); + } + pub const scroll_up_meta: Meta = .{ .description = "Scroll up", .arguments = &.{.integer} }; + + pub fn scroll_down(self: *Self, ctx: Context) Result { + var count: usize = 1; + _ = ctx.args.match(.{tp.extract(&count)}) catch false; + self.scroll_down_internal(count); + } + pub const scroll_down_meta: Meta = .{ .description = "Scroll down", .arguments = &.{.integer} }; + pub fn move_scroll_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); self.with_cursors_const_repeat(root, move_cursor_up, ctx) catch {}; @@ -4427,15 +4570,19 @@ pub const Editor = struct { } pub const move_scroll_down_meta: Meta = .{ .description = "Move and scroll down", .arguments = &.{.integer} }; - pub fn move_scroll_left(self: *Self, _: Context) Result { - self.view.move_left(tui.config().scroll_step_horizontal) catch {}; + pub fn scroll_left(self: *Self, ctx: Context) Result { + var count: usize = 1; + _ = ctx.args.match(.{tp.extract(&count)}) catch false; + self.view.move_left(count) catch {}; } - pub const move_scroll_left_meta: Meta = .{ .description = "Scroll left" }; + pub const scroll_left_meta: Meta = .{ .description = "Scroll left", .arguments = &.{.integer} }; - pub fn move_scroll_right(self: *Self, _: Context) Result { - self.view.move_right(tui.config().scroll_step_horizontal) catch {}; + pub fn scroll_right(self: *Self, ctx: Context) Result { + var count: usize = 1; + _ = ctx.args.match(.{tp.extract(&count)}) catch false; + self.view.move_right(count) catch {}; } - pub const move_scroll_right_meta: Meta = .{ .description = "Scroll right" }; + pub const scroll_right_meta: Meta = .{ .description = "Scroll right", .arguments = &.{.integer} }; pub fn mouse_scroll_left(self: *Self) void { const scroll_step_horizontal = tui.config().scroll_step_horizontal; @@ -7110,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); @@ -7139,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 { @@ -7199,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(); diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 2721df22..0a5bd4e5 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -430,6 +430,17 @@ const cmds_ = struct { } pub const select_textobject_inner_meta: Meta = .{ .description = "select inside object helix" }; + pub fn surround_add(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const b = try ed.buf_for_update(); + const root = try ed.with_cursels_mut_once_arg(b.root, surround_cursel_add, ed.allocator, ctx); + try ed.update_buf(root); + ed.clamp(); + ed.need_render(); + } + pub const surround_add_meta: Meta = .{ .description = "surround add" }; + pub fn select_textobject_around(_: *void, ctx: Ctx) Result { var action: []const u8 = ""; @@ -481,8 +492,8 @@ const cmds_ = struct { pub fn replace_with_character_helix(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; - var root = ed.buf_root() catch return; - root = try ed.with_cursels_mut_once_arg(root, replace_cursel_with_character, ed.allocator, ctx); + const b = try ed.buf_for_update(); + const root = try ed.with_cursels_mut_once_arg(b.root, replace_cursel_with_character, ed.allocator, ctx); try ed.update_buf(root); ed.clamp(); ed.need_render(); @@ -865,6 +876,50 @@ fn replace_cursel_with_character(ed: *Editor, root: Buffer.Root, cursel: *CurSel return insert_replace_selection(ed, root, cursel, replacement, allocator) catch return error.Stop; } +fn find_open_close_pair(bracket: []const u8) struct { left: []const u8, right: []const u8 } { + for (Buffer.unicode.open_close_pairs) |pair| { + if (std.mem.eql(u8, bracket, pair[0]) or std.mem.eql(u8, bracket, pair[1])) { + return .{ .left = pair[0], .right = pair[1] }; + } + } + return .{ .left = bracket, .right = bracket }; +} + +fn surround_cursel_add(ed: *Editor, root: Buffer.Root, cursel: *CurSel, allocator: Allocator, ctx: command.Context) error{Stop}!Buffer.Root { + var encloser: []const u8 = undefined; + if (!(ctx.args.match(.{tp.extract(&encloser)}) catch return error.Stop)) + return error.Stop; + + const enclose_pair = find_open_close_pair(encloser); + var root_: Buffer.Root = root; + cursel.check_selection(root, ed.metrics); + + const sel = cursel.enable_selection(root_, ed.metrics); + var begin = sel.begin; + var end = sel.end; + if (sel.is_reversed()) { + end = sel.begin; + begin = sel.end; + } + if (begin.row == end.row and end.col == begin.col) { + end.move_right(root_, ed.metrics) catch {}; + } + _, _, root_ = root_.insert_chars(end.row, end.col, enclose_pair.right, allocator, ed.metrics) catch return error.Stop; + _, _, root_ = root_.insert_chars(begin.row, begin.col, enclose_pair.left, allocator, ed.metrics) catch return error.Stop; + + if (begin.row == end.row) { + try end.move_right(root_, ed.metrics); // for left-bracket column shift on same row + } + try end.move_right(root_, ed.metrics); // skip past right bracket + cursel.selection = Selection{ .begin = begin, .end = end }; + ed.nudge_insert(.{ .begin = begin, .end = end }, cursel, encloser.len * 2); + + if (end.right_of(begin)) { + try cursel.*.cursor.move_right(root_, ed.metrics); + } + return root_; +} + fn move_noop(_: Buffer.Root, _: *Cursor, _: Buffer.Metrics) error{Stop}!void {} fn move_cursor_word_right_end_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index c419917e..df1ad82c 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -187,10 +187,36 @@ fn prepare_resize_menu(self: *Self, _: *MenuType, _: Widget.Box) Widget.Box { } fn prepare_resize(self: *Self) Widget.Box { + const screen = tui.screen(); + const padding = tui.get_widget_style(widget_type).padding; const w = self.menu_width(); - const x = self.menu_pos_x(); const h = self.menu.menu.widgets.items.len; - return .{ .y = 0, .x = x, .w = w, .h = h }; + return switch (tui.config().palette_placement) { + .top_center => .{ + .y = 0, + .x = self.menu_pos_x(), + .w = w, + .h = h, + }, + .top_left => .{ + .y = 0, + .x = 0, + .w = w, + .h = h, + }, + .top_right => .{ + .y = 0, + .x = if (screen.w > (w - padding.right)) (screen.w - w - padding.right) else 0, + .w = w, + .h = h, + }, + .center => .{ + .y = if (screen.h > h) (screen.h - h) / 2 else 0, + .x = self.menu_pos_x(), + .w = w, + .h = h, + }, + }; } fn do_resize(self: *Self) void { diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index bd53dad8..1c419c73 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -26,7 +26,17 @@ pub const Placement = enum { top_center, top_left, top_right, + center, primary_cursor, + + fn from_config(conf: @import("config").PalettePlacement) Placement { + return switch (conf) { + .top_center => .top_center, + .top_left => .top_left, + .top_right => .top_right, + .center => .center, + }; + } }; pub const ActivateMode = enum { @@ -110,7 +120,10 @@ pub fn Create(options: type) type { .mode = try keybind.mode("overlay/palette", allocator, .{ .insert_command = "overlay_insert_bytes", }), - .placement = if (@hasDecl(options, "placement")) options.placement else .top_center, + .placement = if (@hasDecl(options, "placement")) + options.placement + else + Placement.from_config(tui.config().palette_placement), }; try self.commands.init(self); errdefer self.commands.deinit(); @@ -205,6 +218,7 @@ pub fn Create(options: type) type { .top_center => self.prepare_resize_top_center(screen, w), .top_left => self.prepare_resize_top_left(screen, w), .top_right => self.prepare_resize_top_right(screen, w, padding), + .center => self.prepare_resize_center(screen, w), .primary_cursor => self.prepare_resize_primary_cursor(screen, w, padding), }; } @@ -249,6 +263,14 @@ pub fn Create(options: type) type { return self.prepare_resize_at_y_x(screen, w, cursor.row + 1 + padding.top, cursor.col); } + fn prepare_resize_center(self: *Self, screen: Widget.Box, w: usize) Widget.Box { + const x = if (screen.w > w) (screen.w - w) / 2 else 0; + const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); + const y = if (screen.h > h) (screen.h - h) / 2 else 0; + self.view_rows = get_view_rows(screen) -| y; + return .{ .y = y, .x = x, .w = w, .h = h }; + } + fn after_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { return self.after_resize(); } diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 043ed35a..823cc812 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -220,6 +220,42 @@ const cmds_ = struct { } pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" }; + pub fn select_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + } + pub const select_inside_single_quotes_meta: Meta = .{ .description = "Select inside ''" }; + + pub fn select_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + } + pub const select_around_single_quotes_meta: Meta = .{ .description = "Select around ''" }; + + pub fn select_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + } + pub const select_inside_double_quotes_meta: Meta = .{ .description = "Select inside \"\"" }; + + pub fn select_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + } + pub const select_around_double_quotes_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; @@ -300,6 +336,46 @@ const cmds_ = struct { } pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" }; + pub fn cut_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_single_quotes_meta: Meta = .{ .description = "Cut inside ''" }; + + pub fn cut_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_single_quotes_meta: Meta = .{ .description = "Cut around ''" }; + + pub fn cut_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_double_quotes_meta: Meta = .{ .description = "Cut inside \"\"" }; + + pub fn cut_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_double_quotes_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; @@ -379,6 +455,46 @@ const cmds_ = struct { try ed.copy_internal_vim(ctx); } pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" }; + + pub fn copy_inside_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_single_quotes_meta: Meta = .{ .description = "Copy inside ''" }; + + pub fn copy_around_single_quotes(_: *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_single_quotes_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_single_quotes_meta: Meta = .{ .description = "Copy around ''" }; + + pub fn copy_inside_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_double_quotes_meta: Meta = .{ .description = "Copy inside \"\"" }; + + pub fn copy_around_double_quotes(_: *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_double_quotes_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_double_quotes_meta: Meta = .{ .description = "Copy around \"\"" }; }; fn is_tab_or_space(c: []const u8) bool { @@ -444,56 +560,94 @@ fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Me } fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside); + return try select_scope_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); + return try select_scope_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); + return try select_scope_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); + return try select_scope_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); + return try select_scope_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); + return try select_scope_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 { +fn select_inside_single_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_scope_textobject(root, cursel, metrics, "'", "'", .inside); +} + +fn select_around_single_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_scope_textobject(root, cursel, metrics, "'", "'", .around); +} + +fn select_inside_double_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_scope_textobject(root, cursel, metrics, "\"", "\"", .inside); +} + +fn select_around_double_quotes_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_scope_textobject(root, cursel, metrics, "\"", "\"", .around); +} + +fn select_scope_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); + if (std.mem.eql(u8, opening_char, closing_char)) { + const opening_pos, const closing_pos = + try Editor.find_quote_pair(root, current, metrics, opening_char); prev.row = opening_pos[0]; prev.col = opening_pos[1]; next.row = closing_pos[0]; next.col = closing_pos[1]; + } else { + 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 pair = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch blk: { + break :blk try find_bracket_pair(root, cursel, metrics, .right, opening_char); + }; + + prev.row = pair[0][0]; + prev.col = pair[0][1]; + next.row = pair[1][0]; + next.col = pair[1][1]; + } } prev.move_right(root, metrics) catch {};