Extract History to a common design to support more cases. (#162)

Ref https://github.com/huacnlee/gpui-component/issues/161

For example: Used to do Back/Forward
This commit is contained in:
Jason Lee 2024-08-16 17:04:11 +08:00 committed by GitHub
parent 4490618c4a
commit 46ff43ff36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 243 additions and 123 deletions

192
crates/ui/src/history.rs Normal file
View file

@ -0,0 +1,192 @@
use std::{
fmt::Debug,
time::{Duration, Instant},
};
pub trait HistoryItem: Clone {
fn version(&self) -> usize;
fn set_version(&mut self, version: usize);
}
#[derive(Debug)]
pub struct History<I: HistoryItem> {
undos: Vec<I>,
redos: Vec<I>,
last_changed_at: Instant,
version: usize,
pub(crate) ignore: bool,
max_undo: usize,
group_interval: Option<Duration>,
}
impl<I> History<I>
where
I: HistoryItem,
{
pub fn new() -> Self {
Self {
undos: Default::default(),
redos: Default::default(),
ignore: false,
last_changed_at: Instant::now(),
version: 0,
max_undo: 1000,
group_interval: None,
}
}
/// Set the maximum number of undo steps to keep, defaults to 1000.
pub fn max_undo(mut self, max_undo: usize) -> Self {
self.max_undo = max_undo;
self
}
/// Set the interval in milliseconds to group changes, defaults to None.
pub fn group_interval(mut self, group_interval: Duration) -> Self {
self.group_interval = Some(group_interval);
self
}
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
fn inc_version(&mut self) -> usize {
let t = Instant::now();
if Some(self.last_changed_at.elapsed()) > self.group_interval {
self.version += 1;
}
self.last_changed_at = t;
self.version
}
/// Get the current version number.
pub fn version(&self) -> usize {
self.version
}
pub fn push(&mut self, item: I) {
let version = self.inc_version();
if self.undos.len() >= self.max_undo {
self.undos.remove(0);
}
let mut item = item;
item.set_version(version);
self.undos.push(item);
}
pub fn undo(&mut self) -> Option<Vec<I>> {
if let Some(first_change) = self.undos.pop() {
let mut changes = vec![first_change.clone()];
// pick the next all changes with the same version
while self
.undos
.iter()
.filter(|c| c.version() == first_change.version())
.count()
> 0
{
let change = self.undos.pop().unwrap();
changes.push(change);
}
self.redos.extend(changes.iter().rev().cloned());
Some(changes)
} else {
None
}
}
pub fn redo(&mut self) -> Option<Vec<I>> {
if let Some(first_change) = self.redos.pop() {
let mut changes = vec![first_change.clone()];
// pick the next all changes with the same version
while self
.redos
.iter()
.filter(|c| c.version() == first_change.version())
.count()
> 0
{
let change = self.redos.pop().unwrap();
changes.push(change);
}
self.undos.extend(changes.iter().rev().cloned());
Some(changes)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct TabIndex {
tab_index: usize,
version: usize,
}
impl From<usize> for TabIndex {
fn from(value: usize) -> Self {
TabIndex {
tab_index: value,
version: 0,
}
}
}
impl HistoryItem for TabIndex {
fn version(&self) -> usize {
self.version
}
fn set_version(&mut self, version: usize) {
self.version = version;
}
}
#[test]
fn test_history() {
let mut history: History<TabIndex> = History::new().max_undo(100);
history.push(0.into());
history.push(3.into());
history.push(2.into());
history.push(1.into());
assert_eq!(history.version(), 4);
let changes = history.undo().unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].tab_index, 1);
let changes = history.undo().unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].tab_index, 2);
history.push(5.into());
let changes = history.redo().unwrap();
assert_eq!(changes[0].tab_index, 2);
let changes = history.redo().unwrap();
assert_eq!(changes[0].tab_index, 1);
let changes = history.undo().unwrap();
assert_eq!(changes[0].tab_index, 1);
let changes = history.undo().unwrap();
assert_eq!(changes[0].tab_index, 2);
let changes = history.undo().unwrap();
assert_eq!(changes[0].tab_index, 5);
let changes = history.undo().unwrap();
assert_eq!(changes[0].tab_index, 3);
let changes = history.undo().unwrap();
assert_eq!(changes[0].tab_index, 0);
assert_eq!(history.undo().is_none(), true);
}
}

View file

