diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 727 |
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 |
