diff --git a/Cargo.lock b/Cargo.lock index ec2e2e8..6878ef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,6 +844,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "catty" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf0adb3cc1c06945672f8dcc827e42497ac6d0aff49f459ec918132b82a5cbc" +dependencies = [ + "spin", +] + [[package]] name = "cbc" version = "0.1.2" @@ -5432,6 +5441,7 @@ dependencies = [ name = "scope" version = "0.1.0" dependencies = [ + "catty", "chrono", "dotenv", "env_logger", @@ -5440,6 +5450,7 @@ dependencies = [ "random-string", "reqwest_client", "rust-embed", + "scope-backend-cache", "scope-backend-discord", "scope-chat", "scope-util", @@ -5447,12 +5458,23 @@ dependencies = [ "ui", ] +[[package]] +name = "scope-backend-cache" +version = "0.1.0" +dependencies = [ + "gpui", + "rand 0.8.5", + "scope-chat", + "tokio", +] + [[package]] name = "scope-backend-discord" version = "0.1.0" dependencies = [ "chrono", "gpui", + "scope-backend-cache", "scope-chat", "serenity", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 0d86827..371f1cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] resolver = "2" -members = [ - "src/ui" -] +members = ["src/ui", "src/cache", "src/chat", "src/discord"] [workspace.dependencies] chrono = "0.4.38" \ No newline at end of file diff --git a/src/cache/Cargo.toml b/src/cache/Cargo.toml new file mode 100644 index 0000000..d04cd32 --- /dev/null +++ b/src/cache/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "scope-backend-cache" +version = "0.1.0" +edition = "2021" + +[dependencies] +gpui = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform-window", default-features = false, features = [ + "http_client", + "font-kit", +] } +rand = "0.8.5" +scope-chat = { version = "0.1.0", path = "../chat" } +tokio = "1.41.1" diff --git a/src/cache/src/async_list/mod.rs b/src/cache/src/async_list/mod.rs new file mode 100644 index 0000000..4a46823 --- /dev/null +++ b/src/cache/src/async_list/mod.rs @@ -0,0 +1,83 @@ +pub mod refcache; +pub mod refcacheslice; +pub mod tests; + +use std::collections::HashMap; + +use refcache::CacheReferences; +use refcacheslice::Exists; +use scope_chat::async_list::{AsyncListIndex, AsyncListItem, AsyncListResult}; + +pub struct AsyncListCache { + cache_refs: CacheReferences, + cache_map: HashMap, +} + +impl Default for AsyncListCache { + fn default() -> Self { + Self::new() + } +} + +impl AsyncListCache { + pub fn new() -> Self { + Self { + cache_refs: CacheReferences::new(), + cache_map: HashMap::new(), + } + } + + pub fn append_bottom(&mut self, value: I) { + let identifier = value.get_list_identifier(); + + self.cache_refs.append_bottom(identifier.clone()); + self.cache_map.insert(identifier, value); + } + + pub fn insert(&mut self, index: AsyncListIndex, value: I, is_top: bool, is_bottom: bool) { + let identifier = value.get_list_identifier(); + + self.cache_map.insert(identifier.clone(), value); + self.cache_refs.insert(index, identifier.clone(), is_top, is_bottom); + } + + /// you mut **KNOW** that the item you are inserting is not: + /// - directly next to (Before or After) **any** item in the list + /// - the first or last item in the list + pub fn insert_detached(&mut self, value: I) { + let identifier = value.get_list_identifier(); + + self.cache_map.insert(identifier.clone(), value); + self.cache_refs.insert_detached(identifier); + } + + pub fn bounded_at_top_by(&self) -> Option { + self.cache_refs.top_bound() + } + + pub fn bounded_at_bottom_by(&self) -> Option { + self.cache_refs.bottom_bound() + } + + pub fn get(&self, index: AsyncListIndex) -> Exists> { + let cache_result = self.cache_refs.get(index.clone()); + + if let Exists::Yes(cache_result) = cache_result { + let content = self.cache_map.get(&cache_result).unwrap().clone(); + let is_top = self.cache_refs.top_bound().map(|v| v == content.get_list_identifier()).unwrap_or(false); + let is_bottom = self.cache_refs.bottom_bound().map(|v| v == content.get_list_identifier()).unwrap_or(false); + + return Exists::Yes(AsyncListResult { content, is_top, is_bottom }); + }; + + if let Exists::No = cache_result { + return Exists::No; + } + + Exists::Unknown + } + + pub fn find(&self, identifier: &I::Identifier) -> Option { + self.cache_map.get(identifier).cloned() + } +} diff --git a/src/cache/src/async_list/refcache.rs b/src/cache/src/async_list/refcache.rs new file mode 100644 index 0000000..a81d72b --- /dev/null +++ b/src/cache/src/async_list/refcache.rs @@ -0,0 +1,203 @@ +use std::{collections::HashMap, fmt::Debug}; + +use scope_chat::async_list::AsyncListIndex; + +use super::refcacheslice::{self, CacheReferencesSlice, Exists}; + +pub struct CacheReferences { + // dense segments are unordered (spooky!) slices of content we do! know about. + // the u64 in the hashmap represents a kind of "segment identifier" + dense_segments: HashMap>, + + top_bounded_identifier: Option, + bottom_bounded_identifier: Option, +} + +impl Default for CacheReferences { + fn default() -> Self { + Self::new() + } +} + +impl CacheReferences { + pub fn new() -> Self { + Self { + dense_segments: HashMap::new(), + top_bounded_identifier: None, + bottom_bounded_identifier: None, + } + } + + pub fn append_bottom(&mut self, identifier: I) { + let mut id = None; + + for (segment_id, segment) in self.dense_segments.iter() { + if let Exists::Yes(_) = segment.get(AsyncListIndex::RelativeToBottom(0)) { + if id.is_some() { + panic!("There should only be one bottom bound segment"); + } + + id = Some(*segment_id) + } + } + + if let Some(id) = id { + self.dense_segments.get_mut(&id).unwrap().append_bottom(identifier); + } else { + self.insert(AsyncListIndex::RelativeToBottom(0), identifier, false, true); + } + } + + pub fn top_bound(&self) -> Option { + let index = self.top_bounded_identifier?; + let top_bound = self.dense_segments.get(&index).unwrap(); + + assert!(top_bound.is_bounded_at_top); + + Some(top_bound.item_references.first().unwrap().clone()) + } + + pub fn bottom_bound(&self) -> Option { + let index = self.bottom_bounded_identifier?; + let bottom_bound = self.dense_segments.get(&index).unwrap(); + + assert!(bottom_bound.is_bounded_at_bottom); + + Some(bottom_bound.item_references.last().unwrap().clone()) + } + + pub fn get(&self, index: AsyncListIndex) -> Exists { + for segment in self.dense_segments.values() { + let result = segment.get(index.clone()); + + if let Exists::Yes(value) = result { + return Exists::Yes(value); + } else if let Exists::No = result { + return Exists::No; + } + } + + Exists::Unknown + } + + /// you mut **KNOW** that the item you are inserting is not: + /// - directly next to (Before or After) **any** item in the list + /// - the first or last item in the list + pub fn insert_detached(&mut self, item: I) { + self.dense_segments.insert( + rand::random(), + CacheReferencesSlice { + is_bounded_at_top: false, + is_bounded_at_bottom: false, + + item_references: vec![item], + }, + ); + } + + pub fn insert(&mut self, index: AsyncListIndex, item: I, is_top: bool, is_bottom: bool) { + // insert routine is really complex: + // an insert can "join" together 2 segments + // an insert can append to a segment + // or an insert can construct a new segment + + let mut segments = vec![]; + + for (i, segment) in self.dense_segments.iter() { + if let Some(position) = segment.can_insert(index.clone()) { + segments.push((position, *i)); + } + } + + if segments.is_empty() { + let id = rand::random(); + + self.dense_segments.insert( + id, + CacheReferencesSlice { + is_bounded_at_top: is_top, + is_bounded_at_bottom: is_bottom, + + item_references: vec![item], + }, + ); + + if is_bottom { + self.bottom_bounded_identifier = Some(id); + } + + if is_top { + self.top_bounded_identifier = Some(id); + } + } else if segments.len() == 1 { + self.dense_segments.get_mut(&segments[0].1).unwrap().insert(index.clone(), item, is_bottom, is_top); + + if is_top { + self.top_bounded_identifier = Some(segments[0].1) + } + if is_bottom { + self.bottom_bounded_identifier = Some(segments[0].1) + } + } else if segments.len() == 2 { + assert!(!is_top); + assert!(!is_bottom); + + let (li, ri) = match (segments[0], segments[1]) { + ((refcacheslice::Position::After, lp), (refcacheslice::Position::Before, rp)) => (lp, rp), + ((refcacheslice::Position::Before, rp), (refcacheslice::Position::After, lp)) => (lp, rp), + + _ => panic!("How are there two candidates that aren't (Before, After) or (After, Before)?"), + }; + + let (left, right) = if li < ri { + let right = self.dense_segments.remove(&ri).unwrap(); + let left = self.dense_segments.remove(&li).unwrap(); + + (left, right) + } else { + let left = self.dense_segments.remove(&li).unwrap(); + let right = self.dense_segments.remove(&ri).unwrap(); + + (left, right) + }; + + let mut merged = left.item_references; + + merged.push(item); + + merged.extend(right.item_references); + + let id = rand::random(); + + self.dense_segments.insert( + id, + CacheReferencesSlice { + is_bounded_at_top: left.is_bounded_at_top, + is_bounded_at_bottom: right.is_bounded_at_bottom, + + item_references: merged, + }, + ); + + if left.is_bounded_at_top { + self.top_bounded_identifier = Some(id); + } + + if right.is_bounded_at_bottom { + self.bottom_bounded_identifier = Some(id); + } + } else { + panic!("Impossible state") + } + } +} + +impl Debug for CacheReferences { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CacheReferences") + .field("top_bounded_segment", &self.top_bounded_identifier) + .field("bottom_bounded_segment", &self.bottom_bounded_identifier) + .field("dense_segments", &self.dense_segments) + .finish() + } +} diff --git a/src/cache/src/async_list/refcacheslice.rs b/src/cache/src/async_list/refcacheslice.rs new file mode 100644 index 0000000..0a7bfea --- /dev/null +++ b/src/cache/src/async_list/refcacheslice.rs @@ -0,0 +1,134 @@ +use std::fmt::Debug; + +use scope_chat::async_list::AsyncListIndex; + +pub struct CacheReferencesSlice { + pub is_bounded_at_top: bool, + pub is_bounded_at_bottom: bool, + + // the vec's 0th item is the top, and it's last item is the bottom + // the vec MUST NOT be empty. + pub(super) item_references: Vec, +} + +pub enum Exists { + Yes(T), + No, + Unknown, +} + +impl CacheReferencesSlice { + fn find_index_of(&self, item: I) -> Option { + for (haystack, index) in self.item_references.iter().zip(0..) { + if *haystack == item { + return Some(index); + } + } + + None + } + + fn get_index(&self, index: AsyncListIndex) -> Option { + match index { + AsyncListIndex::RelativeToBottom(count) if self.is_bounded_at_bottom => Some((self.item_references.len() as isize) - (1 + (count as isize))), + + AsyncListIndex::RelativeToTop(count) if self.is_bounded_at_top => Some(count as isize), + + AsyncListIndex::After(item) => Some((self.find_index_of(item)? as isize) + 1), + + AsyncListIndex::Before(item) => Some((self.find_index_of(item)? as isize) - 1), + + _ => None, + } + } + + pub fn append_bottom(&mut self, index: I) { + assert!(self.is_bounded_at_bottom); + + self.item_references.push(index); + } + + pub fn get(&self, index: AsyncListIndex) -> Exists { + let index = self.get_index(index); + + if let Some(index) = index { + if index < 0 { + if self.is_bounded_at_top { + return Exists::No; + } else { + return Exists::Unknown; + } + } + + if index as usize >= self.item_references.len() { + if self.is_bounded_at_bottom { + return Exists::No; + } else { + return Exists::Unknown; + } + } + + Exists::Yes(self.item_references.get(index as usize).cloned().unwrap()) + } else { + Exists::Unknown + } + } + + pub fn can_insert(&self, index: AsyncListIndex) -> Option { + match index { + AsyncListIndex::After(item) => self.find_index_of(item).map(|idx| { + if idx == (self.item_references.len() - 1) { + Position::After + } else { + Position::Inside + } + }), + AsyncListIndex::Before(item) => self.find_index_of(item).map(|idx| if idx == 0 { Position::Before } else { Position::Inside }), + + _ => panic!("TODO: Figure out what well-defined behaviour for what should occur for inserting relative to top or bottom"), + } + } + + pub fn insert(&mut self, index: AsyncListIndex, value: I, is_bottom: bool, is_top: bool) { + if is_bottom { + self.is_bounded_at_bottom = true + } + if is_top { + self.is_bounded_at_top = true + } + + match index { + AsyncListIndex::After(item) => { + let i = self.find_index_of(item).unwrap(); + + self.item_references.insert(i + 1, value); + } + AsyncListIndex::Before(item) => { + let i = self.find_index_of(item).unwrap(); + + self.item_references.insert(i, value); + } + + _ => panic!("TODO: Figure out what well-defined behaviour for what should occur for inserting relative to top or bottom"), + } + } +} + +impl Debug for CacheReferencesSlice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CacheReferences") + .field("is_bounded_at_top", &self.is_bounded_at_top) + .field("is_bounded_at_bottom", &self.is_bounded_at_bottom) + .field("item_references", &self.item_references) + .finish() + } +} + +#[derive(Clone, Copy)] +pub enum Position { + /// Closer to the top + Before, + /// Closer to the bottom + After, + Inside, +} diff --git a/src/cache/src/async_list/tests.rs b/src/cache/src/async_list/tests.rs new file mode 100644 index 0000000..3970fd8 --- /dev/null +++ b/src/cache/src/async_list/tests.rs @@ -0,0 +1,241 @@ +use std::fmt::Debug; + +#[allow(unused_imports)] +use scope_chat::async_list::{AsyncListIndex, AsyncListItem, AsyncListResult}; + +#[allow(unused_imports)] +use crate::async_list::{refcacheslice::Exists, AsyncListCache}; + +#[allow(dead_code)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct ListItem(i64); + +impl AsyncListItem for ListItem { + type Identifier = i64; + + fn get_list_identifier(&self) -> Self::Identifier { + self.0 + } +} + +#[allow(dead_code)] +fn assert_query_exists(result: Exists>, item: I, is_top_in: bool, is_bottom_in: bool) { + if let Exists::Yes(AsyncListResult { content, is_top, is_bottom }) = result { + assert_eq!(content, item); + assert_eq!(is_top, is_top_in); + assert_eq!(is_bottom, is_bottom_in); + } else { + panic!("Expected eq yes") + } +} + +#[test] +pub fn cache_can_append_bottom_in_unbounded_state() { + let mut cache = AsyncListCache::::new(); + + cache.append_bottom(ListItem(0)); + + assert_eq!(cache.bounded_at_bottom_by(), Some(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(0)), ListItem(0), false, true); +} + +#[test] +pub fn cache_can_append_bottom_many_times_successfully() { + let mut cache = AsyncListCache::::new(); + + cache.append_bottom(ListItem(0)); + + assert_eq!(cache.bounded_at_bottom_by(), Some(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(0)), ListItem(0), false, true); + + cache.append_bottom(ListItem(1)); + + assert_eq!(cache.bounded_at_bottom_by(), Some(1)); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(0)), ListItem(1), false, true); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(1)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, true); + + cache.append_bottom(ListItem(2)); + + assert_eq!(cache.bounded_at_bottom_by(), Some(2)); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(0)), ListItem(2), false, true); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(1)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::RelativeToBottom(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(1)), ListItem(2), false, true); +} + +#[test] +pub fn cache_can_work_unlocated() { + let mut cache = AsyncListCache::::new(); + + cache.insert_detached(ListItem(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + + cache.insert(AsyncListIndex::After(0), ListItem(2), false, false); + + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::Before(0), ListItem(-2), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(0), false, false); +} + +#[test] +pub fn cache_can_insert_between() { + let mut cache = AsyncListCache::::new(); + + cache.insert_detached(ListItem(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + + cache.insert(AsyncListIndex::After(0), ListItem(2), false, false); + + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::Before(0), ListItem(-2), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::After(-2), ListItem(-1), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&-1), Some(ListItem(-1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(-1)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-1)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::Before(2), ListItem(1), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&-1), Some(ListItem(-1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(1)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(-1)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-1)), ListItem(0), false, false); + + let mut cache = AsyncListCache::::new(); + + cache.insert_detached(ListItem(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + + cache.insert(AsyncListIndex::After(0), ListItem(2), false, false); + + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::Before(0), ListItem(-2), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::Before(0), ListItem(-1), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&-1), Some(ListItem(-1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(-1)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-1)), ListItem(0), false, false); + + cache.insert(AsyncListIndex::After(0), ListItem(1), false, false); + assert_eq!(cache.find(&-2), Some(ListItem(-2))); + assert_eq!(cache.find(&-1), Some(ListItem(-1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_query_exists(cache.get(AsyncListIndex::After(1)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(0)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(-1)), ListItem(-2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-2)), ListItem(-1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(-1)), ListItem(0), false, false); +} + +#[test] +pub fn cache_can_merge() { + let mut cache = AsyncListCache::::new(); + + cache.insert_detached(ListItem(0)); + assert_eq!(cache.find(&0), Some(ListItem(0))); + + cache.insert(AsyncListIndex::After(0), ListItem(1), false, false); + + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); + + cache.insert_detached(ListItem(4)); + assert_eq!(cache.find(&4), Some(ListItem(4))); + + cache.insert(AsyncListIndex::Before(4), ListItem(3), false, false); + + assert_eq!(cache.find(&4), Some(ListItem(4))); + assert_eq!(cache.find(&3), Some(ListItem(3))); + assert_query_exists(cache.get(AsyncListIndex::After(3)), ListItem(4), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(4)), ListItem(3), false, false); + + cache.insert(AsyncListIndex::Before(3), ListItem(2), false, false); + cache.insert(AsyncListIndex::After(1), ListItem(2), false, false); + + assert_eq!(cache.find(&4), Some(ListItem(4))); + assert_eq!(cache.find(&3), Some(ListItem(3))); + assert_eq!(cache.find(&2), Some(ListItem(2))); + assert_eq!(cache.find(&1), Some(ListItem(1))); + assert_eq!(cache.find(&0), Some(ListItem(0))); + assert_query_exists(cache.get(AsyncListIndex::After(3)), ListItem(4), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(4)), ListItem(3), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(2)), ListItem(3), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(3)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(1)), ListItem(2), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(2)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::After(0)), ListItem(1), false, false); + assert_query_exists(cache.get(AsyncListIndex::Before(1)), ListItem(0), false, false); +} diff --git a/src/cache/src/lib.rs b/src/cache/src/lib.rs new file mode 100644 index 0000000..ce2b145 --- /dev/null +++ b/src/cache/src/lib.rs @@ -0,0 +1 @@ +pub mod async_list; diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs new file mode 100644 index 0000000..c94cb83 --- /dev/null +++ b/src/chat/src/async_list.rs @@ -0,0 +1,58 @@ +use std::{fmt::Debug, future::Future, hash::Hash}; + +pub trait AsyncList { + type Content: AsyncListItem; + + fn bounded_at_top_by(&self) -> impl Future::Identifier>>; + fn get( + &self, + index: AsyncListIndex<::Identifier>, + ) -> impl Future>> + Send; + fn find(&self, identifier: &::Identifier) -> impl Future>; + fn bounded_at_bottom_by(&self) -> impl Future::Identifier>>; +} + +pub trait AsyncListItem: Clone + Debug { + type Identifier: Eq + Hash + Clone + Send + Debug; + + fn get_list_identifier(&self) -> Self::Identifier; +} + +#[derive(Clone)] +pub enum AsyncListIndex { + RelativeToTop(usize), + /// Before is closer to the top + Before(I), + + RelativeToBottom(usize), + /// After is closer to the bottom + After(I), +} + +impl Copy for AsyncListIndex {} + +impl Debug for AsyncListIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::After(i) => f.debug_tuple("AsyncListIndex::After").field(i).finish()?, + Self::Before(i) => f.debug_tuple("AsyncListIndex::Before").field(i).finish()?, + Self::RelativeToTop(i) => f.debug_tuple("AsyncListIndex::RelativeToTop").field(i).finish()?, + Self::RelativeToBottom(i) => f.debug_tuple("AsyncListIndex::RelativeToBottom").field(i).finish()?, + }; + + Ok(()) + } +} + +#[derive(Clone)] +pub struct AsyncListResult { + pub content: T, + pub is_top: bool, + pub is_bottom: bool, +} + +impl Debug for AsyncListResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AsyncListResult").field("content", &self.content).field("is_top", &self.is_top).field("is_bottom", &self.is_bottom).finish() + } +} diff --git a/src/chat/src/channel.rs b/src/chat/src/channel.rs index 5b3d914..12d2a3b 100644 --- a/src/chat/src/channel.rs +++ b/src/chat/src/channel.rs @@ -1,8 +1,8 @@ use tokio::sync::broadcast; -use crate::message::Message; +use crate::{async_list::AsyncList, message::Message}; -pub trait Channel: Clone { +pub trait Channel: AsyncList + Send + Sync + Clone { type Message: Message; fn get_receiver(&self) -> broadcast::Receiver; diff --git a/src/chat/src/lib.rs b/src/chat/src/lib.rs index 96c248d..29fe6c3 100644 --- a/src/chat/src/lib.rs +++ b/src/chat/src/lib.rs @@ -1,2 +1,3 @@ +pub mod async_list; pub mod channel; pub mod message; diff --git a/src/chat/src/message.rs b/src/chat/src/message.rs index f805b36..5ffea52 100644 --- a/src/chat/src/message.rs +++ b/src/chat/src/message.rs @@ -1,7 +1,9 @@ use chrono::{DateTime, Utc}; use gpui::Element; -pub trait Message: Clone { +use crate::async_list::AsyncListItem; + +pub trait Message: Clone + AsyncListItem + Send { fn get_author(&self) -> &impl MessageAuthor; fn get_content(&self) -> impl Element; fn get_identifier(&self) -> String; diff --git a/src/discord/Cargo.toml b/src/discord/Cargo.toml index 417daef..e54d714 100644 --- a/src/discord/Cargo.toml +++ b/src/discord/Cargo.toml @@ -12,3 +12,4 @@ scope-chat = { version = "0.1.0", path = "../chat" } serenity = { git = "https://github.com/scopeclient/serenity", version = "0.13.0" } tokio = "1.41.1" chrono.workspace = true +scope-backend-cache = { version = "0.1.0", path = "../cache" } diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index f8ee153..eeb6590 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,8 +1,12 @@ use std::sync::Arc; -use scope_chat::channel::Channel; -use serenity::all::Timestamp; -use tokio::sync::broadcast; +use scope_backend_cache::async_list::{refcacheslice::Exists, AsyncListCache}; +use scope_chat::{ + async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, + channel::Channel, +}; +use serenity::all::{GetMessages, MessageId, Timestamp}; +use tokio::sync::{broadcast, Mutex, Semaphore}; use crate::{ client::DiscordClient, @@ -14,6 +18,8 @@ pub struct DiscordChannel { channel_id: Snowflake, receiver: broadcast::Receiver, client: Arc, + cache: Arc>>, + blocker: Semaphore, } impl DiscordChannel { @@ -26,6 +32,8 @@ impl DiscordChannel { channel_id, receiver, client, + cache: Arc::new(Mutex::new(AsyncListCache::new())), + blocker: Semaphore::new(1), } } } @@ -57,12 +65,177 @@ impl Channel for DiscordChannel { } } +const DISCORD_MESSAGE_BATCH_SIZE: u8 = 50; + +impl AsyncList for DiscordChannel { + async fn bounded_at_bottom_by(&self) -> Option { + let lock = self.cache.lock().await; + let cache_value = lock.bounded_at_top_by(); + + if let Some(v) = cache_value { + return Some(v); + }; + + self.client.get_messages(self.channel_id, GetMessages::new().limit(1)).await.first().map(|v| Snowflake { content: v.id.get() }) + } + + async fn bounded_at_top_by(&self) -> Option { + let lock = self.cache.lock().await; + let cache_value = lock.bounded_at_bottom_by(); + + if let Some(v) = cache_value { + return Some(v); + }; + + panic!("Unsupported") + } + + async fn find(&self, identifier: &Snowflake) -> Option { + let permit = self.blocker.acquire().await; + + let lock = self.cache.lock().await; + let cache_value = lock.find(identifier); + + drop(lock); + + if let Some(v) = cache_value { + return Some(v); + } + + let result = self.client.get_specific_message(self.channel_id, *identifier).await.map(|v| DiscordMessage::from_serenity(&v)); + + drop(permit); + + result + } + + async fn get(&self, index: AsyncListIndex) -> Option> { + let permit = self.blocker.acquire().await; + let mut lock = self.cache.lock().await; + let cache_value = lock.get(index); + + if let Exists::Yes(v) = cache_value { + return Some(v); + } else if let Exists::No = cache_value { + return None; + } + + let result: Option; + let mut is_top = false; + let mut is_bottom = false; + + match index { + AsyncListIndex::RelativeToTop(_) => todo!("Unsupported"), + AsyncListIndex::RelativeToBottom(index) => { + if index != 0 { + unimplemented!() + } + + let v = self.client.get_messages(self.channel_id, GetMessages::new().limit(DISCORD_MESSAGE_BATCH_SIZE)).await; + + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; + is_bottom = true; + is_top = v.len() == 1; + + result = v.first().map(DiscordMessage::from_serenity); + + let mut iter = v.iter(); + + let v = iter.next(); + + if let Some(v) = v { + let msg = DiscordMessage::from_serenity(v); + let mut id = msg.get_list_identifier(); + lock.append_bottom(msg); + + for message in iter { + let msg = DiscordMessage::from_serenity(message); + let nid = msg.get_list_identifier(); + + lock.insert(AsyncListIndex::Before(id), msg, false, is_end); + + id = nid; + } + }; + } + AsyncListIndex::After(message) => { + // NEWEST first + let v = self + .client + .get_messages( + self.channel_id, + GetMessages::new().after(MessageId::new(message.content)).limit(DISCORD_MESSAGE_BATCH_SIZE), + ) + .await; + let mut current_index: Snowflake = message; + + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; + is_bottom = is_end && v.len() == 1; + + result = v.last().map(DiscordMessage::from_serenity); + + for (message, index) in v.iter().rev().zip(0..) { + lock.insert( + AsyncListIndex::After(current_index), + DiscordMessage::from_serenity(message), + false, + is_end && index == (v.len() - 1), + ); + + current_index = Snowflake { content: message.id.get() } + } + } + AsyncListIndex::Before(message) => { + let v = self + .client + .get_messages( + self.channel_id, + GetMessages::new().before(MessageId::new(message.content)).limit(DISCORD_MESSAGE_BATCH_SIZE), + ) + .await; + let mut current_index: Snowflake = message; + + println!("Discord gave us {:?} messages (out of {:?})", v.len(), DISCORD_MESSAGE_BATCH_SIZE); + + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; + is_top = is_end && v.len() == 1; + + result = v.first().map(DiscordMessage::from_serenity); + + for (message, index) in v.iter().zip(0..) { + lock.insert( + AsyncListIndex::Before(current_index), + DiscordMessage::from_serenity(message), + false, + is_end && index == (v.len() - 1), + ); + + current_index = Snowflake { content: message.id.get() } + } + } + }; + + drop(permit); + drop(lock); + + result.map(|v| AsyncListResult { + content: v, + is_top, + is_bottom, + }) + } + + type Content = DiscordMessage; +} + impl Clone for DiscordChannel { fn clone(&self) -> Self { Self { channel_id: self.channel_id, receiver: self.receiver.resubscribe(), client: self.client.clone(), + cache: self.cache.clone(), + blocker: Semaphore::new(1), } } } diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 4aa2781..b3d0c9c 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -1,16 +1,21 @@ use std::{ - collections::HashMap, sync::{Arc, OnceLock} + collections::HashMap, + sync::{Arc, OnceLock}, }; use serenity::{ - all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, Http, Message, Nonce, RawEventHandler}, async_trait + all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, GetMessages, Http, Message, MessageId, RawEventHandler}, + async_trait, }; use tokio::sync::{broadcast, RwLock}; use crate::{ + channel::DiscordChannel, message::{ - author::{DiscordMessageAuthor, DisplayName}, content::DiscordMessageContent, DiscordMessage - }, snowflake::Snowflake + author::{DiscordMessageAuthor, DisplayName}, + DiscordMessage, + }, + snowflake::Snowflake, }; #[allow(dead_code)] @@ -26,6 +31,7 @@ pub struct DiscordClient { channel_message_event_handlers: RwLock>>>, client: OnceLock, user: OnceLock, + channels: RwLock>>, } impl DiscordClient { @@ -65,6 +71,22 @@ impl DiscordClient { self.channel_message_event_handlers.write().await.entry(channel).or_default().push(sender); } + pub async fn channel(self: Arc, channel_id: Snowflake) -> Arc { + let self_clone = self.clone(); + let mut channels = self_clone.channels.write().await; + let existing = channels.get(&channel_id); + + if let Some(existing) = existing { + return existing.clone(); + } + + let new = Arc::new(DiscordChannel::new(self, channel_id).await); + + channels.insert(channel_id, new.clone()); + + new + } + pub async fn send_message(&self, channel_id: Snowflake, content: String, nonce: String) { ChannelId::new(channel_id.content) .send_message( @@ -74,6 +96,18 @@ impl DiscordClient { .await .unwrap(); } + + pub async fn get_messages(&self, channel_id: Snowflake, builder: GetMessages) -> Vec { + println!("Discord: get_messages: {:?}", builder); + // FIXME: proper error handling + ChannelId::new(channel_id.content).messages(self.discord().http.clone(), builder).await.unwrap() + } + + pub async fn get_specific_message(&self, channel_id: Snowflake, message_id: Snowflake) -> Option { + println!("Discord: get_specific_messages"); + // FIXME: proper error handling + Some(ChannelId::new(channel_id.content).message(self.discord().http.clone(), MessageId::new(message_id.content)).await.unwrap()) + } } struct RawClient(Arc); @@ -119,23 +153,7 @@ impl EventHandler for DiscordClient { if let Some(vec) = self.channel_message_event_handlers.read().await.get(&snowflake) { for sender in vec { - let _ = sender.send(DiscordMessage { - id: snowflake, - author: DiscordMessageAuthor { - display_name: DisplayName(msg.author.name.clone()), - icon: msg.author.avatar_url().unwrap_or(msg.author.default_avatar_url()), - id: msg.author.id.to_string(), - }, - content: DiscordMessageContent { - content: msg.content.clone(), - is_pending: false, - }, - nonce: msg.nonce.clone().map(|n| match n { - Nonce::Number(n) => n.to_string(), - Nonce::String(s) => s, - }), - creation_time: msg.timestamp, - }); + let _ = sender.send(DiscordMessage::from_serenity(&msg)); } } } diff --git a/src/discord/src/message/author.rs b/src/discord/src/message/author.rs index 39d05af..11e73f9 100644 --- a/src/discord/src/message/author.rs +++ b/src/discord/src/message/author.rs @@ -1,7 +1,7 @@ use gpui::{div, Element, IntoElement, ParentElement, RenderOnce, Styled, WindowContext}; use scope_chat::message::MessageAuthor; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DiscordMessageAuthor { pub display_name: DisplayName, pub icon: String, @@ -33,7 +33,7 @@ impl MessageAuthor for DiscordMessageAuthor { } } -#[derive(Clone, IntoElement)] +#[derive(Clone, IntoElement, Debug)] pub struct DisplayName(pub String); impl RenderOnce for DisplayName { diff --git a/src/discord/src/message/content.rs b/src/discord/src/message/content.rs index c22ba17..81b6a0e 100644 --- a/src/discord/src/message/content.rs +++ b/src/discord/src/message/content.rs @@ -1,6 +1,6 @@ use gpui::{div, IntoElement, ParentElement, RenderOnce, Styled, WindowContext}; -#[derive(Clone, IntoElement)] +#[derive(Clone, IntoElement, Debug)] pub struct DiscordMessageContent { pub content: String, pub is_pending: bool, diff --git a/src/discord/src/message/mod.rs b/src/discord/src/message/mod.rs index 8195696..b1eb8d4 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -1,15 +1,16 @@ +use author::{DiscordMessageAuthor, DisplayName}; use chrono::{DateTime, Utc}; -use author::DiscordMessageAuthor; use content::DiscordMessageContent; use gpui::{Element, IntoElement}; -use scope_chat::message::Message; +use scope_chat::{async_list::AsyncListItem, message::Message}; +use serenity::all::Nonce; use crate::snowflake::Snowflake; pub mod author; pub mod content; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DiscordMessage { pub content: DiscordMessageContent, pub author: DiscordMessageAuthor, @@ -18,6 +19,28 @@ pub struct DiscordMessage { pub creation_time: serenity::model::Timestamp, } +impl DiscordMessage { + pub fn from_serenity(msg: &serenity::all::Message) -> Self { + DiscordMessage { + id: Snowflake { content: msg.id.get() }, + author: DiscordMessageAuthor { + display_name: DisplayName(msg.author.name.clone()), + icon: msg.author.avatar_url().unwrap_or(msg.author.default_avatar_url()), + id: msg.author.id.to_string(), + }, + content: DiscordMessageContent { + content: msg.content.clone(), + is_pending: false, + }, + nonce: msg.nonce.clone().map(|n| match n { + Nonce::Number(n) => n.to_string(), + Nonce::String(s) => s, + }), + creation_time: msg.timestamp, + } + } +} + impl Message for DiscordMessage { fn get_author(&self) -> &impl scope_chat::message::MessageAuthor { &self.author @@ -47,3 +70,11 @@ impl Message for DiscordMessage { DateTime::from_timestamp_millis(ts) } } + +impl AsyncListItem for DiscordMessage { + type Identifier = Snowflake; + + fn get_list_identifier(&self) -> Self::Identifier { + self.id + } +} diff --git a/src/ui/Cargo.toml b/src/ui/Cargo.toml index 924cc19..f252266 100644 --- a/src/ui/Cargo.toml +++ b/src/ui/Cargo.toml @@ -21,6 +21,7 @@ reqwest_client = { git = "https://github.com/huacnlee/zed.git", branch = "export scope-chat = { version = "0.1.0", path = "../chat" } scope-util = { version = "0.1.0", path = "../util" } scope-backend-discord = { version = "0.1.0", path = "../discord" } +scope-backend-cache = { version = "0.1.0", path = "../cache" } dotenv = "0.15.0" env_logger = "0.11.5" tokio = { version = "1.41.1", features = ["full"] } @@ -29,6 +30,7 @@ log = "0.4.22" random-string = "1.1.0" rust-embed = "8.5.0" chrono.workspace = true +catty = "0.1.5" [features] default = ["gpui/x11"] diff --git a/src/ui/src/app.rs b/src/ui/src/app.rs index 54449bb..407ab44 100644 --- a/src/ui/src/app.rs +++ b/src/ui/src/app.rs @@ -1,11 +1,11 @@ use components::theme::ActiveTheme; use gpui::{div, img, rgb, Context, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext}; -use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, message::DiscordMessage, snowflake::Snowflake}; +use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, snowflake::Snowflake}; use crate::channel::ChannelView; pub struct App { - channel: Model>>>, + channel: Model>>>, } impl App { @@ -32,12 +32,14 @@ impl App { ) .await; - let view = context.new_view(|cx| ChannelView::::create(cx, channel)).unwrap(); + let view = context.new_view(|cx| ChannelView::::create(cx, channel)).unwrap(); - async_channel.update(&mut context, |a, b| { - *a = Some(view); - b.notify() - }) + async_channel + .update(&mut context, |a, b| { + *a = Some(view); + b.notify() + }) + .unwrap(); }) .detach(); diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index 31216d3..7eef140 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,85 +1,369 @@ -use gpui::{div, IntoElement, ListAlignment, ListState, ParentElement, Pixels}; +use std::sync::Arc; -use scope_chat::message::{Message, MessageAuthor}; +use gpui::{div, list, rgb, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, ViewContext}; +use scope_chat::{ + async_list::{AsyncListIndex, AsyncListItem}, + channel::Channel, + message::Message, +}; +use tokio::sync::RwLock; use super::message::{message, MessageGroup}; -#[derive(Clone)] -pub struct MessageList { - messages: Vec>, +#[derive(Clone, Copy)] +struct ListStateDirtyState { + pub new_items: usize, + pub shift: usize, } -impl Default for MessageList { - fn default() -> Self { - Self::new() - } +#[derive(Clone, Copy)] +struct BoundFlags { + pub before: bool, + pub after: bool, } -impl MessageList { - pub fn new() -> MessageList { - Self { messages: Vec::default() } - } +#[derive(Debug)] +pub enum Element { + Unresolved, + Resolved(T), +} - pub fn add_external_message(&mut self, message: M) { - if let Some(nonce) = message.get_nonce() { - let mut removal_index: Option = None; +pub struct MessageListComponent +where + C::Content: 'static, +{ + list: Arc>, + cache: Model>>>, + overdraw: Pixels, - for (group, index) in self.messages.iter_mut().zip(0..) { - let matching = group.find_matching(nonce); + // top, bottom + bounds_flags: Model, - if let Some(matching) = matching { - if group.size() == 1 { - removal_index = Some(index); - } else { - group.remove(matching); + list_state: Model>, + list_state_dirty: Model>, +} + +pub enum StartAt { + Bottom, + Top, +} + +impl MessageListComponent +where + T: 'static, +{ + pub fn create(cx: &mut ViewContext, list: T, overdraw: Pixels) -> Self { + let cache = cx.new_model(|_| Default::default()); + let list_state = cx.new_model(|_| None); + let list_state_dirty = cx.new_model(|_| None); + + let lsc = list_state.clone(); + + cx.observe(&cache, move |c, _, cx| { + let ls = c.list_state(cx); + + lsc.update(cx, |v, _| *v = Some(ls)); + + cx.notify(); + }) + .detach(); + + let lsc = list_state.clone(); + + cx.observe(&list_state_dirty, move |c, _, cx| { + let ls = c.list_state(cx); + + lsc.update(cx, |v, _| *v = Some(ls)); + + cx.notify(); + }) + .detach(); + + MessageListComponent { + list: Arc::new(RwLock::new(list)), + cache, + overdraw, + bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), + list_state, + list_state_dirty, + } + } + + pub fn append_message(&mut self, cx: &mut ViewContext, message: T::Message) { + self.cache.update(cx, |borrow, cx| { + for item in borrow.iter_mut() { + if let Element::Resolved(Some(haystack)) = item { + if haystack.get_nonce() == message.get_nonce() { + *item = Element::Resolved(Some(message)); + + cx.notify(); + return; } } } - if let Some(removal_index) = removal_index { - self.messages.remove(removal_index); + if let Some(Element::Resolved(None)) = borrow.last() { + borrow.pop(); } - } - let last = self.messages.last_mut(); + borrow.push(Element::Resolved(Some(message))); + borrow.push(Element::Resolved(None)); - if let Some(last_group) = last { - if last_group.get_author().get_id() == message.get_author().get_id() && message.should_group(last_group.last()) { - last_group.add(message); - } else { - self.messages.push(MessageGroup::new(message)); - } - } else { - self.messages.push(MessageGroup::new(message)); - } + cx.update_model(&self.list_state_dirty, |v, _| *v = Some(ListStateDirtyState { new_items: 1, shift: 0 })); + + cx.notify(); + }); } - pub fn add_pending_message(&mut self, pending_message: M) { - if let Some(last) = self.messages.last_mut() { - if last.get_author().get_id() == pending_message.get_author().get_id() && pending_message.should_group(last.last()) { - last.add(pending_message); + fn list_state(&self, cx: &mut gpui::ViewContext) -> ListState { + let bounds_model = self.bounds_flags.clone(); + + let list_state_dirty = *self.list_state_dirty.read(cx); + + let mut added_elements_bottom = 0; + let mut shift = 0; + + let mut remaining_shift = list_state_dirty.map(|v| v.shift).unwrap_or(0); + let mut remaining_gap_new_items = self.cache.read(cx).len() - list_state_dirty.map(|v| v.new_items).unwrap_or(0); + + let mut groups = vec![]; + + for (item, index) in self.cache.read(cx).iter().zip(0..) { + let mut items_added: usize = 0; + + match item { + Element::Unresolved => groups.push(Element::Unresolved), + Element::Resolved(None) => groups.push(Element::Resolved(None)), + Element::Resolved(Some(m)) => match groups.last_mut() { + None | Some(Element::Unresolved) | Some(Element::Resolved(None)) => { + items_added += 1; + groups.push(Element::Resolved(Some(MessageGroup::new(m.clone())))); + } + Some(Element::Resolved(Some(old_group))) => { + if m.get_author() == old_group.last().get_author() && m.should_group(old_group.last()) { + old_group.add(m.clone()); + } else { + items_added += 1; + groups.push(Element::Resolved(Some(MessageGroup::new(m.clone())))); + } + } + }, + } + + if index == 0 { + continue; + } + + if remaining_shift > 0 { + remaining_shift -= 1; + shift += items_added; + } + + if remaining_gap_new_items == 0 { + added_elements_bottom = items_added; } else { - self.messages.push(MessageGroup::new(pending_message)); + remaining_gap_new_items -= 1; } - } else { - self.messages.push(MessageGroup::new(pending_message)); } - } - pub fn length(&self) -> usize { - self.messages.len() - } + let len = groups.len(); + + let new_list_state = ListState::new( + if len == 0 { 1 } else { len + 2 }, + ListAlignment::Bottom, + self.overdraw, + move |idx, cx| { + if len == 0 { + cx.update_model(&bounds_model, |v, _| v.after = true); + + return div().into_any_element(); + } + + if idx == 0 { + cx.update_model(&bounds_model, |v, _| v.before = true); + + div() + } else if idx == len + 1 { + cx.update_model(&bounds_model, |v, _| v.after = true); + + div() + } else { + match &groups[idx - 1] { + Element::Unresolved => div().text_color(rgb(0xFFFFFF)).child("Loading..."), + Element::Resolved(None) => div(), // we've hit the ends + Element::Resolved(Some(group)) => div().child(message(group.clone())), + } + } + .into_any_element() + }, + ); + + let old_list_state = self.list_state.read(cx); + + if let Some(old_list_state) = old_list_state { + let mut new_scroll_top = old_list_state.logical_scroll_top(); + + if old_list_state.logical_scroll_top().item_ix == old_list_state.item_count() { + new_scroll_top.item_ix += added_elements_bottom; + + if added_elements_bottom > 0 { + new_scroll_top.offset_in_item = Pixels(0.); + } + } - pub fn get(&self, index: usize) -> Option<&MessageGroup> { - self.messages.get(index) + new_scroll_top.item_ix += shift; + + new_list_state.scroll_to(new_scroll_top); + }; + + self.list_state.update(cx, |v, _| *v = Some(new_list_state.clone())); + + new_list_state } - pub fn create_list_state(&self) -> ListState { - let clone = self.clone(); + fn update(&mut self, cx: &mut gpui::ViewContext) { + let mut dirty = None; + + let mut flags = *self.bounds_flags.read(cx); - ListState::new(clone.length(), ListAlignment::Bottom, Pixels(20.), move |idx, _cx| { - let item = clone.get(idx).unwrap().clone(); - div().child(message(item)).into_any_element() + // update bottom + if flags.after { + let cache_model = self.cache.clone(); + let list_handle = self.list.clone(); + + self.cache.update(cx, |borrow, cx| { + let last = borrow.last(); + + let index = if let Some(last) = last { + AsyncListIndex::After(if let Element::Resolved(Some(v)) = last { + v.get_list_identifier() + } else { + flags.after = false; + return; + }) + } else { + AsyncListIndex::RelativeToBottom(0) + }; + + borrow.push(Element::Unresolved); + + let insert_index = borrow.len() - 1; + let mut async_ctx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let (sender, receiver) = catty::oneshot(); + + tokio::spawn(async move { + sender.send(list_handle.read().await.get(index).await).unwrap(); + }); + + let v = receiver.await.unwrap(); + + cache_model + .update(&mut async_ctx, |borrow, cx| { + borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); + + cx.notify(); + }) + .unwrap(); + }) + .detach(); + + dirty = Some(ListStateDirtyState { new_items: 1, shift: 0 }); + }); + } + + // update top + if flags.before { + let cache_model = self.cache.clone(); + let list_handle = self.list.clone(); + + self.cache.update(cx, |borrow, cx| { + let first = borrow.first(); + + let index = if let Some(first) = first { + AsyncListIndex::Before(if let Element::Resolved(Some(v)) = first { + v.get_list_identifier() + } else { + flags.before = false; + return; + }) + } else { + flags.before = false; + return; + }; + + borrow.insert(0, Element::Unresolved); + + let insert_index = 0; + let mut async_ctx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let (sender, receiver) = catty::oneshot(); + + tokio::spawn(async move { + sender.send(list_handle.read().await.get(index).await).unwrap(); + }); + + let v = receiver.await.unwrap(); + + cache_model + .update(&mut async_ctx, |borrow, cx| { + borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); + cx.notify(); + }) + .unwrap(); + }) + .detach(); + + dirty = { + let mut v = dirty.unwrap_or(ListStateDirtyState { new_items: 0, shift: 0 }); + + v.shift += 1; + + Some(v) + }; + }); + } + + self.list_state_dirty.update(cx, |v, _| { + *v = dirty; + }); + + if dirty.is_some() { + cx.notify(); + } + + self.bounds_flags.update(cx, |v, _| { + if flags.after { + v.after = false; + } + + if flags.before { + v.before = false; + } }) } } + +impl Render for MessageListComponent { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + self.update(cx); + + let ls = if let Some(v) = self.list_state.read(cx).clone() { + v + } else { + let list_state = self.list_state(cx); + + let lsc = list_state.clone(); + + self.list_state.update(cx, move |v, _| *v = Some(lsc)); + + list_state + }; + + div().w_full().h_full().child(list(ls).w_full().h_full()) + } +} diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 62f9c1c..f61e279 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -2,33 +2,42 @@ pub mod message; pub mod message_list; use components::input::{InputEvent, TextInput}; -use gpui::{div, list, Context, ListState, Model, ParentElement, Render, Styled, View, VisualContext}; -use message_list::MessageList; -use scope_chat::{channel::Channel, message::Message}; +use gpui::{div, ParentElement, Pixels, Render, Styled, View, VisualContext}; +use message_list::MessageListComponent; +use scope_chat::channel::Channel; -pub struct ChannelView { - list_state: ListState, - list_model: Model>, +pub struct ChannelView { + list_view: View>, message_input: View, } -impl ChannelView { - pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: impl Channel + 'static) -> Self { - let state_model = ctx.new_model(|_cx| MessageList::::new()); +impl ChannelView { + pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: C) -> Self { + let channel_listener = channel.get_receiver(); - let async_model = state_model.clone(); + let c2 = channel.clone(); + + let list_view = ctx.new_view(|cx| MessageListComponent::create(cx, channel, Pixels(30.))); + + let async_model = list_view.clone(); let mut async_ctx = ctx.to_async(); - let channel_listener = channel.clone(); ctx .foreground_executor() .spawn(async move { loop { - let message = channel_listener.get_receiver().recv().await.unwrap(); + let (sender, receiver) = catty::oneshot(); + + let mut l = channel_listener.resubscribe(); + + tokio::spawn(async move { + sender.send(l.recv().await).unwrap(); + }); + let message = receiver.await.unwrap().unwrap(); async_model .update(&mut async_ctx, |data, ctx| { - data.add_external_message(message); + data.append_message(ctx, message); ctx.notify(); }) .unwrap(); @@ -36,13 +45,6 @@ impl ChannelView { }) .detach(); - ctx - .observe(&state_model, |this: &mut ChannelView, model, cx| { - this.list_state = model.read(cx).create_list_state(); - cx.notify(); - }) - .detach(); - let message_input = ctx.new_view(|cx| { let mut input = components::input::TextInput::new(cx); @@ -51,41 +53,57 @@ impl ChannelView { input }); + let async_model = list_view.clone(); + ctx - .subscribe(&message_input, move |channel_view, text_input, input_event, ctx| { + .subscribe(&message_input, move |_, text_input, input_event, ctx| { if let InputEvent::PressEnter = input_event { let content = text_input.read(ctx).text().to_string(); if content.is_empty() { return; } - let channel_sender = channel.clone(); text_input.update(ctx, |text_input, cx| { text_input.set_text("", cx); }); let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); - let pending = channel_sender.send_message(content, nonce); + let pending = c2.send_message(content, nonce); + + let mut async_ctx = ctx.to_async(); + + let async_model = async_model.clone(); + + ctx + .foreground_executor() + .spawn(async move { + async_model + .update(&mut async_ctx, |data, ctx| { + data.append_message(ctx, pending); + ctx.notify(); + }) + .unwrap(); + }) + .detach(); - channel_view.list_model.update(ctx, move |v, _| { - v.add_pending_message(pending); - }); - channel_view.list_state = channel_view.list_model.read(ctx).create_list_state(); ctx.notify(); } }) .detach(); - ChannelView:: { - list_state: state_model.read(ctx).create_list_state(), - list_model: state_model, - message_input, - } + ChannelView:: { list_view, message_input } } } -impl Render for ChannelView { +impl Render for ChannelView { fn render(&mut self, _: &mut gpui::ViewContext) -> impl gpui::IntoElement { - div().flex().flex_col().w_full().h_full().p_6().child(list(self.list_state.clone()).w_full().h_full()).child(self.message_input.clone()) + div() + .flex() + .flex_col() + .w_full() + .h_full() + .p_6() + .child(div().w_full().h_full().flex().flex_col().child(self.list_view.clone())) + .child(self.message_input.clone()) } }