@ -0,0 +1,39 @@
use std::{fmt::Debug, ops::Range};
use crate::history::HistoryItem;
#[derive(Debug, Clone)]
pub struct Change {
pub(crate) old_range: Range<usize>,
pub(crate) old_text: String,
pub(crate) new_range: Range<usize>,
pub(crate) new_text: String,
version: usize,
}
impl Change {
pub fn new(
old_range: Range<usize>,
old_text: &str,
new_range: Range<usize>,
new_text: &str,
) -> Self {
Self {
old_range,
old_text: old_text.to_string(),
new_range,
new_text: new_text.to_string(),
version: 0,
}
}
}
impl HistoryItem for Change {
fn version(&self) -> usize {
self.version
}
fn set_version(&mut self, version: usize) {
self.version = version;
}
}

View file

@ -1,116 +0,0 @@
use std::{
fmt::Debug,
ops::Range,
time::{Duration, Instant},
};
const MAX_UNDO: usize = 1000;
/// Group interval in milliseconds
const GROUP_INTERVAL: u64 = 1000;
#[derive(Debug)]
pub struct History {
undos: Vec<Change>,
redos: Vec<Change>,
last_changed_at: Instant,
version: usize,
pub(crate) ignore: bool,
}
#[derive(Debug, Clone)]
pub struct Change {
pub(crate) old_range: Range<usize>,
pub(crate) old_text: String,
pub(crate) new_range: Range<usize>,
pub(crate) new_text: String,
version: usize,
}
impl History {
pub fn new() -> Self {
Self {
undos: Default::default(),
redos: Default::default(),
ignore: false,
last_changed_at: Instant::now(),
version: 0,
}
}
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
fn inc_version(&mut self) -> usize {
let t = Instant::now();
if self.last_changed_at.elapsed().as_millis()
> Duration::from_millis(GROUP_INTERVAL).as_millis()
{
self.version += 1;
}
self.last_changed_at = t;
self.version
}
pub fn push(
&mut self,
old_range: Range<usize>,
old_text: &str,
new_range: Range<usize>,
new_text: &str,
) {
let version = self.inc_version();
if self.undos.len() >= MAX_UNDO {
self.undos.remove(0);
}
self.undos.push(Change {
old_range,
old_text: old_text.to_string(),
new_range,
new_text: new_text.to_string(),
version,
});
}
pub fn undo(&mut self) -> Option<Vec<Change>> {
if let Some(first_change) = self.undos.pop() {
let mut changes = vec![first_change.clone()];
// pick the next all changes with the same version
while self
.undos
.iter()
.filter(|c| c.version == first_change.version)
.count()
> 0
{
let change = self.undos.pop().unwrap();
changes.push(change);
}
self.redos.extend(changes.iter().rev().cloned());
Some(changes)
} else {
None
}
}
pub fn redo(&mut self) -> Option<Vec<Change>> {
if let Some(first_change) = self.redos.pop() {
let mut changes = vec![first_change.clone()];
// pick the next all changes with the same version
while self
.redos
.iter()
.filter(|c| c.version == first_change.version)
.count()
> 0
{
let change = self.redos.pop().unwrap();
changes.push(change);
}
self.undos.extend(changes.iter().rev().cloned());
Some(changes)
} else {
None
}
}
}

View file

@ -6,8 +6,9 @@
use std::ops::Range;
use super::blink_cursor::BlinkCursor;
use super::history::History;
use super::change::Change;
use super::ClearButton;
use crate::history::History;
use crate::indicator::Indicator;
use crate::theme::ActiveTheme;
use crate::StyledExt as _;
@ -110,7 +111,7 @@ pub fn init(cx: &mut AppContext) {
pub struct TextInput {
focus_handle: FocusHandle,
text: SharedString,
history: History,
history: History<Change>,
blink_cursor: Model<BlinkCursor>,
prefix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
suffix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
@ -138,7 +139,7 @@ impl TextInput {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
let blink_cursor = cx.new_model(|_| BlinkCursor::new());
let history = History::new();
let history = History::new().group_interval(std::time::Duration::from_secs(1));
let input = Self {
focus_handle: focus_handle.clone(),
text: "".into(),
@ -445,8 +446,12 @@ impl TextInput {
let new_range = range.start..range.start + new_text.len();
self.history
.push(range.clone(), &old_text, new_range, new_text);
self.history.push(Change::new(
range.clone(),
&old_text,
new_range.clone(),
new_text,
));
}
fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {

View file

@ -1,6 +1,6 @@
mod blink_cursor;
mod change;
mod clear_button;
mod history;
mod input;
mod otp_input;

View file

@ -2,7 +2,6 @@ mod colors;
mod event;
mod focusable;
mod icon;
mod root;
mod styled;
mod svg_img;
@ -15,6 +14,7 @@ pub mod context_menu;
pub mod divider;
pub mod drawer;
pub mod dropdown;
pub mod history;
pub mod indicator;
pub mod input;
pub mod label;