summaryrefslogtreecommitdiff
path: root/src/mouse.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/mouse.zig')
-rw-r--r--src/mouse.zig87
1 files changed, 77 insertions, 10 deletions
diff --git a/src/mouse.zig b/src/mouse.zig
index 1ca53017..5bfd92ee 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -109,20 +109,23 @@ test "a pane's last row of text is no resize handle; the rule over the next pane
try std.testing.expect(nudgeRightSeam(p, 2) == seam + 2);
}
-test "a pane's grip dragged up or down its own column moves its top, down to a tag alone above" {
+test "a pane's grip dragged up or down its own column moves its top, down to the floor of the pane above" {
if (platform == .web) return;
const p = try cornerFixture(std.testing.allocator);
defer p.deinit();
const upper = p.col_panes[1][0];
const lower = p.col_panes[1][1];
const grip_x = p.rects[lower].x;
- // Up, past the pane above's first row: it keeps one row, its tag.
+ // Up, past the pane above's first row: a text pane keeps its tag, a
+ // terminal its tag and two rows (layout.floorRows).
const top = p.rects[upper].y;
+ const above = p.panes[upper].?;
+ const floor: u16 = if (above.isTerminal()) above.tag_rows + layout.min_body_rows else 1;
p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } });
p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = top } });
p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = top } });
p.sync();
- try std.testing.expectEqual(@as(u16, 1), p.rects[upper].h);
+ try std.testing.expectEqual(floor, p.rects[upper].h);
try std.testing.expectEqual(lower, p.col_panes[1][1]);
// Down, inside itself: its top follows, the pane above taking the rows.
const to = p.rects[lower].y + 6;
@@ -385,6 +388,8 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
// A column tag's command acts on that column, empty or not.
p.exec_column = column;
defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
if (word.len > 0) {
if (m.button == config.exec_button) {
const named = std.meta.stringToEnum(Builtin, word);
@@ -709,7 +714,7 @@ fn reportGesture(
text: []const u8,
on_tag: bool,
operand: exec.PointerOperand,
- chorded: bool,
+ chord_arg: ?[]const u8,
) bool {
const scripted = p.panes[id] orelse return false;
if (scripted.fs.readers == 0) return false;
@@ -718,14 +723,29 @@ fn reportGesture(
(if (on_tag) .tag_look else .body_look)
else
(if (on_tag) .tag_exec else .body_exec);
- const named = std.meta.stringToEnum(Builtin, exec.commandText(text)) != null;
+ const named = exec.namesBuiltin(text);
const range = gestureRange(p, id, text, on_tag, operand);
+ // No flag 2: the record already carries the word a click took, its
+ // range and text, and in acme flag 2 promises a second record with them
+ // (editors/acme/exec.c:154-183, look.c:42-68), which none follows here.
var flag: u32 = if (named) ctlfs.events.flag_builtin else 0;
- if (range.q0 == range.q1 and text.len > 0) flag |= ctlfs.events.flag_expansion;
if (is_look) {
if (!named and std.mem.indexOfAny(u8, text, "/.:") != null) flag |= ctlfs.events.flag_filename;
- } else if (chorded) flag |= ctlfs.events.flag_chorded;
- return ctlfs.events.noteAction(p, id, action, range.q0, range.q1, flag, text);
+ } else if (chord_arg != null) flag |= ctlfs.events.flag_chorded;
+ if (!ctlfs.events.noteAction(p, id, action, range.q0, range.q1, flag, text)) return false;
+ // A chorded exec's argument follows as two records, its text and where
+ // it came from (`file:#q0,#q1`, empty when that is not known), as acme
+ // sends them (exec.c:182-188); the pane keeps it for the write-back.
+ if (chord_arg) |a| {
+ ctlfs.events.noteFollow(p, id, action, a);
+ var where: [512]u8 = undefined;
+ ctlfs.events.noteFollow(p, id, action, exec.heldSelectionPlace(p, id, &where) orelse "");
+ const pf = &scripted.fs;
+ if (pf.chord_arg) |old| p.gpa.free(old);
+ pf.chord_arg = p.gpa.dupe(u8, a) catch null;
+ pf.chord_at = range;
+ }
+ return true;
}
fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: exec.PointerOperand) ctlfs.pane.State.Range {
@@ -788,7 +808,7 @@ fn dispatchPointerBuiltin(
// A click that takes no text does nothing, and tells an event reader
// nothing, as acme's look3 and execute return on an empty expansion.
const operand = text orelse return;
- if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return;
+ if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg)) return;
// A middle click on a body is the exec a bound REPL may take.
exec.runBuiltinFrom(p, cmd, id, "", exec.withArg(p, operand, arg), gesture != null and !gesture.?.on_tag);
}
@@ -1017,7 +1037,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
if (line >= 0) {
pane.sel[b].state = .none;
var buf: [48]u8 = undefined;
- const target = std.fmt.bufPrint(&buf, "@p{d}:{d}", .{ s.id, @as(usize, @intCast(line)) + 1 }) catch return;
+ const target = std.fmt.bufPrint(&buf, "@p{d}:{d}", .{ pane.serial, @as(usize, @intCast(line)) + 1 }) catch return;
exec.runBuiltin(p, config.look_cmd, s.id, "", target);
return;
}
@@ -1633,3 +1653,50 @@ test "mouse thumb buttons navigate once and clear selection gestures" {
try std.testing.expect(!pane.body.vsel.active);
try std.testing.expectEqual(.none, p.drag);
}
+
+test "a chorded exec an event reader holds is told with its argument, and written back runs with it" {
+ const th = @import("ninep/testing.zig");
+ const p = try th.withFile(std.testing.allocator, "Msg said\narg-text\n");
+ defer p.deinit();
+ const serial = p.panes[0].?.serial;
+ const event = ctlfs.Node.of(serial, .event);
+ const h = th.call(p, .{ .tag = 1, .op = .open, .node = event, .omode = 2 }).reply.handle;
+ // The argument: a selection held in the pane (its second line).
+ ctlfs.pane.setDot(p.panes[0].?, .{ .q0 = 9, .q1 = 17 });
+ p.chord_arg = try p.gpa.dupe(u8, "arg-text");
+ dispatchPointerBuiltin(p, 0, config.exec_cmd, "Msg", .{ .on_tag = false, .operand = .{ .row = 0, .col = 1, .text = "Msg" } });
+ var records: [3][]const u8 = undefined;
+ var got: [3][64]u8 = undefined;
+ for (&records, &got) |*r, *g| {
+ const bytes = th.call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 64 }).bytes;
+ @memcpy(g[0..bytes.len], bytes);
+ r.* = g[0..bytes.len];
+ }
+ try std.testing.expect(std.mem.endsWith(u8, records[0], " 9 3 Msg\n")); // builtin|chorded, no expansion record
+ try std.testing.expectEqualStrings("X0 0 0 8 arg-text\n", records[1][1..]);
+ try std.testing.expect(std.mem.endsWith(u8, records[2], "test.txt:#9,#17\n"));
+ // Written back whole, it runs with its argument.
+ var back: [64]u8 = undefined;
+ const line = try std.fmt.bufPrint(&back, "M{s}", .{records[0][1..]});
+ try std.testing.expectEqual(ctlfs.Status.ok, th.call(p, .{ .tag = 3, .op = .write, .node = event, .handle = h, .data = line }).reply.status);
+ const pane = p.panes[0].?;
+ try std.testing.expectEqualStrings("arg-text", pane.msg[0..pane.msg_len]);
+ // All three records written back as read, in one write or in three:
+ // the two after the first are its argument and where it came from,
+ // never commands of their own.
+ for ([_]bool{ true, false }) |together| {
+ pane.msg_len = 0;
+ var all: [256]u8 = undefined;
+ var w = std.Io.Writer.fixed(&all);
+ for (records) |r| try w.print("M{s}", .{r[1..]});
+ if (together) {
+ _ = th.call(p, .{ .tag = 5, .op = .write, .node = event, .handle = h, .data = w.buffered() });
+ } else for (records) |r| {
+ var one: [80]u8 = undefined;
+ _ = th.call(p, .{ .tag = 6, .op = .write, .node = event, .handle = h, .data = try std.fmt.bufPrint(&one, "M{s}", .{r[1..]}) });
+ }
+ try std.testing.expectEqualStrings("arg-text", pane.msg[0..pane.msg_len]);
+ try std.testing.expect(!th.logHas(p, "run "));
+ }
+ _ = th.call(p, .{ .tag = 4, .op = .release, .node = event, .handle = h });
+}