const std = @import("std"); const editor_state = @import("editor_state.zig"); const registered_entities = @import("registered_entities.zig"); const Decision = union(enum) { reset, move: struct { raw_offset: usize, preferred_column: usize, }, }; fn decideTarget( input: []const u8, entities: *const registered_entities.State, target_raw_offset: ?usize, preferred_column: usize, ) Decision { const raw_offset = target_raw_offset orelse return .reset; if (entities.entityContaining(input, raw_offset) != null) return .reset; return .{ .move = .{ .raw_offset = raw_offset, .preferred_column = preferred_column, } }; } /// Owns the sticky display-column intent for composer vertical navigation. /// Terminal layout adapters provide target offsets and measured columns. pub const State = struct { preferred_column: ?usize = null, pub fn reset(self: *State) void { self.* = .{}; } pub fn preferredColumn(self: State) ?usize { return self.preferred_column; } pub fn applyTarget( self: *State, edit: *editor_state.State, entities: *const registered_entities.State, target_raw_offset: ?usize, preferred_column: usize, ) bool { return self.applyTargetWithSelection( edit, entities, target_raw_offset, preferred_column, false, ); } pub fn applyTargetWithSelection( self: *State, edit: *editor_state.State, entities: *const registered_entities.State, target_raw_offset: ?usize, preferred_column: usize, extend_selection: bool, ) bool { switch (decideTarget( edit.input.items, entities, target_raw_offset, preferred_column, )) { .reset => { self.reset(); return false; }, .move => |target| { _ = edit.moveCursorTo(target.raw_offset, extend_selection); self.preferred_column = target.preferred_column; return true; }, } } }; test "vertical navigation applies a legal target and clears selection" { const alloc = std.testing.allocator; var edit: editor_state.State = .{}; defer edit.deinit(alloc); try edit.setText(alloc, "abcdef"); _ = edit.beginSelection(1); _ = edit.extendSelection(4); var entities: registered_entities.State = .{}; defer entities.deinit(alloc); var state: State = .{}; try std.testing.expect(state.applyTarget(&edit, &entities, 5, 3)); try std.testing.expectEqual(@as(usize, 5), edit.cursor); try std.testing.expect(edit.selectionRange() == null); try std.testing.expectEqual(@as(?usize, 3), state.preferredColumn()); } test "vertical navigation reset decisions preserve the editor" { const alloc = std.testing.allocator; var edit: editor_state.State = .{}; defer edit.deinit(alloc); try edit.setText(alloc, "abcdef"); _ = edit.beginSelection(1); _ = edit.extendSelection(4); var entities: registered_entities.State = .{}; defer entities.deinit(alloc); var state: State = .{ .preferred_column = 4 }; try std.testing.expect(!state.applyTarget(&edit, &entities, null, 0)); try std.testing.expectEqual(@as(usize, 4), edit.cursor); try std.testing.expectEqual( editor_state.SelectionRange{ .start = 1, .end = 4 }, edit.selectionRange().?, ); try std.testing.expectEqual(@as(?usize, null), state.preferredColumn()); } test "vertical navigation rejects targets inside registered entities" { const alloc = std.testing.allocator; var edit: editor_state.State = .{}; defer edit.deinit(alloc); try edit.setText(alloc, "[Image #1]"); _ = edit.setCursor(0); var entities: registered_entities.State = .{}; defer entities.deinit(alloc); try entities.image_tokens.append(alloc, .{ .id = 1, .span = .{ .raw_start = 0, .raw_end = edit.input.items.len }, }); var state: State = .{ .preferred_column = 2 }; try std.testing.expect(!state.applyTarget(&edit, &entities, 3, 3)); try std.testing.expectEqual(@as(usize, 0), edit.cursor); try std.testing.expectEqual(@as(?usize, null), state.preferredColumn()); } test "vertical navigation extends from the original cursor when requested" { const alloc = std.testing.allocator; var edit: editor_state.State = .{}; defer edit.deinit(alloc); try edit.setText(alloc, "abcdef"); _ = edit.setCursor(2); var entities: registered_entities.State = .{}; defer entities.deinit(alloc); var state: State = .{}; try std.testing.expect(state.applyTargetWithSelection(&edit, &entities, 5, 3, true)); try std.testing.expectEqual( editor_state.SelectionRange{ .start = 2, .end = 5 }, edit.selectionRange().?, ); try std.testing.expectEqual(@as(?usize, 3), state.preferredColumn()); }