diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 168 |
1 files changed, 90 insertions, 78 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 15b2c49b..a29973e7 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1824,10 +1824,10 @@ test "Save on a scratch asks for a path in its inherited dir and makes it a file // Save on a scratch arms a PATH input, prefilled with the inherited dir try std.testing.expect(p.executeBuiltinLine(id, "Save")); try std.testing.expect(np.prompt == .save); - try std.testing.expect(std.mem.endsWith(u8, np.tagSlice(), " Save /tmp/pardes-save-dir/")); + try std.testing.expectEqualStrings(" Save /tmp/pardes-save-dir/", np.promptText().?); // typing the filename and submitting converts it into an ordinary file - try std.testing.expect(np.appendTag("note.txt")); + edit.insertKey(p, &np.input, .{ .cp = 'n', .text = "note.txt" }); exec.submitSave(p, id); try std.testing.expect(np.file.?.output == null); try std.testing.expectEqualStrings("/tmp/pardes-save-dir/note.txt", np.file.?.path); @@ -1852,7 +1852,7 @@ test "Save on a terminal writes its plaintext scrollback and stays a terminal" { try std.testing.expect(p.executeBuiltinLine(0, "Save")); try std.testing.expect(pane.prompt == .save); - try std.testing.expect(pane.appendTag("log.txt")); + edit.insertKey(p, &pane.input, .{ .cp = 'l', .text = "log.txt" }); exec.submitSave(p, 0); // the pane is untouched: still a terminal, no file behind it @@ -1901,7 +1901,7 @@ test "Save on an output buffer writes its rows out and leaves the buffer alone" try std.testing.expectEqualStrings(file_pane_tail, Pardes.curTail(out)); try std.testing.expect(p.executeBuiltinLine(id, "Save")); try std.testing.expect(out.prompt == .save); - try std.testing.expect(out.appendTag("help.txt")); + edit.insertKey(p, &out.input, .{ .cp = 'h', .text = "help.txt" }); exec.submitSave(p, id); // the buffer itself is untouched: same origin, same name, still refillable @@ -4109,6 +4109,7 @@ pub const Pardes = struct { if (comptime pdf_enabled) if (pane.pdf) |*pv| panes.Pdf.deinitPane(p, pane, pv); if (pane.file) |*file| panes.File.deinit(p, pane, file); if (pane.ovl) |o| p.gpa.free(o.text); + if (pane.input.own) |text| p.gpa.free(text); for (pane.body.ed_undo[0..pane.body.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); for (pane.body.ed_redo[0..pane.body.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); panes.Terminal.deinitEmulator(pane, p.gpa); @@ -4836,7 +4837,7 @@ pub const Pardes = struct { } pub fn enterTagEdit(p: *Pardes, pane: *Pane, col: i32) void { - if (pane.tag_edit and pane.prompt == .none) pane.tag_saved_col = pane.tag_col; + if (pane.tag_edit) pane.tag_saved_col = pane.tag_col; const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len); p.seedTail(pane); if (!pane.tag_init) return; @@ -4869,7 +4870,7 @@ pub const Pardes = struct { } pub fn exitTagEdit(pane: *Pane) void { - if (pane.tag_edit and pane.prompt == .none) pane.tag_saved_col = pane.tag_col; + if (pane.tag_edit) pane.tag_saved_col = pane.tag_col; if (pane.isTerminal()) { if (pane.tag_edit) pane.body.mode = pane.tag_mode; } else pane.body.mode = .normal; @@ -4877,8 +4878,6 @@ pub const Pardes = struct { pane.tag_sel = false; pane.tag_name_len = null; pane.tag_scroll = 0; - pane.prompt = .none; // an abandoned prompt stays as tag text - pane.nsel_snap = 0; // ...and its s/S preview keeps whatever it previewed } pub fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } { @@ -5273,6 +5272,7 @@ pub const Pardes = struct { p.active = id; p.exitHeader(); if (pane.tag_edit) exitTagEdit(pane); + pane.closePrompt(); pane.body.mode = previous; switch (previous) { .tty => { @@ -5294,6 +5294,7 @@ pub const Pardes = struct { p.active = id; p.exitHeader(); if (pane.tag_edit) exitTagEdit(pane); + pane.closePrompt(); if (pane.body.mode == .tty) { pane.body.mode = .normal; pane.body.normal.clear(); @@ -5351,7 +5352,7 @@ pub const Pardes = struct { } // insert mode owns Ctrl-w (delete word backward, helix); the focus // prefix applies outside raw tty mode - if (hit(key, config.window_prefix) and pane.body.mode != .insert) { + if (hit(key, config.window_prefix) and pane.body.mode != .insert and pane.prompt == .none) { p.ctrl_w_pending = true; return; } @@ -5384,34 +5385,29 @@ pub const Pardes = struct { if (pane.isTerminal() and tty_toggle) { return p.toggleTty(p.active); } - if (pane.prompt != .none and (hit(key, config.search_submit) or hit(key, config.escape))) { - const submit = hit(key, config.search_submit); - switch (pane.prompt) { - .pipe => |pipe| { - if (submit) Pipe.submitPipe(p, p.active); - pane.tag_tail_len = @min(pipe.at, pane.tag_tail_len); - }, - .save => |at| { - if (submit) exec.submitSave(p, p.active) else pane.tag_tail_len = @min(at, pane.tag_tail_len); - }, - .search => |at| { - if (submit) + if (pane.prompt != .none) { + if (hit(key, config.search_submit) or hit(key, config.escape)) { + const submit = hit(key, config.search_submit); + switch (pane.prompt) { + .pipe => if (submit) Pipe.submitPipe(p, p.active), + .save => if (submit) exec.submitSave(p, p.active), + .search => if (submit) look.submitSearch(p, p.active) else if (normal.selRegexArmed(pane)) |_| - normal.applySelRegex(p, pane, "", false); - pane.tag_tail_len = @min(at, pane.tag_tail_len); - }, - .none, .del_side => unreachable, + normal.applySelRegex(p, pane, "", false), + .none, .del_side => unreachable, + } + pane.closePrompt(); + pane.body.mode = .normal; + pane.body.normal.clear(); + return; } - exitTagEdit(pane); - pane.body.mode = .normal; - pane.body.normal.clear(); + edit.handleInsert(p, &pane.input, key); + if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split); return; } if (pane.tag_edit) { if (pane.body.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key); - const pn = p.panes[p.active] orelse return; - if (normal.selRegexArmed(pn)) |a| normal.applySelRegex(p, pn, a.pat, a.split); return; } if (pane.body.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) { @@ -5506,33 +5502,24 @@ pub const Pardes = struct { break :pipe Pipe.pipeMarker(how); }, }; - p.seedTail(pane); - if (!pane.tag_init) return; + // Save asks for a path, so its answer starts out as the pane's own + // directory for the name to be typed onto. const dir = if (request == .save) paneDir(pane) else ""; - const slash = request == .save and (dir.len == 0 or dir[dir.len - 1] != '/'); - var room = pane.tag_tail.len - pane.tag_tail_len; - if (marker.len > room) return; - room -= marker.len; - if (dir.len > room) return; - room -= dir.len; - if (slash and room == 0) return; - const prefix_len = (p.tagPrefix(pane) catch return).len; - const prompt_at: u16 = @intCast(pane.tag_tail_len); - _ = pane.appendTag(marker); - if (request == .save) { - _ = pane.appendTag(dir); - if (slash) _ = pane.appendTag("/"); - } + const slash: []const u8 = if (request == .save and (dir.len == 0 or dir[dir.len - 1] != '/')) "/" else ""; + const answer = std.mem.concat(p.gpa, u8, &.{ marker, dir, slash }) catch return; + if (pane.tag_edit) exitTagEdit(pane); + // Not closePrompt: an s/S snapshot was taken for this very prompt. + if (pane.input.own) |old| p.gpa.free(old); pane.prompt = switch (request) { - .search => .{ .search = prompt_at }, - .pipe => |how| .{ .pipe = .{ .at = prompt_at, .how = how } }, - .save => .{ .save = prompt_at }, + .search => .search, + .pipe => |how| .{ .pipe = how }, + .save => .save, }; - pane.tag_edit = true; - pane.tag_sel = false; - pane.body.mode = .insert; + pane.input = .{ .what = .input, .own = answer, .mode = .insert, .cur_pinned = true, .cur_col = @intCast(answer.len) }; + // Whatever mode the body was in, it is in normal mode once the + // prompt is answered or dropped. + pane.body.mode = .normal; pane.body.normal.clear(); - pane.tag_col = @intCast(prefix_len + pane.tag_tail_len); } pub const SavePath = Effect.Buf(effect_path_cap); @@ -6066,7 +6053,7 @@ pub const Pardes = struct { p.trackJump(); for (&p.panes, 0..) |*slot, id| { const pane = slot.* orelse continue; - if (pane.tag_edit and pane.prompt == .none and (id != p.active or p.topbar_col != null)) + if (pane.tag_edit and (id != p.active or p.topbar_col != null)) exitTagEdit(pane); if (comptime terminal_panes) if (pane.terminal) |state| { var off: u16 = 0; @@ -6279,18 +6266,14 @@ pub const Pardes = struct { // the detached wire will encode -- which drops every frame for // as long as the prompt is up. const printed = Messages.printRight(s, cx, row, chip -| 1, text, style); - if (kind != .prompt or id != p.active) continue; - const at = pane.promptAt() orelse continue; - const prompt0 = (p.tagPrefix(pane) catch continue).len + at; - const col = @as(usize, pane.tag_col); - if (col < prompt0) continue; + if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue; // The cursor follows the text to wherever it landed; a caret // in the part a narrow band dropped has nowhere to be. - const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]); + const prompt_col = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); if (prompt_col < printed.dropped) continue; const caret = printed.left + (prompt_col - printed.dropped); if (caret < tx + tw) - s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.body.mode == .insert }; + s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.input.mode == .insert }; } } @@ -6652,7 +6635,7 @@ pub const Pardes = struct { layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - TAG_TEXT_INSET, .h = BOX_H }; layer.cols = columns; layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg; - layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null; + layer.cursor = if (p.topbar_col == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null; } var text_buf: [1200]u8 = undefined; // With the workspace tag row handed to native chrome there is no @@ -6711,14 +6694,11 @@ pub const Pardes = struct { p.surface.tag_layers[layer_index].fade = fade; // The caret rides the band, where the prompt's glyphs are, // at the same place in the text the grid pass puts it. - if (kind != .prompt or id != p.active) continue; - const at = pane.promptAt() orelse continue; - const prompt0 = (p.tagPrefix(pane) catch continue).len + at; - if (pane.tag_col < prompt0) continue; - const typed = panes.File.displayWidth(text[0..@min(pane.tag_col - prompt0, text.len)]); + if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue; + const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); const dropped = shown - panes.File.displayWidth(kept); if (typed < dropped) continue; - p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.body.mode == .insert }; + p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.input.mode == .insert }; } } if (p.columnBarHeight() > 0) for (0..p.ncol) |column| { @@ -6877,8 +6857,7 @@ pub const Pardes = struct { .font_role = .tagline, }); const tag = try p.tagText(arena, pane); - const prompt_at = pane.promptAt(); - if (active and pane.tag_edit and prompt_at == null) { + if (active and pane.tag_edit) { const caret = panes.File.rawDisplayCol(tag, pane.tag_col); var scroll: usize = pane.tag_scroll; if (caret < scroll) scroll = caret; @@ -6890,16 +6869,12 @@ pub const Pardes = struct { scroll = panes.File.rawDisplayCol(tag, raw); } pane.tag_scroll = @intCast(scroll); - } else if (!pane.tag_edit or prompt_at != null) pane.tag_scroll = 0; - const tag_cut = if (prompt_at) |at| - @min(tag.len, tag.len - @min(tag.len, pane.tag_tail_len) + at) - else - tag.len; - const tag_start = @min(tag_cut, panes.File.rawAtDisplay(tag, pane.tag_scroll)); - _ = s.print(tx, tag_y, tw, tag[tag_start..tag_cut], tag_style); + } else pane.tag_scroll = 0; + const tag_start = panes.File.rawAtDisplay(tag, pane.tag_scroll); + _ = s.print(tx, tag_y, tw, tag[tag_start..], tag_style); if (tagTintRange(pane, tag, tag.len - curTail(pane).len)) |name| { const lo = @max(name.lo, tag_start); - const hi = @min(name.hi, tag_cut); + const hi = @min(name.hi, tag.len); if (lo < hi) { var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll; const end = panes.File.rawDisplayCol(tag, hi) -| pane.tag_scroll; @@ -7204,6 +7179,43 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch for (pane.sel) |selection| try std.testing.expect(selection.state == .none); } +test "a prompt is typed on its own line in the notice band and leaves the tag row alone" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + const pane = try p.setTestFile("one two\n"); + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const tag_before = try std.testing.allocator.dupe(Cell, p.surface.tag_layers[0].cells); + defer std.testing.allocator.free(tag_before); + + p.update(.{ .key = .{ .cp = '/' } }); + p.update(.{ .key = .{ .cp = 't', .text = "tw" } }); + p.update(.{ .key = .{ .cp = Key.left } }); + try std.testing.expect(pane.prompt == .search and !pane.tag_edit); + try std.testing.expectEqualStrings(config.search_marker ++ "tw", pane.promptText().?); + _ = frame.reset(.retain_capacity); + _ = try p.render(frame.allocator()); + // The tagline is exactly what it was: nothing of the prompt is in it. + try std.testing.expectEqual(tag_before.len, p.surface.tag_layers[0].cells.len); + for (tag_before, p.surface.tag_layers[0].cells) |was, is| try std.testing.expect(was.visuallyEqual(&is)); + try std.testing.expect(p.surface.tag_layers[0].cursor == null); + // The band carries the answer and the caret, one cell back from its end. + const band = for (p.surface.tagLayers()) |*layer| { + if (layer.cols != 0 and layer.kind == .notice) break layer; + } else return error.NoPromptBand; + const caret = band.cursor orelse return error.MissingBandCaret; + try std.testing.expectEqualStrings("t", band.cells[caret.col - 1].grapheme()); + try std.testing.expectEqualStrings("w", band.cells[caret.col].grapheme()); + const grid = p.surface.cursor orelse return error.MissingPromptCursor; + try std.testing.expectEqual(p.bodyTop(p.rects[0]), grid.y); + + // Submitting searches for what was typed and gives the prompt its line back. + p.update(.{ .key = .{ .cp = Key.enter } }); + try std.testing.expect(pane.prompt == .none and pane.input.own == null); +} + test "a stillborn pane gives its column to the first pane that joins it" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 }); |
