summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig727
1 files changed, 13 insertions, 714 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 55e1dc7a..e001aa41 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -11,6 +11,7 @@ pub const syntax = @import("syntax.zig");
pub const locations_config = @import("locations_config.zig");
const tracy = @import("tracy.zig");
pub const panes = @import("panes.zig");
+pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
pub const body_layer = @import("body_layer.zig");
pub const Messages = @import("Messages.zig");
@@ -433,6 +434,7 @@ pub const Loc = struct {
};
test {
+ _ = @import("normal.zig");
_ = @import("edit.zig");
_ = @import("Messages.zig");
_ = @import("selection_pipe.zig");
@@ -576,72 +578,6 @@ test "Detach asks the frontend to leave, and says so when there is nothing to le
try std.testing.expect(!p.quit);
}
-test "gj/gk step the wrapped rows a body draws while j/k keep the file's lines" {
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .tty_only = true });
- defer p.deinit();
- while (p.nextEffect()) |_| {}
- const long = "a" ** 400;
- const pane = try p.setTestFile(long ++ "\nsecond\n");
- p.settings.wrap = true;
- const width = panes.File.wrapWidth(pane, true);
- try std.testing.expect(width > 4 and long.len > width * 3);
-
- // gj holds the column INSIDE the row and lands on the next break; the line
- // is one line the whole way down, which is the whole point.
- pane.cur_row = 0;
- pane.cur_col = 3;
- pane.sticky_col = -1;
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, 0), pane.cur_row);
- try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.cur_col);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, @intCast(width * 2 + 3)), pane.cur_col);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.cur_col);
- // A count is counted in rows, not lines.
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, 0), pane.cur_row);
- try std.testing.expectEqual(@as(i32, @intCast(width * 3 + 3)), pane.cur_col);
-
- // Plain j/k are untouched: one FILE line each, whatever the body drew.
- pane.cur_col = 3;
- pane.sticky_col = -1;
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, 1), pane.cur_row);
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(i32, 0), pane.cur_row);
-
- // The last row of a line steps into the next line's first row, and the
- // first row steps back onto the previous line's LAST row.
- pane.cur_row = 0;
- pane.cur_col = @intCast(long.len - 1);
- pane.sticky_col = -1;
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, 1), pane.cur_row);
- pane.cur_col = 0;
- pane.sticky_col = -1;
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(i32, 0), pane.cur_row);
- try std.testing.expect(pane.cur_col >= @as(i32, @intCast(long.len - long.len % width)));
-
- // Wrap OFF: a line is one visual row, so gj/gk ARE j/k again.
- p.settings.wrap = false;
- pane.cur_row = 0;
- pane.cur_col = 3;
- pane.sticky_col = -1;
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(i32, 1), pane.cur_row);
-}
-
test "the acme chords act once per selection, not once on the primary" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
@@ -2688,57 +2624,6 @@ test "every line of the board's boot buffer renders whole" {
// ---- the boundary types ----
-test "flat text movement and selection replay need no scratch rows" {
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 60, .rows = 12 });
- defer p.deinit();
- const row_text = "one (two) λ\r\n";
- const text = try gpa.alloc(u8, 4096 * row_text.len);
- defer gpa.free(text);
- for (0..4096) |row| @memcpy(text[row * row_text.len ..][0..row_text.len], row_text);
- const pane = try p.setTestFile(text);
- _ = try panes.File.lineIndex(gpa, &pane.file.?);
- var denied = std.testing.FailingAllocator.init(gpa, .{ .fail_index = 0 });
- p.scratch.deinit();
- p.scratch = .init(denied.allocator());
-
- pane.cur_row = 4094;
- p.handleNormal(pane, .{ .cp = 'l' });
- try std.testing.expectEqual(@as(i32, 1), pane.cur_col);
- p.handleNormal(pane, .{ .cp = 'j' });
- try std.testing.expectEqual(@as(i32, 4095), pane.cur_row);
- try std.testing.expectEqual(@as(i32, 1), pane.cur_col);
- p.handleNormal(pane, .{ .cp = 'h' });
- try std.testing.expectEqual(@as(i32, 0), pane.cur_col);
- p.executeNormalAction(pane, .{ .textobject = .{ .char = 'w', .around = false } });
- try std.testing.expectEqual(@as(i32, 2), pane.cur_col);
- try std.testing.expectEqual(@as(i32, 0), pane.vsel.col);
-
- pane.vsel.active = false;
- pane.cur_row = 1024;
- pane.cur_col = 0;
- pane.nsel = 1;
- pane.sels[0] = .{ .row = 3072, .col = 0, .arow = 3072, .acol = 0 };
- p.handleNormal(pane, .{ .cp = 'l' });
- p.handleNormal(pane, .{ .cp = 'j' });
- try std.testing.expectEqual(@as(u8, 1), pane.nsel);
- try std.testing.expectEqual(@as(i32, 1025), pane.cur_row);
- try std.testing.expectEqual(@as(i32, 1), pane.cur_col);
- try std.testing.expectEqual(@as(i32, 3073), pane.sels[0].row);
- try std.testing.expectEqual(@as(i32, 1), pane.sels[0].col);
- try std.testing.expect(!denied.has_induced_failure);
- try std.testing.expectEqual(@as(usize, 0), p.scratch.queryCapacity());
-
- pane.nsel = 0;
- pane.cur_col = 4;
- p.executeNormalAction(pane, .match_bracket);
- try std.testing.expect(denied.has_induced_failure);
- try std.testing.expectEqual(@as(i32, 4), pane.cur_col);
- denied.fail_index = std.math.maxInt(usize);
- p.executeNormalAction(pane, .match_bracket);
- try std.testing.expectEqual(@as(i32, 8), pane.cur_col);
-}
-
test "Unicode display cells map back to body and tag byte cursors" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 12 });
@@ -3272,47 +3157,6 @@ pub fn hit(key: Key, binding: []const config.Chord) bool {
pub const sel_slot = @intFromEnum(config.select_button);
-fn isPrefix(key: Key, cp: u21) bool {
- return key.cp == cp and !key.ctrl and !key.alt;
-}
-fn roleBindingName(comptime role: modal.Normal.Role) []const u8 {
- return switch (role) {
- .prefix_goto => "goto_prefix",
- .prefix_view => "view_prefix",
- .prefix_match => "match_prefix",
- .prefix_find_fwd => "find_char_fwd",
- .prefix_find_back => "find_char_back",
- .prefix_till_fwd => "till_char_fwd",
- .prefix_till_back => "till_char_back",
- .prefix_replace => "replace_prefix",
- .prefix_next => "next_prefix",
- .prefix_prev => "prev_prefix",
- else => @tagName(role),
- };
-}
-
-fn normalInput(key: Key) modal.Normal.Input {
- var out: modal.Normal.Input = .{
- .cp = key.cp,
- .ctrl = key.ctrl,
- .alt = key.alt,
- };
- inline for (std.enums.values(modal.Normal.Role)) |role| {
- const name = comptime roleBindingName(role);
- if (!@hasDecl(config, name))
- @compileError("normal input role has no config binding: " ++ name);
- const binding = @field(config, name);
- const matched = if (@TypeOf(binding) == comptime_int)
- isPrefix(key, binding)
- else if (@TypeOf(binding) == []const config.Chord)
- hit(key, binding)
- else
- @compileError("normal input binding has unsupported type: " ++ name);
- if (matched) out.roles.insert(role);
- }
- return out;
-}
-
/// Pixel dimensions are meaningful only to the native PDF placement path.
/// Keep resize events layout-only when that path is not part of the build.
pub const CellPixels = if (pdf_enabled) struct {
@@ -5694,12 +5538,6 @@ pub const Pardes = struct {
}
}
- fn multiOnce(p: *Pardes) bool {
- if (!p.multi_on) return true;
- p.multi_stop = true;
- return p.multi_first;
- }
-
// ---- tag + selection text (chord sources) ----
pub fn tagPrefix(p: *Pardes, pane: *Pane) ![]u8 {
@@ -6573,8 +6411,8 @@ pub const Pardes = struct {
.search => |at| {
if (submit)
p.submitSearch(p.active)
- else if (selRegexArmed(pane)) |_|
- p.applySelRegex(pane, "", false);
+ else if (normal.selRegexArmed(pane)) |_|
+ normal.applySelRegex(p, pane, "", false);
pane.tag_tail_len = @min(at, pane.tag_tail_len);
},
.none, .del_side => unreachable,
@@ -6587,7 +6425,7 @@ pub const Pardes = struct {
if (pane.tag_edit) {
if (pane.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key);
const pn = p.panes[p.active] orelse return;
- if (selRegexArmed(pn)) |a| p.applySelRegex(pn, a.pat, a.split);
+ if (normal.selRegexArmed(pn)) |a| normal.applySelRegex(p, pn, a.pat, a.split);
return;
}
if (pane.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
@@ -6610,10 +6448,10 @@ pub const Pardes = struct {
p.runBuiltin(cmd, p.active, "", word);
return;
}
- if (pane.mode == .normal and pane.hasPdf()) return p.handlePdfNormal(pane, key);
+ if (pane.mode == .normal and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key);
switch (pane.mode) {
.normal => {
- p.handleNormal(pane, key);
+ normal.handleNormal(p, pane, key);
},
.insert => {
if (hit(key, config.escape)) return edit.exitInsert(p, pane);
@@ -6651,501 +6489,6 @@ pub const Pardes = struct {
// ---- helix-modal normal mode ----
- fn startSelRegex(p: *Pardes, pane: *Pane, split: bool) void {
- const text = edit.flatSurface(p, pane) catch return;
- const got = pane.ranges(text, 0, &pane.sel_snap);
- pane.nsel_snap = @intCast(got.n);
- pane.sel_snap_pri = @intCast(got.pri);
- pane.sel_snap_expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active;
- p.startPrompt(pane, .{ .search = if (split) config.split_marker else config.select_marker });
- }
-
- pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
- const prompt_at = switch (pane.prompt) {
- .search => |at| at,
- else => return null,
- };
- if (pane.nsel_snap == 0) return null;
- const tail = pane.tagSlice();
- const armed = tail[@min(prompt_at, tail.len)..];
- const split = std.mem.startsWith(u8, armed, config.split_marker);
- if (!split and !std.mem.startsWith(u8, armed, config.select_marker)) return null;
- const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return null;
- return .{ .pat = armed[slash + 1 ..], .split = split };
- }
-
- pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void {
- const text = edit.flatSurface(p, pane) catch return;
- const snap = pane.sel_snap[0..pane.nsel_snap];
- var out: [Pane.max_selections]modal.Selection = undefined;
- var m: usize = 0;
- if (pat.len > 0) if (mvzr.compile(pat)) |re| {
- var hay_all = text;
- if (for (pat) |c| {
- if (std.ascii.isUpper(c)) break false;
- } else true) {
- const low = p.scratch.allocator().dupe(u8, text) catch return;
- for (low) |*c| c.* = std.ascii.toLower(c.*);
- hay_all = low;
- }
- for (snap) |r| {
- const from = @min(r.anchor, r.head);
- const to = @min(@max(r.anchor, r.head), text.len);
- if (from >= to) continue;
- const hay = hay_all[from..to];
- var at: usize = 0;
- var piece = from; // split: where the next piece begins
- while (at < hay.len and m < Pane.max_selections) {
- const hit_at = re.matchPos(at, hay) orelse break;
- if (split) {
- out[m] = .{ .anchor = piece, .head = from + hit_at.start };
- m += 1;
- piece = from + hit_at.end;
- } else if (from + hit_at.start != to) {
- out[m] = .{ .anchor = from + hit_at.start, .head = from + hit_at.end };
- m += 1;
- }
- // an empty match would otherwise never advance
- at = if (hit_at.end > hit_at.start) hit_at.end else hit_at.end + 1;
- }
- if (split and piece < to and m < Pane.max_selections) {
- out[m] = .{ .anchor = piece, .head = to };
- m += 1;
- }
- }
- };
- if (m == 0) {
- // the text CAN move under an armed prompt (a tag chord runs a
- // builtin), and these are raw offsets into the surface as it was
- for (snap) |*r| {
- r.anchor = @min(r.anchor, text.len);
- r.head = @min(r.head, text.len);
- }
- return pane.setRanges(text, snap, &.{}, pane.sel_snap_pri, pane.sel_snap_expl);
- }
- pane.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO)
- }
-
- const KeyState = struct {
- mode: Pane.Mode,
- select: bool,
- normal: modal.Normal.State,
- find_op: u8,
- find_ch: u21,
- append_at: @FieldType(Pane, "append_at"),
-
- fn of(pane: *Pane) KeyState {
- return .{
- .mode = pane.mode,
- .select = pane.select,
- .normal = pane.normal,
- .find_op = pane.find_op,
- .find_ch = pane.find_ch,
- .append_at = pane.append_at,
- };
- }
-
- fn into(s: KeyState, pane: *Pane) void {
- pane.mode = s.mode;
- pane.select = s.select;
- pane.normal = s.normal;
- pane.find_op = s.find_op;
- pane.find_ch = s.find_ch;
- pane.append_at = s.append_at;
- }
- };
-
- /// what a replayed key does at each cursor
- const Replay = union(enum) {
- normal: modal.Normal.Action,
- insert: Key,
- };
-
- pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void {
- const id = p.active;
- const serial = pane.serial;
- // the whole selection in pane coordinates, document order. pane.sels
- // is already ordered, so this only slots the primary into place.
- var list: [Pane.max_selections]Pane.SelRange = undefined;
- var n: usize = 0;
- var pri: usize = 0;
- const prim: Pane.SelRange = .{
- .row = pane.cur_row,
- .col = pane.cur_col,
- .arow = if (pane.vsel.active) pane.vsel.row else pane.cur_row,
- .acol = if (pane.vsel.active) pane.vsel.col else pane.cur_col,
- .sticky = pane.sticky_col,
- };
- const pr = Pane.selStart(prim);
- var placed = false;
- for (pane.sels[0..pane.nsel]) |s| {
- const sr = Pane.selStart(s);
- if (!placed and (pr.row < sr.row or (pr.row == sr.row and pr.col <= sr.col))) {
- pri = n;
- list[n] = prim;
- n += 1;
- placed = true;
- }
- list[n] = s;
- n += 1;
- }
- if (!placed) {
- pri = n;
- list[n] = prim;
- n += 1;
- }
-
- const saved = KeyState.of(pane);
- const explicit = pane.vsel.explicit;
- var after = saved;
- var after_expl = explicit;
- // each pass's result: cursor and anchor cells as distances from the
- // end of the surface text, plus the range's own j/k goal column
- var res: [Pane.max_selections]struct { cur: usize, anc: usize, sticky: i32 } = undefined;
- p.multi_on = true;
- p.multi_stop = false;
- var passes: usize = 0;
- var i: usize = n;
- while (i > 0) {
- i -= 1;
- p.multi_first = passes == 0;
- passes += 1;
- saved.into(pane);
- pane.nsel = 0; // the handler underneath sees ONE selection
- pane.cur_row = list[i].row;
- pane.cur_col = list[i].col;
- pane.vsel = .{
- .active = list[i].arow != list[i].row or list[i].acol != list[i].col or pane.select,
- .row = list[i].arow,
- .col = list[i].acol,
- .explicit = explicit,
- };
- pane.msel.active = false;
- pane.sticky_col = list[i].sticky;
- switch (what) {
- .normal => |normal_action| p.executeNormalAction(pane, normal_action),
- .insert => |insert_key| edit.insertKey(p, pane, insert_key),
- }
- if (p.multi_stop) break;
- if (i == pri) {
- after = KeyState.of(pane);
- after_expl = pane.vsel.explicit;
- }
- const text = edit.flatSurface(p, pane) catch {
- p.multi_stop = true;
- break;
- };
- const co = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) });
- const ao = if (pane.vsel.active)
- modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.vsel.row)), .col = @intCast(@max(0, pane.vsel.col)) })
- else
- co;
- res[i] = .{ .cur = text.len - @min(co, text.len), .anc = text.len - @min(ao, text.len), .sticky = pane.sticky_col };
- }
- p.multi_on = false;
- p.multi_first = false;
- if (p.multi_stop) {
- p.multi_stop = false;
- const pn = p.panes[id] orelse return;
- if (pn.serial == serial) pn.nsel = 0;
- return;
- }
- const text = edit.flatSurface(p, pane) catch return;
- var rs: [Pane.max_selections]modal.Selection = undefined;
- var st: [Pane.max_selections]i32 = undefined;
- for (res[0..n], 0..) |r, k| {
- rs[k] = Pane.cellOffRange(text, text.len - @min(r.anc, text.len), text.len - @min(r.cur, text.len));
- st[k] = r.sticky;
- }
- pane.setRanges(text, rs[0..n], st[0..n], pri, after_expl);
- after.into(pane);
- pane.ensureCursorVisible(); // the view follows the PRIMARY, not the last pass
- }
-
- fn handleNormal(p: *Pardes, pane: *Pane, key: Key) void {
- const parsed = modal.Normal.parse(&pane.normal, normalInput(key));
- const semantic = switch (parsed) {
- .pending, .ignored, .unbound => return,
- .action => |value| value,
- };
- if (pane.nsel == 0 or semantic.scope() == .once)
- return p.executeNormalAction(pane, semantic);
- p.replaySels(pane, .{ .normal = semantic });
- }
-
- fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) void {
- const raw_selection = pane.rawPointerText(pane.sel[sel_slot]) != null;
- const before = .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode };
- defer if (raw_selection and !std.meta.eql(before, .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode })) {
- pane.clearPointerSelection(sel_slot);
- pane.sel[sel_slot].state = .none;
- };
- if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return edit.normalYank(p, pane);
- edit.pinPaneCursor(p, pane);
- const text = edit.flatSurface(p, pane) catch return;
- const range = pane.primaryRange(text, 0);
- const cur = modal.selectionCursor(text, range);
-
- switch (semantic) {
- .escape => {
- pane.select = false;
- return p.runBuiltin(.Last, p.active, "", null);
- },
- .goto => |go| switch (go.target) {
- .file_start => {
- const line = if (go.explicit_count) @min(@as(usize, go.count) - 1, Pane.maxLine(text)) else 0;
- return pane.pointMove(text, range, modal.lineStartOffset(text, line));
- },
- .last_line => return pane.pointMove(text, range, modal.lineStartOffset(text, Pane.maxLine(text))),
- .line_start => return pane.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))),
- .line_end => {
- const line = modal.lineAtOffset(text, cur);
- const ls = modal.lineStartOffset(text, line);
- return pane.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line))));
- },
- .first_nonws => {
- const line = modal.lineAtOffset(text, cur);
- const ls = modal.lineStartOffset(text, line);
- const slice = text[ls..modal.lineEndOffset(text, line)];
- const nw = modal.firstNonWs(slice);
- if (nw == slice.len) return;
- return pane.pointMove(text, range, ls + nw);
- },
- .line_down => return pane.visualMove(text, range, true, go.count, edit.paneWrapWidth(p, pane)),
- .line_up => return pane.visualMove(text, range, false, go.count, edit.paneWrapWidth(p, pane)),
- .column => {
- const line = modal.lineAtOffset(text, cur);
- const ls = modal.lineStartOffset(text, line);
- const slice = text[ls..modal.lineEndOffset(text, line)];
- return pane.pointMove(text, range, ls + modal.graphemeAtColumn(slice, @as(usize, go.count) - 1));
- },
- .view_top => return pane.gotoWindow(text, range, .top, go.count),
- .view_center => return pane.gotoWindow(text, range, .center, go.count),
- .view_bottom => return pane.gotoWindow(text, range, .bottom, go.count),
- },
- .view => |view| switch (view) {
- .top => {
- pane.scrollBy(pane.cur_row - pane.scroll());
- pane.ensureCursorVisible();
- },
- .center => {
- pane.scrollBy(pane.cur_row - (pane.scroll() + @divTrunc(@as(i32, pane.rows), 2) - 1));
- pane.ensureCursorVisible();
- },
- .bottom => {
- pane.scrollBy(pane.cur_row - (pane.scroll() + @as(i32, pane.rows) - 1));
- pane.ensureCursorVisible();
- },
- .scroll_down => return pane.scrollViewMove(text, range, 1),
- .scroll_up => return pane.scrollViewMove(text, range, -1),
- },
- .find => |find| {
- const op: u8 = switch (find.kind) {
- .forward => @intCast(config.find_char_fwd),
- .backward => @intCast(config.find_char_back),
- .till_forward => @intCast(config.till_char_fwd),
- .till_backward => @intCast(config.till_char_back),
- };
- pane.find_op = op;
- pane.find_ch = find.char;
- return pane.findMove(
- text,
- range,
- find.char,
- find.kind == .forward or find.kind == .till_forward,
- find.kind == .till_forward or find.kind == .till_backward,
- find.count,
- );
- },
- .replace_char => |char| return edit.normalReplaceChar(p, pane, char),
- .match_bracket => {
- const lines = edit.paneCursorLines(p, pane) catch return;
- const mc = modal.matchBracket(lines, modal.positionAt(text, cur)) orelse return;
- return pane.pointMove(text, range, modal.offsetAt(text, mc));
- },
- .textobject => |object| return edit.textobjectSelect(p, pane, text, object.char, object.around),
- .surround_add => |char| return edit.surroundAdd(p, pane, char),
- .surround_delete => |char| return edit.surroundDelete(p, pane, char),
- .surround_replace => |replace| return edit.surroundReplace(p, pane, replace.from, replace.to),
- .paragraph => |paragraph| {
- const r2 = modal.moveParagraph(text, range, paragraph.count, paragraph.direction == .forward, pane.select);
- return pane.setRange(text, 0, r2, false);
- },
- .add_newline => |newline| return edit.addNewline(p, pane, newline.direction == .forward, newline.count),
- .diagnostic => |diagnostic| {
- const fwd = diagnostic.direction == .forward;
- if (!diagnostic.endpoint) {
- if (panes.Output.resultsFrom(p, pane, .{ .query = .diagnostics }) and
- p.searchStep(p.active, if (fwd) 1 else -1)) return;
- return p.lspRequest(p.active, .diagnostics, "");
- }
- if (!panes.Output.resultsFrom(p, pane, .{ .query = .diagnostics }))
- return p.lspRequest(p.active, .diagnostics, "");
- if (!fwd) {
- pane.search_row = null;
- _ = p.searchStep(p.active, 1);
- return;
- }
- const rp = p.panes[pane.search_pane orelse return] orelse return;
- const rf = rp.file orelse return;
- pane.search_row = std.mem.count(u8, rf.content, "\n");
- _ = p.searchStep(p.active, -1);
- },
- .move => |move| switch (move.motion) {
- .left => {
- var target = cur;
- for (0..move.count) |_| target = modal.prevGrapheme(text, target);
- return pane.pointMove(text, range, target);
- },
- .right => {
- var target = cur;
- for (0..move.count) |_| target = modal.nextGrapheme(text, target);
- return pane.pointMove(text, range, target);
- },
- .down => return pane.verticalMove(text, range, true, move.count),
- .up => return pane.verticalMove(text, range, false, move.count),
- .next_word_start => return pane.wordMove(text, range, move.count, .next_word_start),
- .prev_word_start => return pane.wordMove(text, range, move.count, .prev_word_start),
- .next_word_end => return pane.wordMove(text, range, move.count, .next_word_end),
- .next_long_word_start => return pane.wordMove(text, range, move.count, .next_long_word_start),
- .prev_long_word_start => return pane.wordMove(text, range, move.count, .prev_long_word_start),
- .next_long_word_end => return pane.wordMove(text, range, move.count, .next_long_word_end),
- },
- .repeat_find => |count| {
- if (pane.find_op == 0) return;
- const fwd = pane.find_op == config.find_char_fwd or pane.find_op == config.till_char_fwd;
- const till = pane.find_op == config.till_char_fwd or pane.find_op == config.till_char_back;
- var repeated = range;
- var moved = false;
- for (0..count) |_| {
- const cc = modal.selectionCursor(text, repeated);
- const target = modal.findTarget(text, cc, pane.find_ch, fwd, till, 1) orelse break;
- repeated = if (pane.select)
- modal.moveSelectionCursor(text, repeated, target, true)
- else
- modal.moveSelectionCursor(text, .{ .anchor = cc, .head = cc }, target, true);
- moved = true;
- }
- if (!moved) return;
- return pane.setRange(text, 0, repeated, false);
- },
- .line => |line_kind| switch (line_kind) {
- .start => return pane.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))),
- .end => {
- const line = modal.lineAtOffset(text, cur);
- const ls = modal.lineStartOffset(text, line);
- return pane.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line))));
- },
- .first_nonws => {
- const line = modal.lineAtOffset(text, cur);
- const ls = modal.lineStartOffset(text, line);
- const slice = text[ls..modal.lineEndOffset(text, line)];
- const nw = modal.firstNonWs(slice);
- if (nw == slice.len) return;
- return pane.pointMove(text, range, ls + nw);
- },
- },
- .goto_line => |go| {
- if (!go.explicit) return;
- const line = @min(@as(usize, go.count) - 1, Pane.maxLine(text));
- return pane.pointMove(text, range, modal.lineStartOffset(text, line));
- },
- .page => |page| switch (page.kind) {
- // Counts were parsed historically but these four text view
- // actions intentionally move exactly one viewport unit.
- .half_down => return pane.halfPageMove(text, range, true),
- .half_up => return pane.halfPageMove(text, range, false),
- .down => return pane.scrollViewMove(text, range, @as(i32, pane.rows)),
- .up => return pane.scrollViewMove(text, range, -@as(i32, pane.rows)),
- },
- .insert => |insert| {
- const where: edit.InsertAt = switch (insert.kind) {
- .at => .at,
- .append => .append,
- .line_start => .line_start,
- .line_end => .line_end,
- .open_below => .open_below,
- .open_above => .open_above,
- };
- return edit.enterInsert(p, pane, where, insert.count);
- },
- .select => |select| switch (select.kind) {
- .mode => {
- if (pane.select) {
- pane.select = false;
- } else {
- pane.select = true;
- if (pane.msel.active) {
- pane.setRange(text, 0, range, true);
- } else if (!pane.vsel.active) {
- pane.vsel = .{ .active = true, .row = pane.cur_row, .col = pane.cur_col, .explicit = true };
- } else {
- pane.vsel.explicit = true;
- }
- }
- pane.cur_pinned = true;
- },
- .line => return pane.lineSelect(text, range, select.count),
- .line_bounds => return pane.lineBoundsSelect(text, range),
- .shrink_to_line_bounds => return pane.shrinkSelToLineBounds(text, range),
- .collapse => return pane.setRange(text, 0, .{ .anchor = cur, .head = cur }, false),
- .flip => {
- const explicit = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active;
- return pane.setRange(text, 0, .{ .anchor = range.head, .head = range.anchor }, explicit);
- },
- .all => return pane.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false),
- },
- .multi => |multi| {
- if (multi.kind == .keep_primary) {
- pane.nsel = 0;
- return;
- }
- return pane.multiSelAction(text, multi.kind, multi.count);
- },
- .select_regex => |split| return p.startSelRegex(pane, split),
- .edit => |op| switch (op.kind) {
- .delete => return edit.normalDelete(p, pane, true),
- .delete_noyank => return edit.normalDelete(p, pane, false),
- .change => return edit.normalChange(p, pane),
- .yank => return edit.normalYank(p, pane),
- .replace_with_yank => return edit.normalReplaceYank(p, pane),
- .paste_after => return edit.pasteText(p, pane, p.yank orelse return, false, op.count),
- .paste_before => return edit.pasteText(p, pane, p.yank orelse return, true, op.count),
- .switch_case => return edit.normalCase(p, pane, .toggle),
- .lowercase => return edit.normalCase(p, pane, .lower),
- .uppercase => return edit.normalCase(p, pane, .upper),
- .join_lines => return edit.normalJoin(p, pane),
- .indent => return edit.normalIndent(p, pane, op.count, true),
- .unindent => return edit.normalIndent(p, pane, op.count, false),
- .comment_toggle => return edit.normalToggleComment(p, pane),
- .undo => return edit.doUndo(p, pane),
- .redo => return edit.doRedo(p, pane),
- },
- .lsp => |request| return p.lspRequest(p.active, switch (request) {
- .definition => .definition,
- .declaration => .declaration,
- .type_definition => .type_definition,
- .implementation => .implementation,
- .references => .references,
- .format => .format,
- }, ""),
- .adjust_number => |delta| return edit.normalAdjustNumber(p, pane, delta),
- .leader => {
- p.leader_on = true;
- p.leader_n = 0;
- },
- .command_line => {
- p.enterTagEdit(pane, -1);
- if (pane.tag_edit) pane.mode = .normal;
- },
- .pipe_selection => |how| return p.startPrompt(pane, .{ .pipe = how }),
- .search => return p.startPrompt(pane, .{ .search = config.search_marker }),
- .search_step => |direction| return p.lookWalk(
- if (direction == .forward) @as(i32, 1) else -1,
- ),
- }
- }
-
// ---- selection pipe (`|`): visible prompt, async shell, atomic edit ----
// ---- search (`/`, Find, n/N): results into an output buffer, look the rows ----
@@ -7279,7 +6622,7 @@ pub const Pardes = struct {
fn submitSearch(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
- if (selRegexArmed(pane)) |a| return p.applySelRegex(pane, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| return normal.applySelRegex(p, pane, a.pat, a.split);
const armed = pane.promptText() orelse return;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
if (std.mem.startsWith(u8, armed, config.rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
@@ -7400,7 +6743,7 @@ pub const Pardes = struct {
try panes.Output.fillResults(p, id, dir, from, pat, content, anchor);
}
- fn searchStep(p: *Pardes, id: usize, delta: i32) bool {
+ pub fn searchStep(p: *Pardes, id: usize, delta: i32) bool {
const pane = p.panes[id] orelse return false;
const rid = pane.search_pane orelse return false;
const rp = p.panes[rid] orelse return false;
@@ -7477,7 +6820,7 @@ pub const Pardes = struct {
}
pub fn lspRequest(p: *Pardes, id: usize, kind: lsp.Kind, arg: []const u8) void {
- if (!p.multiOnce()) return; // one question per keystroke, from the primary
+ if (!normal.multiOnce(p)) return; // one question per keystroke, from the primary
if (!lsp.supports.contains(kind)) return;
const pane = p.panes[id] orelse return;
if (kind != .status) {
@@ -7803,7 +7146,7 @@ pub const Pardes = struct {
return null;
}
- fn lookWalk(p: *Pardes, delta: i32) void {
+ pub fn lookWalk(p: *Pardes, delta: i32) void {
var buf: [MAX_PANES]usize = undefined;
const order = p.lookWalkPanes(&buf);
if (order.len == 0) return;
@@ -8976,50 +8319,6 @@ pub const Pardes = struct {
return pane;
}
- fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void {
- if (comptime !pdf_enabled) return;
- if (pane.pdf == null) return;
- if (hit(key, config.leave_pane)) {
- pane.normal.clear();
- pane.select = false;
- return p.runBuiltin(.Last, p.active, "", null);
- }
- if (pane.normal.prefix == .none and isPrefix(key, 'f')) {
- pane.normal.clear();
- return p.runBuiltin(.PdfSections, p.active, "", null);
- }
- const parsed = modal.Normal.parse(&pane.normal, normalInput(key));
- switch (parsed) {
- .pending, .ignored, .unbound => {},
- .action => |semantic| {
- const result = panes.Pdf.applyNormal(
- &pane.pdf.?,
- p.pdf_gpa,
- semantic,
- p.native_images,
- .{ .w = p.cell_pixels.w, .h = p.cell_pixels.h },
- panes.Pdf.paneViewport(p, pane),
- panes.Pdf.paneGeometry(p, pane),
- );
- if (result.page_changed) panes.Pdf.resetPageChrome(pane);
- switch (result.host) {
- .none => {},
- .leader => {
- p.leader_on = true;
- p.leader_n = 0;
- },
- .command_line => {
- p.enterTagEdit(pane, -1);
- if (pane.tag_edit) pane.mode = .normal;
- },
- .search => p.startPrompt(pane, .{ .search = config.search_marker }),
- .search_forward => p.lookWalk(1),
- .search_backward => p.lookWalk(-1),
- }
- },
- }
- }
-
// ---- the ONE dispatcher: look (right/Enter) and execute (middle/Tab) ----
pub fn focusPaneLine(p: *Pardes, id: usize, at: look.Spot, landing: enum { center, keep }) void {
@@ -9358,8 +8657,8 @@ pub const Pardes = struct {
while (lines.next()) |line| _ = p.executeBuiltinLine(p.active, line);
}
- fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
- if (!p.multiOnce()) return; // a builtin is per-keystroke, never per-cursor
+ pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
+ if (!normal.multiOnce(p)) return; // a builtin is per-keystroke, never per-cursor
const pane = p.panes[id] orelse return;
// A builtin says its own name before it runs, so a tagline click is
// legible even when what it did is invisible. Whatever the builtin