diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-27 23:06:11 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 95a3a1bb822e583988bfe0f312d0ef219b27a454 (patch) | |
| tree | 95306e8d3f36f8af05b3ea919f6cdef1e94a028b /src/normal.zig | |
| parent | c8f0c25a854b8e169db08a84d1fdcd915a70aaa0 (diff) | |
| download | pardes-95a3a1bb822e583988bfe0f312d0ef219b27a454.tar.gz pardes-95a3a1bb822e583988bfe0f312d0ef219b27a454.zip | |
Give a pane's body its own Text holding the cursor, selections, mode and undo
acme keeps what edits a text in its Text (dat.h:171-190) and the window
holds a body and a tag of that type. The cursor, the selections, the modal
state and the edit-buffer undo move off Pane into Text.zig, Pane holds
them as its body, and the edit and normal-mode operations take the Text
they edit. Nothing changes in behaviour; this is the step that lets the
tag become a second Text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/normal.zig')
| -rw-r--r-- | src/normal.zig | 464 |
1 files changed, 233 insertions, 231 deletions
diff --git a/src/normal.zig b/src/normal.zig index 6647dcca..192fd933 100644 --- a/src/normal.zig +++ b/src/normal.zig @@ -12,6 +12,7 @@ const edit = @import("edit.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const Pane = panes.Pane; +const Text = panes.Text; const Key = pardes.Key; const hit = pardes.hit; const sel_slot = pardes.sel_slot; @@ -66,11 +67,11 @@ pub fn multiOnce(p: *Pardes) bool { } 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); + const text = edit.flatSurface(p, &pane.body) catch return; + const got = pane.body.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; + pane.sel_snap_expl = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active; p.startPrompt(pane, .{ .search = if (split) config.split_marker else config.select_marker }); } @@ -89,9 +90,9 @@ pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } { } pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void { - const text = edit.flatSurface(p, pane) catch return; + const text = edit.flatSurface(p, &pane.body) catch return; const snap = pane.sel_snap[0..pane.nsel_snap]; - var out: [Pane.max_selections]modal.Selection = undefined; + var out: [Text.max_selections]modal.Selection = undefined; var m: usize = 0; if (pat.len > 0) if (mvzr.compile(pat)) |re| { var hay_all = text; @@ -109,7 +110,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void 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) { + while (at < hay.len and m < Text.max_selections) { const hit_at = re.matchPos(at, hay) orelse break; if (split) { out[m] = .{ .anchor = piece, .head = from + hit_at.start }; @@ -122,7 +123,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void // 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) { + if (split and piece < to and m < Text.max_selections) { out[m] = .{ .anchor = piece, .head = to }; m += 1; } @@ -135,37 +136,37 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void 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); + return pane.body.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) + pane.body.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO) } const KeyState = struct { - mode: Pane.Mode, + mode: Text.Mode, select: bool, normal: modal.Normal.State, find_op: u8, find_ch: u21, - append_at: @FieldType(Pane, "append_at"), + append_at: @FieldType(Text, "append_at"), - fn of(pane: *Pane) KeyState { + fn of(t: *Text) 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, + .mode = t.mode, + .select = t.select, + .normal = t.normal, + .find_op = t.find_op, + .find_ch = t.find_ch, + .append_at = t.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; + fn into(s: KeyState, t: *Text) void { + t.mode = s.mode; + t.select = s.select; + t.normal = s.normal; + t.find_op = s.find_op; + t.find_ch = s.find_ch; + t.append_at = s.append_at; } }; @@ -175,25 +176,25 @@ const Replay = union(enum) { insert: Key, }; -pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { +pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void { const id = p.active; - const serial = pane.serial; - // the whole selection in pane coordinates, document order. pane.sels + const serial = t.pane().serial; + // the whole selection in text coordinates, document order. t.sels // is already ordered, so this only slots the primary into place. - var list: [Pane.max_selections]Pane.SelRange = undefined; + var list: [Text.max_selections]Text.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 prim: Text.SelRange = .{ + .row = t.cur_row, + .col = t.cur_col, + .arow = if (t.vsel.active) t.vsel.row else t.cur_row, + .acol = if (t.vsel.active) t.vsel.col else t.cur_col, + .sticky = t.sticky_col, }; - const pr = Pane.selStart(prim); + const pr = Text.selStart(prim); var placed = false; - for (pane.sels[0..pane.nsel]) |s| { - const sr = Pane.selStart(s); + for (t.sels[0..t.nsel]) |s| { + const sr = Text.selStart(s); if (!placed and (pr.row < sr.row or (pr.row == sr.row and pr.col <= sr.col))) { pri = n; list[n] = prim; @@ -209,13 +210,13 @@ pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { n += 1; } - const saved = KeyState.of(pane); - const explicit = pane.vsel.explicit; + const saved = KeyState.of(t); + const explicit = t.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; + var res: [Text.max_selections]struct { cur: usize, anc: usize, sticky: i32 } = undefined; p.multi_on = true; p.multi_stop = false; var passes: usize = 0; @@ -224,98 +225,99 @@ pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { 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, + saved.into(t); + t.nsel = 0; // the handler underneath sees ONE selection + t.cur_row = list[i].row; + t.cur_col = list[i].col; + t.vsel = .{ + .active = list[i].arow != list[i].row or list[i].acol != list[i].col or t.select, .row = list[i].arow, .col = list[i].acol, .explicit = explicit, }; - pane.msel.active = false; - pane.sticky_col = list[i].sticky; + t.msel.active = false; + t.sticky_col = list[i].sticky; switch (what) { - .normal => |normal_action| executeNormalAction(p, pane, normal_action), - .insert => |insert_key| edit.insertKey(p, pane, insert_key), + .normal => |normal_action| executeNormalAction(p, t, normal_action), + .insert => |insert_key| edit.insertKey(p, t, insert_key), } if (p.multi_stop) break; if (i == pri) { - after = KeyState.of(pane); - after_expl = pane.vsel.explicit; + after = KeyState.of(t); + after_expl = t.vsel.explicit; } - const text = edit.flatSurface(p, pane) catch { + const text = edit.flatSurface(p, t) 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)) }) + const co = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); + const ao = if (t.vsel.active) + modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) }) else co; - res[i] = .{ .cur = text.len - @min(co, text.len), .anc = text.len - @min(ao, text.len), .sticky = pane.sticky_col }; + res[i] = .{ .cur = text.len - @min(co, text.len), .anc = text.len - @min(ao, text.len), .sticky = t.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; + if (pn.serial == serial) pn.body.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; + const text = edit.flatSurface(p, t) catch return; + var rs: [Text.max_selections]modal.Selection = undefined; + var st: [Text.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)); + rs[k] = Text.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 + t.setRanges(text, rs[0..n], st[0..n], pri, after_expl); + after.into(t); + t.pane().ensureCursorVisible(); // the view follows the PRIMARY, not the last pass } -pub fn handleNormal(p: *Pardes, pane: *Pane, key: Key) void { - const parsed = modal.Normal.parse(&pane.normal, normalInput(key)); +pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void { + const parsed = modal.Normal.parse(&t.normal, normalInput(key)); const semantic = switch (parsed) { .pending, .ignored, .unbound => return, .action => |value| value, }; - if (pane.nsel == 0 or semantic.scope() == .once) - return executeNormalAction(p, pane, semantic); - replaySels(p, pane, .{ .normal = semantic }); + if (t.nsel == 0 or semantic.scope() == .once) + return executeNormalAction(p, t, semantic); + replaySels(p, t, .{ .normal = semantic }); } -fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) void { +fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void { + const pane = t.pane(); 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 })) { + const before = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.select, t.nsel, t.mode }; + defer if (raw_selection and !std.meta.eql(before, .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.select, t.nsel, t.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); + if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return edit.normalYank(p, t); + edit.pinPaneCursor(p, t); + const text = edit.flatSurface(p, t) catch return; + const range = t.primaryRange(text, 0); const cur = modal.selectionCursor(text, range); switch (semantic) { .escape => { - pane.select = false; + t.select = false; return exec.runBuiltin(p, .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)); + const line = if (go.explicit_count) @min(@as(usize, go.count) - 1, Text.maxLine(text)) else 0; + return t.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))), + .last_line => return t.pointMove(text, range, modal.lineStartOffset(text, Text.maxLine(text))), + .line_start => return t.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)))); + return t.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); }, .first_nonws => { const line = modal.lineAtOffset(text, cur); @@ -323,35 +325,35 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v 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); + return t.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)), + .line_down => return t.visualMove(text, range, true, go.count, edit.paneWrapWidth(p, t)), + .line_up => return t.visualMove(text, range, false, go.count, edit.paneWrapWidth(p, t)), .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)); + return t.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_top => return t.gotoWindow(text, range, .top, go.count), + .view_center => return t.gotoWindow(text, range, .center, go.count), + .view_bottom => return t.gotoWindow(text, range, .bottom, go.count), }, .view => |view| switch (view) { .top => { - pane.scrollBy(pane.cur_row - pane.scroll()); + pane.scrollBy(t.cur_row - pane.scroll()); pane.ensureCursorVisible(); }, .center => { - pane.scrollBy(pane.cur_row - (pane.scroll() + @divTrunc(@as(i32, pane.rows), 2) - 1)); + pane.scrollBy(t.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.scrollBy(t.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), + .scroll_down => return t.scrollViewMove(text, range, 1), + .scroll_up => return t.scrollViewMove(text, range, -1), }, .find => |find| { const op: u8 = switch (find.kind) { @@ -360,9 +362,9 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .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( + t.find_op = op; + t.find_ch = find.char; + return t.findMove( text, range, find.char, @@ -371,21 +373,21 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v find.count, ); }, - .replace_char => |char| return edit.normalReplaceChar(p, pane, char), + .replace_char => |char| return edit.normalReplaceChar(p, t, char), .match_bracket => { - const lines = edit.paneCursorLines(p, pane) catch return; + const lines = edit.paneCursorLines(p, t) catch return; const mc = modal.matchBracket(lines, modal.positionAt(text, cur)) orelse return; - return pane.pointMove(text, range, modal.offsetAt(text, mc)); + return t.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), + .textobject => |object| return edit.textobjectSelect(p, t, text, object.char, object.around), + .surround_add => |char| return edit.surroundAdd(p, t, char), + .surround_delete => |char| return edit.surroundDelete(p, t, char), + .surround_replace => |replace| return edit.surroundReplace(p, t, 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); + const r2 = modal.moveParagraph(text, range, paragraph.count, paragraph.direction == .forward, t.select); + return t.setRange(text, 0, r2, false); }, - .add_newline => |newline| return edit.addNewline(p, pane, newline.direction == .forward, newline.count), + .add_newline => |newline| return edit.addNewline(p, t, newline.direction == .forward, newline.count), .diagnostic => |diagnostic| { const fwd = diagnostic.direction == .forward; if (!diagnostic.endpoint) { @@ -409,46 +411,46 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .left => { var target = cur; for (0..move.count) |_| target = modal.prevGrapheme(text, target); - return pane.pointMove(text, range, target); + return t.pointMove(text, range, target); }, .right => { var target = cur; for (0..move.count) |_| target = modal.nextGrapheme(text, target); - return pane.pointMove(text, range, target); + return t.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), + .down => return t.verticalMove(text, range, true, move.count), + .up => return t.verticalMove(text, range, false, move.count), + .next_word_start => return t.wordMove(text, range, move.count, .next_word_start), + .prev_word_start => return t.wordMove(text, range, move.count, .prev_word_start), + .next_word_end => return t.wordMove(text, range, move.count, .next_word_end), + .next_long_word_start => return t.wordMove(text, range, move.count, .next_long_word_start), + .prev_long_word_start => return t.wordMove(text, range, move.count, .prev_long_word_start), + .next_long_word_end => return t.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; + if (t.find_op == 0) return; + const fwd = t.find_op == config.find_char_fwd or t.find_op == config.till_char_fwd; + const till = t.find_op == config.till_char_fwd or t.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) + const target = modal.findTarget(text, cc, t.find_ch, fwd, till, 1) orelse break; + repeated = if (t.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); + return t.setRange(text, 0, repeated, false); }, .line => |line_kind| switch (line_kind) { - .start => return pane.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))), + .start => return t.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)))); + return t.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); }, .first_nonws => { const line = modal.lineAtOffset(text, cur); @@ -456,21 +458,21 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v 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); + return t.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)); + const line = @min(@as(usize, go.count) - 1, Text.maxLine(text)); + return t.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)), + .half_down => return t.halfPageMove(text, range, true), + .half_up => return t.halfPageMove(text, range, false), + .down => return t.scrollViewMove(text, range, @as(i32, pane.rows)), + .up => return t.scrollViewMove(text, range, -@as(i32, pane.rows)), }, .insert => |insert| { const where: edit.InsertAt = switch (insert.kind) { @@ -481,59 +483,59 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .open_below => .open_below, .open_above => .open_above, }; - return edit.enterInsert(p, pane, where, insert.count); + return edit.enterInsert(p, t, where, insert.count); }, .select => |select| switch (select.kind) { .mode => { - if (pane.select) { - pane.select = false; + if (t.select) { + t.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 }; + t.select = true; + if (t.msel.active) { + t.setRange(text, 0, range, true); + } else if (!t.vsel.active) { + t.vsel = .{ .active = true, .row = t.cur_row, .col = t.cur_col, .explicit = true }; } else { - pane.vsel.explicit = true; + t.vsel.explicit = true; } } - pane.cur_pinned = true; + t.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), + .line => return t.lineSelect(text, range, select.count), + .line_bounds => return t.lineBoundsSelect(text, range), + .shrink_to_line_bounds => return t.shrinkSelToLineBounds(text, range), + .collapse => return t.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); + const explicit = (t.vsel.active and t.vsel.explicit) or t.msel.active; + return t.setRange(text, 0, .{ .anchor = range.head, .head = range.anchor }, explicit); }, - .all => return pane.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false), + .all => return t.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false), }, .multi => |multi| { if (multi.kind == .keep_primary) { - pane.nsel = 0; + t.nsel = 0; return; } - return pane.multiSelAction(text, multi.kind, multi.count); + return t.multiSelAction(text, multi.kind, multi.count); }, .select_regex => |split| return startSelRegex(p, 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), + .delete => return edit.normalDelete(p, t, true), + .delete_noyank => return edit.normalDelete(p, t, false), + .change => return edit.normalChange(p, t), + .yank => return edit.normalYank(p, t), + .replace_with_yank => return edit.normalReplaceYank(p, t), + .paste_after => return edit.pasteText(p, t, p.yank orelse return, false, op.count), + .paste_before => return edit.pasteText(p, t, p.yank orelse return, true, op.count), + .switch_case => return edit.normalCase(p, t, .toggle), + .lowercase => return edit.normalCase(p, t, .lower), + .uppercase => return edit.normalCase(p, t, .upper), + .join_lines => return edit.normalJoin(p, t), + .indent => return edit.normalIndent(p, t, op.count, true), + .unindent => return edit.normalIndent(p, t, op.count, false), + .comment_toggle => return edit.normalToggleComment(p, t), + .undo => return edit.doUndo(p, t), + .redo => return edit.doRedo(p, t), }, .lsp => |request| return p.lspRequest(p.active, switch (request) { .definition => .definition, @@ -543,14 +545,14 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .references => .references, .format => .format, }, ""), - .adjust_number => |delta| return edit.normalAdjustNumber(p, pane, delta), + .adjust_number => |delta| return edit.normalAdjustNumber(p, t, delta), .leader => { p.leader_on = true; p.leader_n = 0; }, .command_line => { p.enterTagEdit(pane, -1); - if (pane.tag_edit) pane.mode = .normal; + if (pane.tag_edit) t.mode = .normal; }, .pipe_selection => |how| return p.startPrompt(pane, .{ .pipe = how }), .search => return p.startPrompt(pane, .{ .search = config.search_marker }), @@ -565,15 +567,15 @@ pub 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; + pane.body.normal.clear(); + pane.body.select = false; return exec.runBuiltin(p, .Last, p.active, "", null); } - if (pane.normal.prefix == .none and isPrefix(key, 'f')) { - pane.normal.clear(); + if (pane.body.normal.prefix == .none and isPrefix(key, 'f')) { + pane.body.normal.clear(); return exec.runBuiltin(p, .PdfSections, p.active, "", null); } - const parsed = modal.Normal.parse(&pane.normal, normalInput(key)); + const parsed = modal.Normal.parse(&pane.body.normal, normalInput(key)); switch (parsed) { .pending, .ignored, .unbound => {}, .action => |semantic| { @@ -595,7 +597,7 @@ pub fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void { }, .command_line => { p.enterTagEdit(pane, -1); - if (pane.tag_edit) pane.mode = .normal; + if (pane.tag_edit) pane.body.mode = .normal; }, .search => p.startPrompt(pane, .{ .search = config.search_marker }), .search_forward => look.lookWalk(p, 1), @@ -618,57 +620,57 @@ test "gj/gk step the wrapped rows a body draws while j/k keep the file's lines" // 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; + pane.body.cur_row = 0; + pane.body.cur_col = 3; + pane.body.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); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.body.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); + try std.testing.expectEqual(@as(i32, @intCast(width * 2 + 3)), pane.body.cur_col); p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.body.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); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(width * 3 + 3)), pane.body.cur_col); // Plain j/k are untouched: one FILE line each, whatever the body drew. - pane.cur_col = 3; - pane.sticky_col = -1; + pane.body.cur_col = 3; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.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; + pane.body.cur_row = 0; + pane.body.cur_col = @intCast(long.len - 1); + pane.body.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; + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); + pane.body.cur_col = 0; + pane.body.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))); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expect(pane.body.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; + pane.body.cur_row = 0; + pane.body.cur_col = 3; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); } test "flat text movement and selection replay need no scratch rows" { @@ -685,39 +687,39 @@ test "flat text movement and selection replay need no scratch rows" { p.scratch.deinit(); p.scratch = .init(denied.allocator()); - pane.cur_row = 4094; - handleNormal(p, pane, .{ .cp = 'l' }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - handleNormal(p, pane, .{ .cp = 'j' }); - try std.testing.expectEqual(@as(i32, 4095), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - handleNormal(p, pane, .{ .cp = 'h' }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); - executeNormalAction(p, 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.body.cur_row = 4094; + handleNormal(p, &pane.body, .{ .cp = 'l' }); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + handleNormal(p, &pane.body, .{ .cp = 'j' }); + try std.testing.expectEqual(@as(i32, 4095), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + handleNormal(p, &pane.body, .{ .cp = 'h' }); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); + executeNormalAction(p, &pane.body, .{ .textobject = .{ .char = 'w', .around = false } }); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.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 }; - handleNormal(p, pane, .{ .cp = 'l' }); - handleNormal(p, 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); + pane.body.vsel.active = false; + pane.body.cur_row = 1024; + pane.body.cur_col = 0; + pane.body.nsel = 1; + pane.body.sels[0] = .{ .row = 3072, .col = 0, .arow = 3072, .acol = 0 }; + handleNormal(p, &pane.body, .{ .cp = 'l' }); + handleNormal(p, &pane.body, .{ .cp = 'j' }); + try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(i32, 1025), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 3073), pane.body.sels[0].row); + try std.testing.expectEqual(@as(i32, 1), pane.body.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; - executeNormalAction(p, pane, .match_bracket); + pane.body.nsel = 0; + pane.body.cur_col = 4; + executeNormalAction(p, &pane.body, .match_bracket); try std.testing.expect(denied.has_induced_failure); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); denied.fail_index = std.math.maxInt(usize); - executeNormalAction(p, pane, .match_bracket); - try std.testing.expectEqual(@as(i32, 8), pane.cur_col); + executeNormalAction(p, &pane.body, .match_bracket); + try std.testing.expectEqual(@as(i32, 8), pane.body.cur_col); } |
