Compare commits

...

3 commits

Author SHA1 Message Date
d53d155c6d
feat: add V language support
closes #509
2026-03-31 14:03:18 +02:00
Paul Graydon
ce7cc48a72 feat: [vim] Add bracket textobject actions 2026-03-31 10:07:55 +02:00
Paul Graydon
ba840b72e0 feat: [vim] Add word textobject actions 2026-03-31 10:07:55 +02:00
5 changed files with 483 additions and 6 deletions

View file

@ -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",

View file

@ -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 = .{

View file

@ -81,14 +81,62 @@
["dgg", "cut_buffer_begin"],
["\"_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"]],
["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"]],
["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"]],
["<C-u>", "move_scroll_half_page_up_vim"],
["<C-d>", "move_scroll_half_page_down_vim"],
@ -159,6 +207,22 @@
["B", "select_word_left"],
["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"],
[":", "open_command_palette"],

View file

@ -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;
@ -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 {

View file

@ -2,6 +2,14 @@ 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;
const bracket_search_radius = @import("../editor.zig").bracket_search_radius;
var commands: Commands = undefined;
pub fn init() !void {
@ -138,6 +146,398 @@ 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 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;
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 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;
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" };
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 {
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;
}
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;
}