2023-10-06 22:35:27 -07:00
|
|
|
//! Message scrollback
|
2024-03-09 04:04:52 +00:00
|
|
|
use ratatui_image::Image;
|
2022-12-29 18:00:59 -08:00
|
|
|
use regex::Regex;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId};
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
use modalkit_ratatui::{ScrollActions, TerminalCursor, WindowOps};
|
|
|
|
use ratatui::{
|
2023-07-07 22:16:57 -07:00
|
|
|
buffer::Buffer,
|
|
|
|
layout::{Alignment, Rect},
|
|
|
|
style::{Modifier as StyleModifier, Style},
|
2023-07-01 08:58:48 +01:00
|
|
|
text::{Line, Span},
|
2023-07-07 22:16:57 -07:00
|
|
|
widgets::{Paragraph, StatefulWidget, Widget},
|
|
|
|
};
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-02-28 23:00:25 -08:00
|
|
|
use modalkit::actions::{
|
|
|
|
Action,
|
|
|
|
CursorAction,
|
|
|
|
EditAction,
|
|
|
|
Editable,
|
|
|
|
EditorAction,
|
|
|
|
EditorActions,
|
|
|
|
HistoryAction,
|
|
|
|
InsertTextAction,
|
|
|
|
Jumpable,
|
|
|
|
PromptAction,
|
|
|
|
Promptable,
|
|
|
|
Scrollable,
|
|
|
|
Searchable,
|
|
|
|
SelectionAction,
|
2024-03-09 00:47:05 -08:00
|
|
|
WindowAction,
|
2024-02-28 23:00:25 -08:00
|
|
|
};
|
2022-12-29 18:00:59 -08:00
|
|
|
use modalkit::editing::{
|
2023-03-01 18:46:33 -08:00
|
|
|
completion::CompletionList,
|
2024-02-27 21:21:05 -08:00
|
|
|
context::Resolve,
|
2022-12-29 18:00:59 -08:00
|
|
|
cursor::{CursorGroup, CursorState},
|
|
|
|
history::HistoryList,
|
|
|
|
rope::EditRope,
|
|
|
|
store::{RegisterCell, RegisterPutFlags},
|
|
|
|
};
|
2024-02-28 23:00:25 -08:00
|
|
|
use modalkit::errors::{EditError, EditResult, UIError, UIResult};
|
2024-02-27 21:21:05 -08:00
|
|
|
use modalkit::prelude::*;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
use crate::{
|
2023-12-18 20:55:04 -08:00
|
|
|
base::{
|
|
|
|
IambBufferId,
|
2024-03-09 00:47:05 -08:00
|
|
|
IambId,
|
2023-12-18 20:55:04 -08:00
|
|
|
IambInfo,
|
|
|
|
IambResult,
|
|
|
|
Need,
|
|
|
|
ProgramContext,
|
|
|
|
ProgramStore,
|
2024-03-09 00:47:05 -08:00
|
|
|
RoomFetchStatus,
|
2023-12-18 20:55:04 -08:00
|
|
|
RoomFocus,
|
|
|
|
RoomInfo,
|
|
|
|
},
|
2023-01-06 16:56:28 -08:00
|
|
|
config::ApplicationSettings,
|
2023-02-09 17:53:33 -08:00
|
|
|
message::{Message, MessageCursor, MessageKey, Messages},
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
fn no_msgs() -> EditError<IambInfo> {
|
|
|
|
let msg = "No messages to select.";
|
|
|
|
EditError::Failure(msg.to_string())
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn nth_key_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut end = &pos;
|
2024-03-09 00:47:05 -08:00
|
|
|
let iter = thread.range(..=&pos).rev().enumerate();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
for (i, (key, _)) in iter {
|
|
|
|
end = key;
|
|
|
|
|
|
|
|
if i >= n {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
end.clone()
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
|
|
|
nth_key_before(pos, n, thread).into()
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut end = &pos;
|
2024-03-09 00:47:05 -08:00
|
|
|
let iter = thread.range(&pos..).enumerate();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
for (i, (key, _)) in iter {
|
|
|
|
end = key;
|
|
|
|
|
|
|
|
if i >= n {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
end.clone()
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
|
|
|
nth_key_after(pos, n, thread).into()
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> {
|
|
|
|
thread.range(..key).next_back().map(|(_, v)| v)
|
2023-01-29 18:07:00 -08:00
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub struct ScrollbackState {
|
2023-01-12 21:20:32 -08:00
|
|
|
/// The room identifier.
|
2022-12-29 18:00:59 -08:00
|
|
|
room_id: OwnedRoomId,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
/// The buffer identifier used for saving marks, etc.
|
2022-12-29 18:00:59 -08:00
|
|
|
id: IambBufferId,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
/// The currently focused thread in this room.
|
|
|
|
thread: Option<OwnedEventId>,
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
/// The currently selected message in the scrollback.
|
2022-12-29 18:00:59 -08:00
|
|
|
cursor: MessageCursor,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
/// Contextual info about the viewport used during rendering.
|
2022-12-29 18:00:59 -08:00
|
|
|
viewctx: ViewportContext<MessageCursor>,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
/// The jumplist of visited messages.
|
2022-12-29 18:00:59 -08:00
|
|
|
jumped: HistoryList<MessageCursor>,
|
2023-01-12 21:20:32 -08:00
|
|
|
|
|
|
|
/// Whether the full message should be drawn during the next render() call.
|
|
|
|
///
|
|
|
|
/// This is used to ensure that ^E/^Y work nicely when the cursor is currently
|
|
|
|
/// on a multiline message.
|
|
|
|
show_full_on_redraw: bool,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ScrollbackState {
|
2024-03-09 00:47:05 -08:00
|
|
|
pub fn new(room_id: OwnedRoomId, thread: Option<OwnedEventId>) -> ScrollbackState {
|
|
|
|
let id = IambBufferId::Room(room_id.to_owned(), thread.clone(), RoomFocus::Scrollback);
|
2022-12-29 18:00:59 -08:00
|
|
|
let cursor = MessageCursor::default();
|
|
|
|
let viewctx = ViewportContext::default();
|
|
|
|
let jumped = HistoryList::default();
|
2023-01-12 21:20:32 -08:00
|
|
|
let show_full_on_redraw = false;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
ScrollbackState {
|
|
|
|
room_id,
|
|
|
|
id,
|
2024-03-09 00:47:05 -08:00
|
|
|
thread,
|
2023-01-12 21:20:32 -08:00
|
|
|
cursor,
|
|
|
|
viewctx,
|
|
|
|
jumped,
|
|
|
|
show_full_on_redraw,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn goto_latest(&mut self) {
|
|
|
|
self.cursor = MessageCursor::latest();
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the dimensions and placement within the terminal window for this list.
|
|
|
|
pub fn set_term_info(&mut self, area: Rect) {
|
|
|
|
self.viewctx.dimensions = (area.width as usize, area.height as usize);
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
pub fn get_key(&self, info: &mut RoomInfo) -> Option<MessageKey> {
|
|
|
|
self.cursor
|
|
|
|
.timestamp
|
|
|
|
.clone()
|
2024-03-23 19:20:06 -07:00
|
|
|
.or_else(|| self.get_thread(info)?.last_key_value().map(|kv| kv.0.clone()))
|
2023-01-12 21:20:32 -08:00
|
|
|
}
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
pub fn get_mut<'a>(&mut self, info: &'a mut RoomInfo) -> Option<&'a mut Message> {
|
|
|
|
let thread = self.get_thread_mut(info);
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
if let Some(k) = &self.cursor.timestamp {
|
2024-03-23 19:20:06 -07:00
|
|
|
thread.get_mut(k)
|
2023-01-10 19:59:30 -08:00
|
|
|
} else {
|
2024-03-23 19:20:06 -07:00
|
|
|
thread.last_entry().map(|o| o.into_mut())
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
pub fn thread(&self) -> Option<&OwnedEventId> {
|
|
|
|
self.thread.as_ref()
|
|
|
|
}
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
pub fn get_thread<'a>(&self, info: &'a RoomInfo) -> Option<&'a Messages> {
|
|
|
|
info.get_thread(self.thread.as_deref())
|
2024-03-09 00:47:05 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_thread_mut<'a>(&self, info: &'a mut RoomInfo) -> &'a mut Messages {
|
2024-03-23 19:20:06 -07:00
|
|
|
info.get_thread_mut(self.thread.clone())
|
2024-03-09 00:47:05 -08:00
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn messages<'a>(
|
|
|
|
&self,
|
|
|
|
range: EditRange<MessageCursor>,
|
|
|
|
info: &'a RoomInfo,
|
|
|
|
) -> impl Iterator<Item = (&'a MessageKey, &'a Message)> {
|
2024-03-23 19:20:06 -07:00
|
|
|
let Some(thread) = self.get_thread(info) else {
|
|
|
|
return Default::default();
|
|
|
|
};
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let start = range.start.to_key(thread);
|
|
|
|
let end = range.end.to_key(thread);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
let (start, end) = if let (Some(start), Some(end)) = (start, end) {
|
|
|
|
(start, end)
|
2024-03-09 00:47:05 -08:00
|
|
|
} else if let Some((last, _)) = thread.last_key_value() {
|
2022-12-29 18:00:59 -08:00
|
|
|
(last, last)
|
|
|
|
} else {
|
2024-03-09 00:47:05 -08:00
|
|
|
return thread.range(..);
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
if range.inclusive {
|
2024-03-09 00:47:05 -08:00
|
|
|
thread.range(start..=end)
|
2022-12-29 18:00:59 -08:00
|
|
|
} else {
|
2024-03-09 00:47:05 -08:00
|
|
|
thread.range(start..end)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn need_more_messages(&self, info: &RoomInfo) -> bool {
|
|
|
|
match info.fetch_id {
|
|
|
|
// Don't fetch if we've already hit the end of history.
|
|
|
|
RoomFetchStatus::Done => return false,
|
|
|
|
// Fetch at least once if we're viewing a room.
|
|
|
|
RoomFetchStatus::NotStarted => return true,
|
|
|
|
_ => {},
|
|
|
|
}
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
let first_key = self.get_thread(info).and_then(|t| t.first_key_value()).map(|(k, _)| k);
|
2024-03-09 00:47:05 -08:00
|
|
|
let at_top = first_key == self.viewctx.corner.timestamp.as_ref();
|
|
|
|
|
|
|
|
match (at_top, self.thread.as_ref()) {
|
|
|
|
(false, _) => {
|
|
|
|
// Not scrolled to top, don't fetch.
|
|
|
|
false
|
|
|
|
},
|
|
|
|
(true, None) => {
|
|
|
|
// Scrolled to top in non-thread, fetch.
|
|
|
|
true
|
|
|
|
},
|
|
|
|
(true, Some(thread_root)) => {
|
|
|
|
// Scrolled to top in thread, fetch until we have the thread root.
|
|
|
|
//
|
|
|
|
// Typically, if the user has entered a thread view, we should already have fetched
|
|
|
|
// all the way back to the thread root, but it is technically possible via :threads
|
|
|
|
// or when restoring a thread view in the layout at startup to not have the message
|
|
|
|
// yet.
|
|
|
|
!info.keys.contains_key(thread_root)
|
|
|
|
},
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
fn scrollview(
|
|
|
|
&mut self,
|
|
|
|
idx: MessageKey,
|
|
|
|
pos: MovePosition,
|
|
|
|
info: &RoomInfo,
|
|
|
|
settings: &ApplicationSettings,
|
|
|
|
) {
|
2024-03-23 19:20:06 -07:00
|
|
|
let Some(thread) = self.get_thread(info) else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let selidx = if let Some(key) = self.cursor.to_key(thread) {
|
2022-12-29 18:00:59 -08:00
|
|
|
key
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
match pos {
|
|
|
|
MovePosition::Beginning => {
|
|
|
|
self.viewctx.corner = idx.into();
|
|
|
|
},
|
|
|
|
MovePosition::Middle => {
|
|
|
|
let mut lines = 0;
|
|
|
|
let target = self.viewctx.get_height() / 2;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, item) in thread.range(..=&idx).rev() {
|
2022-12-29 18:00:59 -08:00
|
|
|
let sel = selidx == key;
|
2024-03-09 00:47:05 -08:00
|
|
|
let prev = prevmsg(key, thread);
|
2023-01-29 18:07:00 -08:00
|
|
|
let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
if key == &idx {
|
|
|
|
lines += len / 2;
|
|
|
|
} else {
|
|
|
|
lines += len;
|
|
|
|
}
|
|
|
|
|
|
|
|
if lines >= target {
|
|
|
|
// We've moved back far enough.
|
|
|
|
self.viewctx.corner.timestamp = key.clone().into();
|
|
|
|
self.viewctx.corner.text_row = lines - target;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
MovePosition::End => {
|
|
|
|
let mut lines = 0;
|
|
|
|
let target = self.viewctx.get_height();
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, item) in thread.range(..=&idx).rev() {
|
2022-12-29 18:00:59 -08:00
|
|
|
let sel = key == selidx;
|
2024-03-09 00:47:05 -08:00
|
|
|
let prev = prevmsg(key, thread);
|
2023-01-29 18:07:00 -08:00
|
|
|
let len = item.show(prev, sel, &self.viewctx, info, settings).lines.len();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
lines += len;
|
|
|
|
|
|
|
|
if lines >= target {
|
|
|
|
// We've moved back far enough.
|
|
|
|
self.viewctx.corner.timestamp = key.clone().into();
|
|
|
|
self.viewctx.corner.text_row = lines - target;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn jump_changed(&mut self) -> bool {
|
|
|
|
self.jumped.current() != &self.cursor
|
|
|
|
}
|
|
|
|
|
|
|
|
fn push_jump(&mut self) {
|
|
|
|
self.jumped.push(self.cursor.clone());
|
|
|
|
}
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
fn shift_cursor(&mut self, info: &RoomInfo, settings: &ApplicationSettings) {
|
2024-03-23 19:20:06 -07:00
|
|
|
let Some(thread) = self.get_thread(info) else {
|
|
|
|
return;
|
|
|
|
};
|
2024-03-09 00:47:05 -08:00
|
|
|
|
|
|
|
let last_key = if let Some(k) = thread.last_key_value() {
|
2022-12-29 18:00:59 -08:00
|
|
|
k.0
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let corner_key = self.viewctx.corner.timestamp.as_ref().unwrap_or(last_key);
|
|
|
|
|
|
|
|
if self.cursor < self.viewctx.corner {
|
|
|
|
// Cursor is above the viewport; move it inside.
|
|
|
|
self.cursor = corner_key.clone().into();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether the cursor is below the viewport.
|
|
|
|
let mut lines = 0;
|
|
|
|
|
|
|
|
let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key);
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut prev = prevmsg(cursor_key, thread);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (idx, item) in thread.range(corner_key.clone()..) {
|
2022-12-29 18:00:59 -08:00
|
|
|
if idx == cursor_key {
|
|
|
|
// Cursor is already within the viewport.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
lines += item.show(prev, false, &self.viewctx, info, settings).height().max(1);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
if lines >= self.viewctx.get_height() {
|
|
|
|
// We've reached the end of the viewport; move cursor into it.
|
|
|
|
self.cursor = idx.clone().into();
|
|
|
|
break;
|
|
|
|
}
|
2023-01-29 18:07:00 -08:00
|
|
|
|
|
|
|
prev = Some(item);
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn _range_to(&self, cursor: MessageCursor) -> EditRange<MessageCursor> {
|
|
|
|
EditRange::inclusive(self.cursor.clone(), cursor, TargetShape::LineWise)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn movement(
|
|
|
|
&self,
|
|
|
|
pos: MessageKey,
|
|
|
|
movement: &MoveType,
|
|
|
|
count: &Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
info: &RoomInfo,
|
|
|
|
) -> Option<MessageCursor> {
|
|
|
|
let count = ctx.resolve(count);
|
|
|
|
|
|
|
|
match movement {
|
|
|
|
// These movements don't map meaningfully onto the scrollback history.
|
|
|
|
MoveType::BufferByteOffset => None,
|
|
|
|
MoveType::Column(_, _) => None,
|
|
|
|
MoveType::ItemMatch => None,
|
|
|
|
MoveType::LineColumnOffset => None,
|
|
|
|
MoveType::LinePercent => None,
|
|
|
|
MoveType::LinePos(_) => None,
|
|
|
|
MoveType::SentenceBegin(_) => None,
|
|
|
|
MoveType::ScreenFirstWord(_) => None,
|
|
|
|
MoveType::ScreenLinePos(_) => None,
|
|
|
|
MoveType::WordBegin(_, _) => None,
|
|
|
|
MoveType::WordEnd(_, _) => None,
|
|
|
|
|
|
|
|
MoveType::BufferLineOffset => None,
|
|
|
|
MoveType::BufferLinePercent => None,
|
|
|
|
MoveType::BufferPos(MovePosition::Beginning) => {
|
2024-03-23 19:20:06 -07:00
|
|
|
let start = self.get_thread(info)?.first_key_value()?.0.clone();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
Some(start.into())
|
|
|
|
},
|
|
|
|
MoveType::BufferPos(MovePosition::Middle) => None,
|
|
|
|
MoveType::BufferPos(MovePosition::End) => Some(MessageCursor::latest()),
|
|
|
|
MoveType::FinalNonBlank(dir) |
|
|
|
|
MoveType::FirstWord(dir) |
|
|
|
|
MoveType::Line(dir) |
|
|
|
|
MoveType::ScreenLine(dir) |
|
|
|
|
MoveType::ParagraphBegin(dir) |
|
|
|
|
MoveType::SectionBegin(dir) |
|
|
|
|
MoveType::SectionEnd(dir) => {
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info)?;
|
2024-03-09 00:47:05 -08:00
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
match dir {
|
2024-03-09 00:47:05 -08:00
|
|
|
MoveDir1D::Previous => nth_before(pos, count, thread).into(),
|
|
|
|
MoveDir1D::Next => nth_after(pos, count, thread).into(),
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
MoveType::ViewportPos(MovePosition::Beginning) => {
|
|
|
|
return self.viewctx.corner.timestamp.as_ref().map(|k| k.clone().into());
|
|
|
|
},
|
|
|
|
MoveType::ViewportPos(MovePosition::Middle) => {
|
|
|
|
// XXX: Need to calculate an accurate middle position.
|
|
|
|
return None;
|
|
|
|
},
|
|
|
|
MoveType::ViewportPos(MovePosition::End) => {
|
|
|
|
// XXX: Need store to calculate an accurate end position.
|
|
|
|
return None;
|
|
|
|
},
|
|
|
|
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range_of_movement(
|
|
|
|
&self,
|
|
|
|
pos: MessageKey,
|
|
|
|
movement: &MoveType,
|
|
|
|
count: &Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
info: &RoomInfo,
|
|
|
|
) -> Option<EditRange<MessageCursor>> {
|
|
|
|
let other = self.movement(pos.clone(), movement, count, ctx, info)?;
|
|
|
|
|
|
|
|
Some(EditRange::inclusive(pos.into(), other, TargetShape::LineWise))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn range(
|
|
|
|
&self,
|
|
|
|
pos: MessageKey,
|
|
|
|
range: &RangeType,
|
|
|
|
_: bool,
|
|
|
|
count: &Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
info: &RoomInfo,
|
|
|
|
) -> Option<EditRange<MessageCursor>> {
|
|
|
|
match range {
|
|
|
|
RangeType::Bracketed(_, _) => None,
|
|
|
|
RangeType::Item => None,
|
|
|
|
RangeType::Quote(_) => None,
|
|
|
|
RangeType::Word(_) => None,
|
|
|
|
RangeType::XmlTag => None,
|
|
|
|
|
|
|
|
RangeType::Buffer => {
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info)?;
|
2024-03-09 00:47:05 -08:00
|
|
|
let start = thread.first_key_value()?.0.clone();
|
|
|
|
let end = thread.last_key_value()?.0.clone();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
Some(EditRange::inclusive(start.into(), end.into(), TargetShape::LineWise))
|
|
|
|
},
|
|
|
|
RangeType::Line | RangeType::Paragraph | RangeType::Sentence => {
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
let count = ctx.resolve(count);
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut end = &pos;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (i, (key, _)) in thread.range(&pos..).enumerate() {
|
2022-12-29 18:00:59 -08:00
|
|
|
if i >= count {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
end = key;
|
|
|
|
}
|
|
|
|
|
|
|
|
let end = end.clone().into();
|
|
|
|
let start = pos.into();
|
|
|
|
|
|
|
|
Some(EditRange::inclusive(start, end, TargetShape::LineWise))
|
|
|
|
},
|
|
|
|
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn find_message_next(
|
|
|
|
&self,
|
|
|
|
start: MessageKey,
|
|
|
|
needle: &Regex,
|
|
|
|
mut count: usize,
|
|
|
|
info: &RoomInfo,
|
|
|
|
) -> Option<MessageCursor> {
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut mc = None;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, msg) in thread.range(&start..) {
|
2022-12-29 18:00:59 -08:00
|
|
|
if count == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if key == &start {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-01-23 17:08:11 -08:00
|
|
|
if needle.is_match(msg.event.body().as_ref()) {
|
2022-12-29 18:00:59 -08:00
|
|
|
mc = MessageCursor::from(key.clone()).into();
|
|
|
|
count -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mc;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn find_message_prev(
|
|
|
|
&self,
|
|
|
|
end: MessageKey,
|
|
|
|
needle: &Regex,
|
|
|
|
mut count: usize,
|
|
|
|
info: &RoomInfo,
|
2023-12-18 20:55:04 -08:00
|
|
|
) -> (Option<MessageCursor>, bool) {
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut mc = None;
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
let Some(thread) = self.get_thread(info) else {
|
|
|
|
return (None, false);
|
|
|
|
};
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, msg) in thread.range(..&end).rev() {
|
2022-12-29 18:00:59 -08:00
|
|
|
if count == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-01-23 17:08:11 -08:00
|
|
|
if needle.is_match(msg.event.body().as_ref()) {
|
2022-12-29 18:00:59 -08:00
|
|
|
mc = MessageCursor::from(key.clone()).into();
|
|
|
|
count -= 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-18 20:55:04 -08:00
|
|
|
return (mc, count > 0);
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn find_message(
|
|
|
|
&self,
|
|
|
|
key: MessageKey,
|
|
|
|
dir: MoveDir1D,
|
|
|
|
needle: &Regex,
|
|
|
|
count: usize,
|
|
|
|
info: &RoomInfo,
|
2023-12-18 20:55:04 -08:00
|
|
|
) -> (Option<MessageCursor>, bool) {
|
2022-12-29 18:00:59 -08:00
|
|
|
match dir {
|
2023-12-18 20:55:04 -08:00
|
|
|
MoveDir1D::Next => (self.find_message_next(key, needle, count, info), false),
|
|
|
|
MoveDir1D::Previous => self.find_message_prev(key, needle, count, info),
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WindowOps<IambInfo> for ScrollbackState {
|
|
|
|
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
|
|
|
|
Scrollback::new(store).focus(focused).render(area, buf, self)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn dup(&self, _: &mut ProgramStore) -> Self {
|
|
|
|
ScrollbackState {
|
|
|
|
room_id: self.room_id.clone(),
|
|
|
|
id: self.id.clone(),
|
2024-03-09 00:47:05 -08:00
|
|
|
thread: self.thread.clone(),
|
2022-12-29 18:00:59 -08:00
|
|
|
cursor: self.cursor.clone(),
|
|
|
|
viewctx: self.viewctx.clone(),
|
|
|
|
jumped: self.jumped.clone(),
|
2023-01-12 21:20:32 -08:00
|
|
|
show_full_on_redraw: false,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool {
|
|
|
|
// XXX: what's the right closing behaviour for a room?
|
|
|
|
// Should write send a message?
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn write(
|
|
|
|
&mut self,
|
|
|
|
_: Option<&str>,
|
|
|
|
flags: WriteFlags,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> IambResult<EditInfo> {
|
|
|
|
if flags.contains(WriteFlags::FORCE) {
|
|
|
|
Ok(None)
|
|
|
|
} else {
|
|
|
|
Err(EditError::ReadOnly.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_completions(&self) -> Option<CompletionList> {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
fn get_cursor_word(&self, _: &WordStyle) -> Option<String> {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_selected_word(&self) -> Option<String> {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn edit(
|
|
|
|
&mut self,
|
|
|
|
operation: &EditAction,
|
|
|
|
motion: &EditTarget,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
|
|
|
let key = self.cursor.to_key(thread).ok_or_else(no_msgs)?.clone();
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
match operation {
|
|
|
|
EditAction::Motion => {
|
|
|
|
if motion.is_jumping() {
|
2024-03-09 00:47:05 -08:00
|
|
|
self.push_jump();
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
let pos = match motion {
|
|
|
|
EditTarget::CurrentPosition | EditTarget::Selection => {
|
|
|
|
return Ok(None);
|
|
|
|
},
|
|
|
|
EditTarget::Boundary(rt, inc, term, count) => {
|
|
|
|
self.range(key, rt, *inc, count, ctx, info).map(|r| {
|
|
|
|
match term {
|
|
|
|
MoveTerminus::Beginning => r.start,
|
|
|
|
MoveTerminus::End => r.end,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => {
|
|
|
|
let mark = ctx.resolve(mark);
|
|
|
|
let cursor = store.cursors.get_mark(self.id.clone(), mark)?;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
if let Some(mc) = MessageCursor::from_cursor(&cursor, thread) {
|
|
|
|
Some(mc)
|
2022-12-29 18:00:59 -08:00
|
|
|
} else {
|
|
|
|
let msg = "Failed to restore mark";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
EditTarget::Motion(mt, count) => self.movement(key, mt, count, ctx, info),
|
|
|
|
EditTarget::Range(_, _, _) => {
|
|
|
|
return Err(EditError::Failure("Cannot use ranges in a list".to_string()));
|
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Char(_), _, _) => {
|
|
|
|
let msg = "Cannot perform character search in a list";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Regex, flip, count) => {
|
|
|
|
let count = ctx.resolve(count);
|
|
|
|
|
|
|
|
let dir = ctx.get_search_regex_dir();
|
|
|
|
let dir = flip.resolve(&dir);
|
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
let lsearch = store.registers.get(&Register::LastSearch)?;
|
|
|
|
let lsearch = lsearch.value.to_string();
|
|
|
|
let needle = Regex::new(lsearch.as_ref())?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-12-18 20:55:04 -08:00
|
|
|
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
|
|
|
if needs_load {
|
|
|
|
store
|
|
|
|
.application
|
|
|
|
.need_load
|
|
|
|
.insert(self.room_id.clone(), Need::MESSAGES);
|
|
|
|
}
|
|
|
|
mc
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Word(_, _), _, _) => {
|
|
|
|
let msg = "Cannot perform word search in a list";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
|
|
|
|
_ => {
|
2023-01-30 13:51:32 -08:00
|
|
|
let msg = format!("Unknown editing target: {motion:?}");
|
2022-12-29 18:00:59 -08:00
|
|
|
let err = EditError::Unimplemented(msg);
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(pos) = pos {
|
|
|
|
self.cursor = pos;
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
self.show_full_on_redraw = true;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
return Ok(None);
|
|
|
|
},
|
|
|
|
EditAction::Yank => {
|
|
|
|
let range = match motion {
|
|
|
|
EditTarget::CurrentPosition | EditTarget::Selection => {
|
|
|
|
Some(self._range_to(key.into()))
|
|
|
|
},
|
|
|
|
EditTarget::Boundary(rt, inc, term, count) => {
|
|
|
|
self.range(key, rt, *inc, count, ctx, info).map(|r| {
|
|
|
|
self._range_to(match term {
|
|
|
|
MoveTerminus::Beginning => r.start,
|
|
|
|
MoveTerminus::End => r.end,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => {
|
|
|
|
let mark = ctx.resolve(mark);
|
|
|
|
let cursor = store.cursors.get_mark(self.id.clone(), mark)?;
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
if let Some(c) = MessageCursor::from_cursor(&cursor, thread) {
|
2022-12-29 18:00:59 -08:00
|
|
|
self._range_to(c).into()
|
|
|
|
} else {
|
|
|
|
let msg = "Failed to restore mark";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
EditTarget::Motion(mt, count) => {
|
|
|
|
self.range_of_movement(key, mt, count, ctx, info)
|
|
|
|
},
|
|
|
|
EditTarget::Range(rt, inc, count) => {
|
|
|
|
self.range(key, rt, *inc, count, ctx, info)
|
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Char(_), _, _) => {
|
|
|
|
let msg = "Cannot perform character search in a list";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Regex, flip, count) => {
|
|
|
|
let count = ctx.resolve(count);
|
|
|
|
|
|
|
|
let dir = ctx.get_search_regex_dir();
|
|
|
|
let dir = flip.resolve(&dir);
|
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
let lsearch = store.registers.get(&Register::LastSearch)?;
|
|
|
|
let lsearch = lsearch.value.to_string();
|
|
|
|
let needle = Regex::new(lsearch.as_ref())?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-12-18 20:55:04 -08:00
|
|
|
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
|
|
|
if needs_load {
|
|
|
|
store
|
|
|
|
.application
|
|
|
|
.need_load
|
|
|
|
.insert(self.room_id.to_owned(), Need::MESSAGES);
|
|
|
|
}
|
|
|
|
|
|
|
|
mc.map(|c| self._range_to(c))
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
EditTarget::Search(SearchType::Word(_, _), _, _) => {
|
|
|
|
let msg = "Cannot perform word search in a list";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
|
|
|
|
_ => {
|
2023-01-30 13:51:32 -08:00
|
|
|
let msg = format!("Unknown motion: {motion:?}");
|
2022-12-29 18:00:59 -08:00
|
|
|
let err = EditError::Unimplemented(msg);
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(range) = range {
|
|
|
|
let mut yanked = EditRope::from("");
|
|
|
|
|
|
|
|
for (_, msg) in self.messages(range, info) {
|
2023-01-23 17:08:11 -08:00
|
|
|
yanked += EditRope::from(msg.event.body());
|
2022-12-29 18:00:59 -08:00
|
|
|
yanked += EditRope::from('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
let cell = RegisterCell::new(TargetShape::LineWise, yanked);
|
|
|
|
let register = ctx.get_register().unwrap_or(Register::Unnamed);
|
|
|
|
let mut flags = RegisterPutFlags::NONE;
|
|
|
|
|
|
|
|
if ctx.get_register_append() {
|
|
|
|
flags |= RegisterPutFlags::APPEND;
|
|
|
|
}
|
|
|
|
|
2023-02-09 23:05:02 -08:00
|
|
|
store.registers.put(®ister, cell, flags)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(None);
|
|
|
|
},
|
|
|
|
|
|
|
|
// Everything else is a modifying action.
|
|
|
|
EditAction::ChangeCase(_) => Err(EditError::ReadOnly),
|
2023-02-09 23:05:02 -08:00
|
|
|
EditAction::ChangeNumber(_, _) => Err(EditError::ReadOnly),
|
2022-12-29 18:00:59 -08:00
|
|
|
EditAction::Delete => Err(EditError::ReadOnly),
|
|
|
|
EditAction::Format => Err(EditError::ReadOnly),
|
|
|
|
EditAction::Indent(_) => Err(EditError::ReadOnly),
|
|
|
|
EditAction::Join(_) => Err(EditError::ReadOnly),
|
|
|
|
EditAction::Replace(_) => Err(EditError::ReadOnly),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn mark(
|
|
|
|
&mut self,
|
|
|
|
name: Mark,
|
|
|
|
_: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
let info = store.application.get_room_info(self.room_id.clone());
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
|
|
|
let cursor = self.cursor.to_cursor(thread).ok_or_else(no_msgs)?;
|
|
|
|
store.cursors.set_mark(self.id.clone(), name, cursor);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
Ok(None)
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn complete(
|
|
|
|
&mut self,
|
|
|
|
_: &CompletionType,
|
|
|
|
_: &CompletionSelection,
|
|
|
|
_: &CompletionDisplay,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
Err(EditError::ReadOnly)
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
fn insert_text(
|
|
|
|
&mut self,
|
|
|
|
_: &InsertTextAction,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
Err(EditError::ReadOnly)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn selection_command(
|
|
|
|
&mut self,
|
|
|
|
_: &SelectionAction,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
Err(EditError::Failure("Cannot perform selection actions in a list".into()))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn history_command(
|
|
|
|
&mut self,
|
|
|
|
act: &HistoryAction,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
match act {
|
|
|
|
HistoryAction::Checkpoint => Ok(None),
|
|
|
|
HistoryAction::Undo(_) => Err(EditError::Failure("Nothing to undo".into())),
|
|
|
|
HistoryAction::Redo(_) => Err(EditError::Failure("Nothing to redo".into())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn cursor_command(
|
|
|
|
&mut self,
|
|
|
|
act: &CursorAction,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
let info = store.application.get_room_info(self.room_id.clone());
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
match act {
|
|
|
|
CursorAction::Close(_) => Ok(None),
|
|
|
|
CursorAction::Rotate(_, _) => Ok(None),
|
|
|
|
CursorAction::Split(_) => Ok(None),
|
|
|
|
|
|
|
|
CursorAction::Restore(_) => {
|
|
|
|
let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup);
|
|
|
|
|
|
|
|
// Get saved group.
|
|
|
|
let ngroup = store.cursors.get_group(self.id.clone(), ®)?;
|
|
|
|
|
|
|
|
// Lists don't have groups; override current position.
|
2024-03-09 00:47:05 -08:00
|
|
|
if self.jump_changed() {
|
|
|
|
self.push_jump();
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
if let Some(mc) = MessageCursor::from_cursor(ngroup.leader.cursor(), thread) {
|
2022-12-29 18:00:59 -08:00
|
|
|
self.cursor = mc;
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
} else {
|
|
|
|
let msg = "Cannot restore position in message history";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
Err(err)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
CursorAction::Save(_) => {
|
|
|
|
let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup);
|
|
|
|
|
|
|
|
// Lists don't have groups; override any previously saved group.
|
2024-03-09 00:47:05 -08:00
|
|
|
let cursor = self.cursor.to_cursor(thread).ok_or_else(|| {
|
2022-12-29 18:00:59 -08:00
|
|
|
let msg = "Cannot save position in message history";
|
|
|
|
EditError::Failure(msg.into())
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let group = CursorGroup {
|
|
|
|
leader: CursorState::Location(cursor),
|
|
|
|
members: vec![],
|
|
|
|
};
|
|
|
|
|
|
|
|
store.cursors.set_group(self.id.clone(), reg, group)?;
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
2023-01-30 13:51:32 -08:00
|
|
|
_ => Err(EditError::Unimplemented(format!("Unknown action: {act:?}"))),
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Editable<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn editor_command(
|
|
|
|
&mut self,
|
|
|
|
act: &EditorAction,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
match act {
|
|
|
|
EditorAction::Cursor(act) => self.cursor_command(act, ctx, store),
|
|
|
|
EditorAction::Edit(ea, et) => self.edit(&ctx.resolve(ea), et, ctx, store),
|
|
|
|
EditorAction::History(act) => self.history_command(act, ctx, store),
|
|
|
|
EditorAction::InsertText(act) => self.insert_text(act, ctx, store),
|
|
|
|
EditorAction::Mark(name) => self.mark(ctx.resolve(name), ctx, store),
|
|
|
|
EditorAction::Selection(act) => self.selection_command(act, ctx, store),
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
EditorAction::Complete(_, _, _) => {
|
|
|
|
let msg = "Nothing to complete in message scrollback";
|
|
|
|
let err = EditError::Failure(msg.into());
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
Err(err)
|
|
|
|
},
|
|
|
|
|
2023-01-30 13:51:32 -08:00
|
|
|
_ => Err(EditError::Unimplemented(format!("Unknown action: {act:?}"))),
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Jumpable<ProgramContext, IambInfo> for ScrollbackState {
|
|
|
|
fn jump(
|
|
|
|
&mut self,
|
|
|
|
list: PositionList,
|
|
|
|
dir: MoveDir1D,
|
|
|
|
count: usize,
|
|
|
|
_: &ProgramContext,
|
|
|
|
) -> UIResult<usize, IambInfo> {
|
|
|
|
match list {
|
|
|
|
PositionList::ChangeList => {
|
|
|
|
let msg = "No changes to jump to within the list";
|
|
|
|
let err = UIError::Failure(msg.into());
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
Err(err)
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
PositionList::JumpList => {
|
|
|
|
let (len, pos) = match dir {
|
|
|
|
MoveDir1D::Previous => {
|
2024-03-09 00:47:05 -08:00
|
|
|
if self.jumped.future_len() == 0 && self.jump_changed() {
|
2022-12-29 18:00:59 -08:00
|
|
|
// Push current position if this is the first jump backwards.
|
2024-03-09 00:47:05 -08:00
|
|
|
self.push_jump();
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
let plen = self.jumped.past_len();
|
|
|
|
let pos = self.jumped.prev(count);
|
|
|
|
|
|
|
|
(plen, pos)
|
|
|
|
},
|
|
|
|
MoveDir1D::Next => {
|
|
|
|
let flen = self.jumped.future_len();
|
|
|
|
let pos = self.jumped.next(count);
|
|
|
|
|
|
|
|
(flen, pos)
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
if len > 0 {
|
|
|
|
self.cursor = pos.clone();
|
|
|
|
}
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
Ok(count.saturating_sub(len))
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Promptable<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn prompt(
|
|
|
|
&mut self,
|
|
|
|
act: &PromptAction,
|
2024-03-09 00:47:05 -08:00
|
|
|
ctx: &ProgramContext,
|
2022-12-29 18:00:59 -08:00
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<Vec<(Action<IambInfo>, ProgramContext)>, IambInfo> {
|
|
|
|
let info = store.application.get_room_info(self.room_id.clone());
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let Some(key) = self.cursor.to_key(thread) else {
|
2022-12-29 18:00:59 -08:00
|
|
|
let msg = "No message currently selected";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
return Err(err);
|
|
|
|
};
|
|
|
|
|
|
|
|
match act {
|
|
|
|
PromptAction::Submit => {
|
2024-03-09 00:47:05 -08:00
|
|
|
if self.thread.is_some() {
|
|
|
|
let msg =
|
|
|
|
"You are already in a thread. Use :reply to reply to a specific message.";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
Err(err)
|
|
|
|
} else {
|
|
|
|
let root = key.1.clone();
|
|
|
|
let room_id = self.room_id.clone();
|
|
|
|
let id = IambId::Room(room_id, Some(root));
|
|
|
|
let open = WindowAction::Switch(OpenTarget::Application(id));
|
|
|
|
Ok(vec![(open.into(), ctx.clone())])
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
PromptAction::Abort(..) => {
|
|
|
|
let msg = "Cannot abort a message.";
|
|
|
|
let err = EditError::Failure(msg.into());
|
2024-03-09 00:47:05 -08:00
|
|
|
Err(err)
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
PromptAction::Recall(..) => {
|
|
|
|
let msg = "Cannot recall previous messages.";
|
|
|
|
let err = EditError::Failure(msg.into());
|
2024-03-09 00:47:05 -08:00
|
|
|
Err(err)
|
2022-12-29 18:00:59 -08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn dirscroll(
|
|
|
|
&mut self,
|
|
|
|
dir: MoveDir2D,
|
|
|
|
size: ScrollSize,
|
|
|
|
count: &Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
2023-01-06 16:56:28 -08:00
|
|
|
let settings = &store.application.settings;
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut corner = self.viewctx.corner.clone();
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let last_key = if let Some(k) = thread.last_key_value() {
|
2022-12-29 18:00:59 -08:00
|
|
|
k.0
|
|
|
|
} else {
|
|
|
|
return Ok(None);
|
|
|
|
};
|
|
|
|
|
|
|
|
let corner_key = corner.timestamp.as_ref().unwrap_or(last_key).clone();
|
|
|
|
let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key);
|
|
|
|
|
|
|
|
let count = ctx.resolve(count);
|
|
|
|
let height = self.viewctx.get_height();
|
|
|
|
let mut rows = match size {
|
|
|
|
ScrollSize::Cell => count,
|
|
|
|
ScrollSize::HalfPage => count.saturating_mul(height) / 2,
|
|
|
|
ScrollSize::Page => count.saturating_mul(height),
|
|
|
|
};
|
|
|
|
|
|
|
|
match dir {
|
|
|
|
MoveDir2D::Up => {
|
2024-03-09 00:47:05 -08:00
|
|
|
let first_key = thread.first_key_value().map(|f| f.0.clone());
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, item) in thread.range(..=&corner_key).rev() {
|
2022-12-29 18:00:59 -08:00
|
|
|
let sel = key == cursor_key;
|
2024-03-09 00:47:05 -08:00
|
|
|
let prev = prevmsg(key, thread);
|
2023-01-29 18:07:00 -08:00
|
|
|
let txt = item.show(prev, sel, &self.viewctx, info, settings);
|
2022-12-29 18:00:59 -08:00
|
|
|
let len = txt.height().max(1);
|
|
|
|
let max = len.saturating_sub(1);
|
|
|
|
|
|
|
|
if key != &corner_key {
|
|
|
|
corner.text_row = max;
|
|
|
|
}
|
|
|
|
|
|
|
|
corner.timestamp = key.clone().into();
|
|
|
|
|
|
|
|
if rows == 0 {
|
|
|
|
break;
|
|
|
|
} else if corner.text_row >= rows {
|
|
|
|
corner.text_row -= rows;
|
|
|
|
break;
|
|
|
|
} else if corner.timestamp == first_key {
|
|
|
|
corner.text_row = 0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
rows -= corner.text_row + 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
MoveDir2D::Down => {
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut prev = prevmsg(&corner_key, thread);
|
2023-01-29 18:07:00 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, item) in thread.range(&corner_key..) {
|
2022-12-29 18:00:59 -08:00
|
|
|
let sel = key == cursor_key;
|
2023-01-29 18:07:00 -08:00
|
|
|
let txt = item.show(prev, sel, &self.viewctx, info, settings);
|
2022-12-29 18:00:59 -08:00
|
|
|
let len = txt.height().max(1);
|
|
|
|
let max = len.saturating_sub(1);
|
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
prev = Some(item);
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
if key != &corner_key {
|
|
|
|
corner.text_row = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
corner.timestamp = key.clone().into();
|
|
|
|
|
|
|
|
if rows == 0 {
|
|
|
|
break;
|
|
|
|
} else if key == last_key {
|
|
|
|
corner.text_row = corner.text_row.saturating_add(rows).min(max);
|
|
|
|
break;
|
|
|
|
} else if corner.text_row >= max {
|
|
|
|
rows -= 1;
|
|
|
|
continue;
|
|
|
|
} else if corner.text_row + rows <= max {
|
|
|
|
corner.text_row += rows;
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
rows -= len - corner.text_row;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
MoveDir2D::Left | MoveDir2D::Right => {
|
|
|
|
let msg = "Cannot scroll vertically in message scrollback";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
return Err(err);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
self.viewctx.corner = corner;
|
2023-01-06 16:56:28 -08:00
|
|
|
self.shift_cursor(info, settings);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn cursorpos(
|
|
|
|
&mut self,
|
|
|
|
pos: MovePosition,
|
|
|
|
axis: Axis,
|
|
|
|
_: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
match axis {
|
|
|
|
Axis::Horizontal => {
|
|
|
|
let msg = "Cannot scroll vertically in message scrollback";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
Err(err)
|
|
|
|
},
|
|
|
|
Axis::Vertical => {
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
2023-01-06 16:56:28 -08:00
|
|
|
let settings = &store.application.settings;
|
2024-03-23 19:20:06 -07:00
|
|
|
let thread = self.get_thread(info).ok_or_else(no_msgs)?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
if let Some(key) = self.cursor.to_key(thread).cloned() {
|
2023-01-06 16:56:28 -08:00
|
|
|
self.scrollview(key, pos, info, settings);
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn linepos(
|
|
|
|
&mut self,
|
|
|
|
_: MovePosition,
|
|
|
|
_: &Count,
|
|
|
|
_: &ProgramContext,
|
|
|
|
_: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
let msg = "Cannot scroll in message scrollback using line numbers";
|
|
|
|
let err = EditError::Failure(msg.into());
|
|
|
|
|
|
|
|
Err(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Scrollable<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn scroll(
|
|
|
|
&mut self,
|
|
|
|
style: &ScrollStyle,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> EditResult<EditInfo, IambInfo> {
|
|
|
|
match style {
|
|
|
|
ScrollStyle::Direction2D(dir, size, count) => {
|
|
|
|
return self.dirscroll(*dir, *size, count, ctx, store);
|
|
|
|
},
|
|
|
|
ScrollStyle::CursorPos(pos, axis) => {
|
|
|
|
return self.cursorpos(*pos, *axis, ctx, store);
|
|
|
|
},
|
|
|
|
ScrollStyle::LinePos(pos, count) => {
|
|
|
|
return self.linepos(*pos, count, ctx, store);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Searchable<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|
|
|
fn search(
|
|
|
|
&mut self,
|
|
|
|
dir: MoveDirMod,
|
|
|
|
count: Count,
|
|
|
|
ctx: &ProgramContext,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> UIResult<EditInfo, IambInfo> {
|
|
|
|
let search = EditTarget::Search(SearchType::Regex, dir, count);
|
|
|
|
|
|
|
|
Ok(self.edit(&EditAction::Motion, &search, ctx, store)?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TerminalCursor for ScrollbackState {
|
|
|
|
fn get_term_cursor(&self) -> Option<(u16, u16)> {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-07 22:16:57 -07:00
|
|
|
fn render_jump_to_recent(area: Rect, buf: &mut Buffer, focused: bool) -> Rect {
|
|
|
|
if area.height <= 5 || area.width <= 20 {
|
|
|
|
return area;
|
|
|
|
}
|
|
|
|
|
|
|
|
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
|
|
|
let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
|
|
|
|
let msg = vec![
|
|
|
|
Span::raw("Use "),
|
|
|
|
Span::styled("G", Style::default().add_modifier(StyleModifier::BOLD)),
|
|
|
|
Span::raw(if focused { "" } else { " in scrollback" }),
|
|
|
|
Span::raw(" to jump to latest message"),
|
|
|
|
];
|
|
|
|
|
2023-07-01 08:58:48 +01:00
|
|
|
Paragraph::new(Line::from(msg))
|
2023-07-07 22:16:57 -07:00
|
|
|
.alignment(Alignment::Center)
|
|
|
|
.render(bar, buf);
|
|
|
|
|
|
|
|
return top;
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub struct Scrollback<'a> {
|
2023-06-14 22:42:23 -07:00
|
|
|
room_focused: bool,
|
2022-12-29 18:00:59 -08:00
|
|
|
focused: bool,
|
|
|
|
store: &'a mut ProgramStore,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Scrollback<'a> {
|
|
|
|
pub fn new(store: &'a mut ProgramStore) -> Self {
|
2023-06-14 22:42:23 -07:00
|
|
|
Scrollback { room_focused: false, focused: false, store }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Indicate whether the room window is currently focused, regardless of whether the scrollback
|
|
|
|
/// also is.
|
|
|
|
pub fn room_focus(mut self, focused: bool) -> Self {
|
|
|
|
self.room_focused = focused;
|
|
|
|
self
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Indicate whether the scrollback is currently focused.
|
|
|
|
pub fn focus(mut self, focused: bool) -> Self {
|
|
|
|
self.focused = focused;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StatefulWidget for Scrollback<'a> {
|
|
|
|
type State = ScrollbackState;
|
|
|
|
|
|
|
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
2023-03-01 18:46:33 -08:00
|
|
|
let info = self.store.application.rooms.get_or_default(state.room_id.clone());
|
2023-01-06 16:56:28 -08:00
|
|
|
let settings = &self.store.application.settings;
|
2023-07-07 22:16:57 -07:00
|
|
|
let area = if state.cursor.timestamp.is_some() {
|
|
|
|
render_jump_to_recent(area, buf, self.focused)
|
|
|
|
} else {
|
|
|
|
info.render_typing(area, buf, &self.store.application.settings)
|
|
|
|
};
|
2023-01-03 13:57:28 -08:00
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
state.set_term_info(area);
|
|
|
|
|
|
|
|
let height = state.viewctx.get_height();
|
|
|
|
|
|
|
|
if height == 0 {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
let Some(thread) = state.get_thread(info) else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
if state.cursor.timestamp < state.viewctx.corner.timestamp {
|
|
|
|
state.viewctx.corner = state.cursor.clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
let cursor = &state.cursor;
|
2024-03-09 00:47:05 -08:00
|
|
|
let cursor_key = if let Some(k) = cursor.to_key(thread) {
|
2022-12-29 18:00:59 -08:00
|
|
|
k
|
|
|
|
} else {
|
2024-03-09 00:47:05 -08:00
|
|
|
if state.need_more_messages(info) {
|
|
|
|
self.store
|
|
|
|
.application
|
|
|
|
.need_load
|
|
|
|
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let corner = &state.viewctx.corner;
|
2023-01-10 19:59:30 -08:00
|
|
|
let corner_key = if let Some(k) = &corner.timestamp {
|
|
|
|
k.clone()
|
|
|
|
} else {
|
2024-03-09 00:47:05 -08:00
|
|
|
nth_key_before(cursor_key.clone(), height, thread)
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
2023-01-12 21:20:32 -08:00
|
|
|
let foc = self.focused || cursor.timestamp.is_some();
|
|
|
|
let full = std::mem::take(&mut state.show_full_on_redraw) || cursor.timestamp.is_none();
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut lines = vec![];
|
|
|
|
let mut sawit = false;
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut prev = prevmsg(&corner_key, thread);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
for (key, item) in thread.range(&corner_key..) {
|
2022-12-29 18:00:59 -08:00
|
|
|
let sel = key == cursor_key;
|
2024-03-23 19:41:05 -07:00
|
|
|
let (txt, mut msg_preview) =
|
|
|
|
item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
let incomplete_ok = !full || !sel;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
for (row, line) in txt.lines.into_iter().enumerate() {
|
2023-01-10 19:59:30 -08:00
|
|
|
if sawit && lines.len() >= height && incomplete_ok {
|
2022-12-29 18:00:59 -08:00
|
|
|
// Check whether we've seen the first line of the
|
|
|
|
// selected message and can fill the screen.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if key == &corner_key && row < corner.text_row {
|
|
|
|
// Skip rows above the viewport corner.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
let line_preview = match msg_preview {
|
|
|
|
// Only take the preview into the matching row number.
|
|
|
|
Some((_, _, y)) if y as usize == row => msg_preview.take(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
|
|
|
|
lines.push((key, row, line, line_preview));
|
2022-12-29 18:00:59 -08:00
|
|
|
sawit |= sel;
|
|
|
|
}
|
2023-11-16 08:36:22 -08:00
|
|
|
|
|
|
|
prev = Some(item);
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if lines.len() > height {
|
|
|
|
let n = lines.len() - height;
|
|
|
|
let _ = lines.drain(..n);
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
if let Some(((ts, event_id), row, _, _)) = lines.first() {
|
2022-12-29 18:00:59 -08:00
|
|
|
state.viewctx.corner.timestamp = Some((*ts, event_id.clone()));
|
|
|
|
state.viewctx.corner.text_row = *row;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut y = area.top();
|
|
|
|
let x = area.left();
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
let mut image_previews = vec![];
|
|
|
|
for ((_, _), _, txt, line_preview) in lines.into_iter() {
|
2023-07-01 08:58:48 +01:00
|
|
|
let _ = buf.set_line(x, y, &txt, area.width);
|
2023-11-16 08:36:22 -08:00
|
|
|
if let Some((backend, msg_x, _)) = line_preview {
|
|
|
|
image_previews.push((x + msg_x, y, backend));
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
y += 1;
|
|
|
|
}
|
2023-11-16 08:36:22 -08:00
|
|
|
// Render image previews after all text lines have been drawn, as the render might draw below the current
|
|
|
|
// line.
|
|
|
|
for (x, y, backend) in image_previews {
|
2024-03-09 04:04:52 +00:00
|
|
|
let image_widget = Image::new(backend);
|
2023-11-16 08:36:22 -08:00
|
|
|
let mut rect = backend.rect();
|
|
|
|
rect.x = x;
|
|
|
|
rect.y = y;
|
|
|
|
// Don't render outside of scrollback area
|
|
|
|
if rect.bottom() <= area.bottom() && rect.right() <= area.right() {
|
|
|
|
image_widget.render(rect, buf);
|
|
|
|
}
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-06-14 22:42:23 -07:00
|
|
|
if self.room_focused &&
|
|
|
|
settings.tunables.read_receipt_send &&
|
|
|
|
state.cursor.timestamp.is_none()
|
|
|
|
{
|
2023-01-26 15:40:16 -08:00
|
|
|
// If the cursor is at the last message, then update the read marker.
|
2024-03-09 00:47:05 -08:00
|
|
|
if let Some((k, _)) = thread.last_key_value() {
|
2024-02-28 09:03:28 -08:00
|
|
|
info.set_receipt(settings.profile.user_id.clone(), k.1.clone());
|
|
|
|
}
|
2023-01-26 15:40:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether we should load older messages for this room.
|
2024-03-09 00:47:05 -08:00
|
|
|
if state.need_more_messages(info) {
|
2022-12-29 18:00:59 -08:00
|
|
|
// If the top of the screen is the older message, load more.
|
2023-12-18 20:55:04 -08:00
|
|
|
self.store
|
|
|
|
.application
|
|
|
|
.need_load
|
|
|
|
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
2024-03-22 00:46:46 +00:00
|
|
|
|
|
|
|
info.draw_last = self.store.application.draw_curr;
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::tests::*;
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_search_messages() {
|
2022-12-29 18:00:59 -08:00
|
|
|
let room_id = TEST_ROOM1_ID.clone();
|
2023-01-10 19:59:30 -08:00
|
|
|
let mut store = mock_store().await;
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut scrollback = ScrollbackState::new(room_id.clone(), None);
|
2022-12-29 18:00:59 -08:00
|
|
|
let ctx = ProgramContext::default();
|
|
|
|
|
|
|
|
let next = MoveDirMod::Exact(MoveDir1D::Next);
|
|
|
|
let prev = MoveDirMod::Exact(MoveDir1D::Previous);
|
|
|
|
|
|
|
|
// Search through the messages:
|
|
|
|
//
|
|
|
|
// MSG2: "helium"
|
|
|
|
// MSG3: "this\nis\na\nmultiline\nmessage"
|
|
|
|
// MSG4: "help"
|
|
|
|
// MSG5: "character"
|
|
|
|
// MSG1: "writhe"
|
|
|
|
store.set_last_search("he");
|
|
|
|
|
|
|
|
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
|
|
|
|
|
|
|
// Search backwards to MSG4.
|
|
|
|
scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
|
|
|
|
// Search backwards to MSG2.
|
|
|
|
scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG2_KEY.clone().into());
|
2023-12-18 20:55:04 -08:00
|
|
|
assert_eq!(
|
|
|
|
std::mem::take(&mut store.application.need_load)
|
|
|
|
.into_iter()
|
|
|
|
.collect::<Vec<(OwnedRoomId, Need)>>()
|
|
|
|
.is_empty(),
|
|
|
|
true,
|
|
|
|
);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
// Can't go any further; need_load now contains the room ID.
|
|
|
|
scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG2_KEY.clone().into());
|
2023-12-18 20:55:04 -08:00
|
|
|
assert_eq!(
|
|
|
|
std::mem::take(&mut store.application.need_load)
|
|
|
|
.into_iter()
|
|
|
|
.collect::<Vec<(OwnedRoomId, Need)>>(),
|
|
|
|
vec![(room_id.clone(), Need::MESSAGES)]
|
|
|
|
);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
// Search forward twice to MSG1.
|
|
|
|
scrollback.search(next.clone(), 2.into(), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
|
|
|
|
|
|
|
// Can't go any further.
|
|
|
|
scrollback.search(next.clone(), 2.into(), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_movement() {
|
|
|
|
let mut store = mock_store().await;
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None);
|
2022-12-29 18:00:59 -08:00
|
|
|
let ctx = ProgramContext::default();
|
|
|
|
|
|
|
|
let prev = |n: usize| EditTarget::Motion(MoveType::Line(MoveDir1D::Previous), n.into());
|
|
|
|
|
|
|
|
let next = |n: usize| EditTarget::Motion(MoveType::Line(MoveDir1D::Next), n.into());
|
|
|
|
|
|
|
|
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &prev(1), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &prev(2), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG3_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &prev(5), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG2_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG3_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
|
|
|
|
|
|
|
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_dirscroll() {
|
|
|
|
let mut store = mock_store().await;
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None);
|
2022-12-29 18:00:59 -08:00
|
|
|
let ctx = ProgramContext::default();
|
|
|
|
|
|
|
|
let prev = MoveDir2D::Up;
|
|
|
|
let next = MoveDir2D::Down;
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
// Skip rendering typing notices.
|
|
|
|
store.application.settings.tunables.typing_notice_display = false;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
|
|
|
assert_eq!(scrollback.viewctx.dimensions, (0, 0));
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::latest());
|
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
// Set a terminal width of 60, and height of 4, rendering in scrollback as:
|
2022-12-29 18:00:59 -08:00
|
|
|
//
|
|
|
|
// |------------------------------------------------------------|
|
2023-01-29 18:07:00 -08:00
|
|
|
// MSG2: | Wednesday, December 31 1969 |
|
|
|
|
// | @user2:example.com helium |
|
2022-12-29 18:00:59 -08:00
|
|
|
// MSG3: | @user2:example.com this |
|
|
|
|
// | is |
|
|
|
|
// | a |
|
|
|
|
// | multiline |
|
|
|
|
// | message |
|
|
|
|
// MSG4: | @user1:example.com help |
|
|
|
|
// MSG5: | @user2:example.com character |
|
2023-01-29 18:07:00 -08:00
|
|
|
// MSG1: | XXXday, Month NN 20XX |
|
|
|
|
// | @user1:example.com writhe |
|
2022-12-29 18:00:59 -08:00
|
|
|
// |------------------------------------------------------------|
|
2023-01-29 18:07:00 -08:00
|
|
|
let area = Rect::new(0, 0, 60, 4);
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut buffer = Buffer::empty(area);
|
|
|
|
scrollback.draw(area, &mut buffer, true, &mut store);
|
|
|
|
|
|
|
|
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
2023-01-29 18:07:00 -08:00
|
|
|
assert_eq!(scrollback.viewctx.dimensions, (60, 4));
|
2022-12-29 18:00:59 -08:00
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0));
|
|
|
|
|
|
|
|
// Scroll up a line at a time until we hit the first message.
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 2));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0));
|
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 1));
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 0));
|
|
|
|
|
|
|
|
// Cannot scroll any further.
|
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 0));
|
|
|
|
|
|
|
|
// Now scroll back down one line at a time.
|
2023-01-29 18:07:00 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 1));
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 2));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG5_KEY.clone(), 0));
|
|
|
|
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 0));
|
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 1));
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
// Cannot scroll down any further.
|
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
2023-01-29 18:07:00 -08:00
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 1));
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
// Scroll up two Pages (eight lines).
|
2022-12-29 18:00:59 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(prev, ScrollSize::Page, &2.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
2023-01-29 18:07:00 -08:00
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0));
|
2022-12-29 18:00:59 -08:00
|
|
|
|
2023-01-29 18:07:00 -08:00
|
|
|
// Scroll down two HalfPages (four lines).
|
2022-12-29 18:00:59 -08:00
|
|
|
scrollback
|
|
|
|
.dirscroll(next, ScrollSize::HalfPage, &2.into(), &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_cursorpos() {
|
|
|
|
let mut store = mock_store().await;
|
2024-03-09 00:47:05 -08:00
|
|
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone(), None);
|
2022-12-29 18:00:59 -08:00
|
|
|
let ctx = ProgramContext::default();
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
// Skip rendering typing notices.
|
|
|
|
store.application.settings.tunables.typing_notice_display = false;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
// Set a terminal width of 60, and height of 3, rendering in scrollback as:
|
|
|
|
//
|
|
|
|
// |------------------------------------------------------------|
|
2023-01-29 18:07:00 -08:00
|
|
|
// MSG2: | Wednesday, December 31 1969 |
|
|
|
|
// | @user2:example.com helium |
|
2022-12-29 18:00:59 -08:00
|
|
|
// MSG3: | @user2:example.com this |
|
|
|
|
// | is |
|
|
|
|
// | a |
|
|
|
|
// | multiline |
|
|
|
|
// | message |
|
|
|
|
// MSG4: | @user1:example.com help |
|
|
|
|
// MSG5: | @user2:example.com character |
|
2023-01-29 18:07:00 -08:00
|
|
|
// MSG1: | XXXday, Month NN 20XX |
|
|
|
|
// | @user1:example.com writhe |
|
2022-12-29 18:00:59 -08:00
|
|
|
// |------------------------------------------------------------|
|
|
|
|
let area = Rect::new(0, 0, 60, 3);
|
|
|
|
let mut buffer = Buffer::empty(area);
|
|
|
|
scrollback.cursor = MSG4_KEY.clone().into();
|
|
|
|
scrollback.draw(area, &mut buffer, true, &mut store);
|
|
|
|
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
assert_eq!(scrollback.viewctx.dimensions, (60, 3));
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3));
|
|
|
|
|
|
|
|
// Scroll so that the cursor is at the top of the screen.
|
|
|
|
scrollback
|
|
|
|
.cursorpos(MovePosition::Beginning, Axis::Vertical, &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0));
|
|
|
|
|
|
|
|
// Scroll so that the cursor is at the bottom of the screen.
|
|
|
|
scrollback
|
|
|
|
.cursorpos(MovePosition::End, Axis::Vertical, &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3));
|
|
|
|
|
|
|
|
// Scroll so that the cursor is in the middle of the screen.
|
|
|
|
scrollback
|
|
|
|
.cursorpos(MovePosition::Middle, Axis::Vertical, &ctx, &mut store)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(scrollback.cursor, MSG4_KEY.clone().into());
|
|
|
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
|
|
|
}
|
|
|
|
}
|