diff options
Diffstat (limited to 'src/edit.zig')
| -rw-r--r-- | src/edit.zig | 116 |
1 files changed, 75 insertions, 41 deletions
diff --git a/src/edit.zig b/src/edit.zig index f1b381d6..5249a64c 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -84,12 +84,23 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { const pane = p.panes[id] orelse return; if (req) |r| if (pane.serial != r.serial) return; p.active = id; + if (pane.prompt != .none and pane.prompt != .del_side) { + // an answer is one line: a newline in it would only be submitting it + const line = p.scratch.allocator().alloc(u8, bytes.len) catch return; + var n: usize = 0; + for (bytes) |byte| if (byte != '\n' and byte != '\r') { + line[n] = byte; + n += 1; + }; + if (n > 0) insertKey(p, &pane.input, .{ .cp = line[0], .text = line[0..n] }); + if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split); + return; + } if (pane.tag_edit) { const text = p.tagText(p.scratch.allocator(), pane) catch return; const range: @TypeOf(Pardes.tagSelBounds(pane)) = if (pane.tag_sel) Pardes.tagSelBounds(pane) else .{ .lo = pane.tag_col, .hi = pane.tag_col }; const hi = if (pane.tag_sel) modal.nextGrapheme(text, range.hi) else range.hi; _ = p.replaceTagText(pane, range.lo, hi, bytes); - if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split); return; } if (pane.isTerminal() and pane.body.mode == .tty) return typeToTty(p, id, pane, bytes); @@ -274,7 +285,7 @@ pub fn exitInsert(p: *Pardes, t: *Text) void { t.cur_col = @intCast(bc.col); t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); for (t.sels[0..t.nsel]) |*s| { const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) }); const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap); @@ -288,8 +299,15 @@ pub fn exitInsert(p: *Pardes, t: *Text) void { } pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { - const pane = t.pane(); const arena = p.scratch.allocator(); + if (t.what != .body) { + const text = t.own orelse ""; + const lines = try arena.alloc([]const u8, std.mem.count(u8, text, "\n") + 1); + var it = std.mem.splitScalar(u8, text, '\n'); + for (lines) |*line| line.* = it.next().?; + return lines; + } + const pane = t.pane(); if (pane.file) |*f| return panes.File.cursorLines(arena, pane, f); if (pane.hasPdf()) { if (comptime pdf_enabled) return panes.Pdf.textLines(&pane.pdf.?, p.pdf_gpa, arena); @@ -299,9 +317,8 @@ pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { } pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_col: i32) i32 { - const pane = t.pane(); - if (pane.file != null) - return panes.File.byteAtRowDisplay(pane, row, from_raw, display_col); + if (t.what == .body and t.pane().file != null) + return panes.File.byteAtRowDisplay(t.pane(), row, from_raw, display_col); const lines = paneCursorLines(p, t) catch return @max(0, from_raw + display_col); const local = row; if (local < 0 or @as(usize, @intCast(local)) >= lines.len) @@ -316,14 +333,20 @@ pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_ /// Freeze the live terminal cursor into the modal coordinate space. The /// emulator reports screen cells; editing state stores UTF-8 byte offsets. pub fn pinPaneCursor(p: *Pardes, t: *Text) void { - const pane = t.pane(); if (t.cur_pinned) return; + // Only a body has a live cursor of someone else's to freeze. + if (t.what != .body) { + t.cur_pinned = true; + return; + } + const pane = t.pane(); pane.pinCursor(); if (pane.file == null and !pane.hasPdf()) t.cur_col = paneByteAtDisplay(p, t, t.cur_row, 0, t.cur_col); } pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { + if (t.what != .body) return t.own orelse ""; const pane = t.pane(); if (pane.file) |f| return f.content; if (pane.hasPdf()) { @@ -335,13 +358,16 @@ pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { } pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize { + if (t.what != .body) return 0; const pane = t.pane(); if (pane.file == null) return 0; return panes.File.wrapWidth(pane, p.settings.wrap); } -/// Route shared edit operations to a file's content or a terminal overlay. +/// Route shared edit operations to the characters `t` edits: its own, or +/// for a body its file's content or its terminal's overlay. fn editText(p: *Pardes, t: *Text, lo: i32, hi: i32, col: i32) ?panes.EditText { + if (t.what != .body) return .{ .text = t.own orelse &.{}, .row0 = 0 }; const pane = t.pane(); if (pane.file) |f| return .{ .text = f.content, .row0 = 0 }; if (pane.image != null or pane.hasPdf()) return null; @@ -358,6 +384,11 @@ fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText { /// install a rewritten editable text (frees the old one) fn setEditText(p: *Pardes, t: *Text, new: []u8) void { + if (t.what != .body) { + if (t.own) |old| p.gpa.free(old); + t.own = new; + return; + } const pane = t.pane(); if (pane.file) |*f| return panes.File.setContent(p, f, new); panes.Terminal.setEditText(p, pane, new); @@ -368,6 +399,7 @@ pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_a /// Files insert only inside existing rows and at grapheme boundaries. /// Terminal overlays can materialize virtual rows/columns, so keep those. pub fn clampFileCursor(t: *Text) bool { + if (t.what != .body) return false; const pane = t.pane(); const file = if (pane.file) |*file| file else return false; const old_row = t.cur_row; @@ -380,8 +412,7 @@ pub fn clampFileCursor(t: *Text) bool { } pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { - const pane = t.pane(); - if (pane.hasPdf()) return; + if (t.what == .body and t.pane().hasPdf()) return; pinPaneCursor(p, t); _ = clampFileCursor(t); // snapshot once per insert session (WITH the pre-insert selection) so @@ -478,7 +509,7 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { t.mode = .insert; t.msel.active = false; t.normal.clear(); - pane.ensureCursorVisible(); + t.show(); } pub fn handleInsert(p: *Pardes, t: *Text, key: Key) void { @@ -487,7 +518,6 @@ pub fn handleInsert(p: *Pardes, t: *Text, key: Key) void { } pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { - const pane = t.pane(); // helix aliases: normalize to the base key and re-dispatch if (hit(key, config.insert_backspace_alias)) return insertKey(p, t, .{ .cp = Key.backspace }); if (hit(key, config.insert_enter_alias)) return insertKey(p, t, .{ .cp = Key.enter }); @@ -507,7 +537,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { else => cur0, }; t.fromModalCursor(nc); - pane.ensureCursorVisible(); + t.show(); return; }, Key.page_up, Key.page_down => { @@ -515,14 +545,15 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { // scroll + cursor snap as normal mode const flat = flatSurface(p, t) catch return; const range = t.primaryRange(flat, 0); - const step: i32 = @intCast(@max(1, pane.rows)); + if (t.what != .body) return; + const step: i32 = @intCast(@max(1, t.pane().rows)); t.scrollViewMove(flat, range, if (key.cp == Key.page_down) step else -step); return; }, Key.home => { t.cur_col = 0; t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; }, Key.end => { @@ -531,7 +562,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { const cur0 = t.toModalCursor(); t.cur_col = @intCast(panes.File.textLine(t, text, cur0.row).len); t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; }, else => {}, @@ -549,7 +580,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { } else t.cur_col += 1; } t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; } // helix insert-mode kills (word/line; deleteSpan is exclusive) @@ -569,7 +600,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { t.cur_row = @as(i32, @intCast(fc.row)) + e2.row0; t.cur_col = @intCast(fc.col); t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; } if (hit(key, config.delete_word_forward)) { @@ -592,7 +623,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { setEditText(p, t, new); t.cur_col = 0; t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; } if (hit(key, config.kill_to_line_end)) { @@ -615,7 +646,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { t.cur_row += 1; t.cur_col = @intCast(indent); t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); }, Key.backspace => { if (t.cur_col > 0) { @@ -625,7 +656,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { setEditText(p, t, new); t.cur_col = @intCast(prev); t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); return; } // at col 0: eat the newline above (join up) — a terminal grows @@ -640,7 +671,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { t.cur_row -= 1; t.cur_col = @intCast(prevlen); t.cur_pinned = true; - pane.ensureCursorVisible(); + t.show(); }, Key.delete => { // delete forward; at line end eat the newline (join), which on @@ -661,7 +692,8 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { Key.tab => { const ln = modal.lineSlice(text, c.row); if (!p.multi_on and c.col > 0 and c.col <= ln.len and ln[c.col - 1] == '.') dot: { - const f = pane.file orelse break :dot; + if (t.what != .body) break :dot; + const f = t.pane().file orelse break :dot; if (f.output != null or !lsp.speaks(f.path)) break :dot; const seq = p.lsp_seq; p.lspRequest(p.active, .completion, ""); @@ -684,7 +716,7 @@ pub fn insertTab(p: *Pardes, t: *Text) void { setEditText(p, t, new); t.cur_col += @intCast(pad); t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); } const Bounds = struct { lo_row: i32, lo_col: i32, hi_row: i32, hi_col: i32 }; @@ -779,7 +811,7 @@ pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void { t.cur_col = 0; t.msel.active = false; t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); } else { pushUndo(p, t); const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return; @@ -799,12 +831,11 @@ pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void { } pub fn normalYank(p: *Pardes, t: *Text) void { - const pane = t.pane(); t.select = false; - if (pane.rawPointerText(pane.sel[sel_slot])) |text| { + if (t.what == .body) if (t.pane().rawPointerText(t.pane().sel[sel_slot])) |text| { setYank(p, text); return; - } + }; if (t.vsel.active) { setYank(p, vselText(p, t)); return; @@ -882,11 +913,10 @@ pub fn pasteText(p: *Pardes, t: *Text, y0: []const u8, before: bool, count: usiz } t.msel.active = false; t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); } pub fn normalChange(p: *Pardes, t: *Text) void { - const pane = t.pane(); t.select = false; t.sticky_col = -1; if (t.vsel.active) { @@ -913,7 +943,7 @@ pub fn normalChange(p: *Pardes, t: *Text) void { t.cur_pinned = true; t.mode = .insert; t.normal.clear(); - pane.ensureCursorVisible(); + t.show(); return; } setYank(p, vselText(p, t)); @@ -974,7 +1004,7 @@ pub fn normalChange(p: *Pardes, t: *Text) void { t.cur_pinned = true; t.mode = .insert; t.normal.clear(); - pane.ensureCursorVisible(); + t.show(); } else { pushUndo(p, t); const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return; @@ -1075,7 +1105,7 @@ fn replaceWithText(p: *Pardes, t: *Text, y: []const u8) void { } t.cur_pinned = true; t.sticky_col = -1; - t.pane().ensureCursorVisible(); + t.show(); } /// `~` / `` ` `` / ``Alt-` ``: case-map the selection (or the cursor char), @@ -1174,7 +1204,7 @@ pub fn normalJoin(p: *Pardes, t: *Text) void { t.msel.active = false; t.cur_pinned = true; t.sticky_col = -1; - t.pane().ensureCursorVisible(); + t.show(); } pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void { @@ -1242,7 +1272,7 @@ pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void { t.msel.active = false; t.cur_pinned = true; t.sticky_col = -1; - t.pane().ensureCursorVisible(); + t.show(); } pub fn normalToggleComment(p: *Pardes, t: *Text) void { @@ -1277,7 +1307,7 @@ pub fn normalToggleComment(p: *Pardes, t: *Text) void { } next = @max(next, end); } - const ext = if (t.pane().file) |f| std.fs.path.extension(f.path) else ""; + const ext = if (t.what != .body) "" else if (t.pane().file) |f| std.fs.path.extension(f.path) else ""; var token: []const u8 = config.comment_token_default; lang: for (config.comment_tokens) |row| { for (row.exts) |e| if (std.ascii.eqlIgnoreCase(ext, e)) { @@ -1359,7 +1389,7 @@ pub fn normalAdjustNumber(p: *Pardes, t: *Text, delta: i64) void { t.cur_col = @intCast(cc.col); t.cur_pinned = true; t.sticky_col = -1; - t.pane().ensureCursorVisible(); + t.show(); } /// `]Space` / `[Space`: add blank lines below/above, the cursor staying @@ -1381,7 +1411,7 @@ pub fn addNewline(p: *Pardes, t: *Text, below: bool, cnt: usize) void { t.vsel.row += @intCast(cnt); } t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); } /// pair mapping for m-mode: either bracket names the pair; anything else @@ -1450,7 +1480,7 @@ pub fn surroundAdd(p: *Pardes, t: *Text, ch: u21) void { t.cur_col = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0)); t.cur_pinned = true; t.sticky_col = -1; - t.pane().ensureCursorVisible(); + t.show(); } /// `md<ch>`: delete the enclosing pair's chars; the cursor maps through @@ -1487,7 +1517,7 @@ pub fn surroundDelete(p: *Pardes, t: *Text, ch: u21) void { t.vsel.active = false; t.msel.active = false; t.cur_pinned = true; - t.pane().ensureCursorVisible(); + t.show(); } /// `mr<from><to>`: swap the enclosing <from> pair's chars for <to>'s @@ -1512,20 +1542,24 @@ pub fn surroundReplace(p: *Pardes, t: *Text, from: u21, to: u21) void { } pub fn pushUndo(p: *Pardes, t: *Text) void { - const pane = t.pane(); // one keystroke, one undo step — even when it edited at ten cursors. if (p.multi_on and !p.multi_first) return; + // a prompt's answer is typed once and gone; it keeps no history + if (t.what != .body) return; + const pane = t.pane(); if (pane.file != null) return panes.File.pushUndo(p, pane); panes.Terminal.pushUndo(p, pane); } pub fn doUndo(p: *Pardes, t: *Text) void { + if (t.what != .body) return; const pane = t.pane(); if (pane.file != null) return panes.File.undo(p, pane); panes.Terminal.undo(p, pane); } pub fn doRedo(p: *Pardes, t: *Text) void { + if (t.what != .body) return; const pane = t.pane(); if (pane.file != null) return panes.File.redo(p, pane); panes.Terminal.redo(p, pane); |
