From 587f4a5ba36f3abf7d7e3b6aa2f44c90906b937a Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sat, 16 Nov 2024 14:59:13 -0500 Subject: [PATCH 01/16] started work on new datastructures + cache --- Cargo.lock | 9 ++++ Cargo.toml | 4 +- src/cache/Cargo.toml | 12 +++++ src/cache/src/async_list/mod.rs | 66 ++++++++++++++++++++++++++++ src/cache/src/async_list/refcache.rs | 47 ++++++++++++++++++++ src/cache/src/lib.rs | 1 + src/chat/src/async_list.rs | 24 ++++++++++ src/chat/src/channel.rs | 4 +- src/chat/src/lib.rs | 1 + src/chat/src/message.rs | 4 +- src/discord/src/channel/mod.rs | 25 ++++++++++- src/discord/src/client.rs | 25 ++++++++++- src/discord/src/message/mod.rs | 6 ++- 13 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 src/cache/Cargo.toml create mode 100644 src/cache/src/async_list/mod.rs create mode 100644 src/cache/src/async_list/refcache.rs create mode 100644 src/cache/src/lib.rs create mode 100644 src/chat/src/async_list.rs diff --git a/Cargo.lock b/Cargo.lock index ec2e2e8..fa1b0ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5447,6 +5447,15 @@ dependencies = [ "ui", ] +[[package]] +name = "scope-backend-cache" +version = "0.1.0" +dependencies = [ + "gpui", + "scope-chat", + "tokio", +] + [[package]] name = "scope-backend-discord" version = "0.1.0" 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..60a7e32 --- /dev/null +++ b/src/cache/Cargo.toml @@ -0,0 +1,12 @@ +[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", +] } +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..63ec2b1 --- /dev/null +++ b/src/cache/src/async_list/mod.rs @@ -0,0 +1,66 @@ +pub mod refcache; + +use std::{collections::HashMap, future::Future}; + +use refcache::CacheReferences; +use scope_chat::{ + async_list::{AsyncList, AsyncListIndex, AsyncListItem}, + channel::Channel, +}; +use tokio::sync::RwLock; + +pub struct AsyncListCache { + underlying: L, + cache_refs: RwLock::Identifier>>, + cache_map: RwLock::Identifier, L::Content>>, +} + +impl AsyncListCache {} + +impl AsyncList for AsyncListCache { + type Content = L::Content; + + async fn bounded_at_top_by(&self) -> Option<::Identifier> { + let refs_read_bound = self.cache_refs.read().await; + refs_read_bound.top_bound().cloned() + } + + async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { + let refs_read_bound = self.cache_refs.read().await; + refs_read_bound.bottom_bound().cloned() + } + + async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option { + let cache_read_handle = self.cache_refs.read().await; + let cache_result = cache_read_handle.get(index.clone()); + + if let Some(cache_result) = cache_result { + return Some(self.cache_map.read().await.get(&cache_result).unwrap().clone()); + }; + + let authoritative = self.underlying.get(index).await; + + if let Some(ref authoritative) = authoritative { + unimplemented!() + } + + authoritative + } + + async fn find(&self, identifier: &::Identifier) -> Option { + let cache_read_handle = self.cache_map.read().await; + let cache_result = cache_read_handle.get(identifier); + + if let Some(cache_result) = cache_result { + return Some(cache_result.clone()); + }; + + let authoritative = self.underlying.find(identifier).await; + + if let Some(ref authoritative) = authoritative { + self.cache_map.write().await.insert(identifier.clone(), authoritative.clone()); + } + + authoritative + } +} diff --git a/src/cache/src/async_list/refcache.rs b/src/cache/src/async_list/refcache.rs new file mode 100644 index 0000000..54851e2 --- /dev/null +++ b/src/cache/src/async_list/refcache.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use scope_chat::async_list::AsyncListIndex; + +struct CacheReferencesSlice { + is_bounded_at_top: bool, + 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. + item_references: Vec, +} + +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_segment: HashMap>, + + top_bounded_identifier: Option, + bottom_bounded_identifier: Option, +} + +impl CacheReferences { + pub fn top_bound(&self) -> Option<&I> { + self.top_bounded_identifier.map(|v| { + let top_bound = self.dense_segment.get(&v).unwrap(); + + assert!(top_bound.is_bounded_at_top); + + top_bound.item_references.first().unwrap() + }) + } + + pub fn bottom_bound(&self) -> Option<&I> { + self.bottom_bounded_identifier.map(|v| { + let bottom_bound = self.dense_segment.get(&v).unwrap(); + + assert!(bottom_bound.is_bounded_at_bottom); + + bottom_bound.item_references.last().unwrap() + }) + } + + pub fn get(&self, index: AsyncListIndex) -> Option { + unimplemented!() + } +} 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..39bc8ee --- /dev/null +++ b/src/chat/src/async_list.rs @@ -0,0 +1,24 @@ +use std::hash::Hash; + +pub trait AsyncList { + type Content: AsyncListItem; + + fn bounded_at_top_by(&self) -> impl std::future::Future::Identifier>>; + fn get(&self, index: AsyncListIndex<::Identifier>) -> impl std::future::Future>; + fn find(&self, identifier: &::Identifier) -> impl std::future::Future>; + fn bounded_at_bottom_by(&self) -> impl std::future::Future::Identifier>>; +} + +pub trait AsyncListItem: Clone { + type Identifier: Eq + Hash + Clone; +} + +#[derive(Clone)] +pub enum AsyncListIndex { + RelativeToTop(usize), + After(I), + Before(I), + RelativeToBottom(usize), +} + +impl Copy for AsyncListIndex {} diff --git a/src/chat/src/channel.rs b/src/chat/src/channel.rs index 5b3d914..119d847 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: Clone + AsyncList { 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..fd2db96 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 { fn get_author(&self) -> &impl MessageAuthor; fn get_content(&self) -> impl Element; fn get_identifier(&self) -> String; diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index f8ee153..c63d593 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use scope_chat::channel::Channel; +use scope_chat::{ + async_list::{AsyncList, AsyncListIndex, AsyncListItem}, + channel::Channel, +}; use serenity::all::Timestamp; use tokio::sync::broadcast; @@ -57,6 +60,26 @@ impl Channel for DiscordChannel { } } +impl AsyncList for DiscordChannel { + async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { + unimplemented!() + } + + async fn bounded_at_top_by(&self) -> Option<::Identifier> { + unimplemented!() + } + + async fn find(&self, identifier: &::Identifier) -> Option { + unimplemented!() + } + + async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option { + unimplemented!() + } + + type Content = DiscordMessage; +} + impl Clone for DiscordChannel { fn clone(&self) -> Self { Self { diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 4aa2781..3473ac9 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -8,9 +8,13 @@ use serenity::{ use tokio::sync::{broadcast, RwLock}; use crate::{ + channel::DiscordChannel, message::{ - author::{DiscordMessageAuthor, DisplayName}, content::DiscordMessageContent, DiscordMessage - }, snowflake::Snowflake + author::{DiscordMessageAuthor, DisplayName}, + content::DiscordMessageContent, + DiscordMessage, + }, + snowflake::{self, Snowflake}, }; #[allow(dead_code)] @@ -26,6 +30,7 @@ pub struct DiscordClient { channel_message_event_handlers: RwLock>>>, client: OnceLock, user: OnceLock, + channels: RwLock>>, } impl DiscordClient { @@ -65,6 +70,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( diff --git a/src/discord/src/message/mod.rs b/src/discord/src/message/mod.rs index 8195696..eba1591 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -2,7 +2,7 @@ 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 crate::snowflake::Snowflake; @@ -47,3 +47,7 @@ impl Message for DiscordMessage { DateTime::from_timestamp_millis(ts) } } + +impl AsyncListItem for DiscordMessage { + type Identifier = String; +} From b6831b7230dd0280a1b26b9a455d34b25a2f4cb1 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sun, 17 Nov 2024 01:24:23 -0500 Subject: [PATCH 02/16] incomplete --- Cargo.lock | 1 + src/cache/Cargo.toml | 1 + src/cache/src/async_list/mod.rs | 21 ++-- src/cache/src/async_list/refcache.rs | 134 ++++++++++++++++++---- src/cache/src/async_list/refcacheslice.rs | 87 ++++++++++++++ src/chat/src/async_list.rs | 2 + 6 files changed, 211 insertions(+), 35 deletions(-) create mode 100644 src/cache/src/async_list/refcacheslice.rs diff --git a/Cargo.lock b/Cargo.lock index fa1b0ce..e3ce5c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5452,6 +5452,7 @@ name = "scope-backend-cache" version = "0.1.0" dependencies = [ "gpui", + "rand 0.8.5", "scope-chat", "tokio", ] diff --git a/src/cache/Cargo.toml b/src/cache/Cargo.toml index 60a7e32..d04cd32 100644 --- a/src/cache/Cargo.toml +++ b/src/cache/Cargo.toml @@ -8,5 +8,6 @@ gpui = { git = "https://github.com/huacnlee/zed.git", branch = "export-platform- "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 index 63ec2b1..3428528 100644 --- a/src/cache/src/async_list/mod.rs +++ b/src/cache/src/async_list/mod.rs @@ -1,12 +1,10 @@ pub mod refcache; +pub mod refcacheslice; -use std::{collections::HashMap, future::Future}; +use std::collections::HashMap; use refcache::CacheReferences; -use scope_chat::{ - async_list::{AsyncList, AsyncListIndex, AsyncListItem}, - channel::Channel, -}; +use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; use tokio::sync::RwLock; pub struct AsyncListCache { @@ -22,26 +20,29 @@ impl AsyncList for AsyncListCache { async fn bounded_at_top_by(&self) -> Option<::Identifier> { let refs_read_bound = self.cache_refs.read().await; - refs_read_bound.top_bound().cloned() + refs_read_bound.top_bound().await } async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { let refs_read_bound = self.cache_refs.read().await; - refs_read_bound.bottom_bound().cloned() + refs_read_bound.bottom_bound().await } async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option { let cache_read_handle = self.cache_refs.read().await; - let cache_result = cache_read_handle.get(index.clone()); + let cache_result = cache_read_handle.get(index.clone()).await; if let Some(cache_result) = cache_result { return Some(self.cache_map.read().await.get(&cache_result).unwrap().clone()); }; - let authoritative = self.underlying.get(index).await; + let authoritative = self.underlying.get(index.clone()).await; if let Some(ref authoritative) = authoritative { - unimplemented!() + let identifier = authoritative.get_list_identifier(); + + self.cache_map.write().await.insert(identifier.clone(), authoritative.clone()); + self.cache_refs.write().await.insert(index, identifier.clone()); } authoritative diff --git a/src/cache/src/async_list/refcache.rs b/src/cache/src/async_list/refcache.rs index 54851e2..b8335e5 100644 --- a/src/cache/src/async_list/refcache.rs +++ b/src/cache/src/async_list/refcache.rs @@ -1,47 +1,131 @@ use std::collections::HashMap; use scope_chat::async_list::AsyncListIndex; +use tokio::{fs::read, sync::RwLock}; -struct CacheReferencesSlice { - is_bounded_at_top: bool, - is_bounded_at_bottom: bool, +use super::refcacheslice::{self, CacheReferencesSlice}; - // the vec's 0th item is the top, and it's last item is the bottom - // the vec MUST NOT be empty. - item_references: Vec, -} - -pub struct CacheReferences { +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_segment: HashMap>, + dense_segments: RwLock>>, top_bounded_identifier: Option, bottom_bounded_identifier: Option, } -impl CacheReferences { - pub fn top_bound(&self) -> Option<&I> { - self.top_bounded_identifier.map(|v| { - let top_bound = self.dense_segment.get(&v).unwrap(); +impl CacheReferences { + pub async fn top_bound(&self) -> Option { + let index = self.top_bounded_identifier?; + let read_handle = self.dense_segments.read().await; + let top_bound = read_handle.get(&index).unwrap(); - assert!(top_bound.is_bounded_at_top); + assert!(top_bound.is_bounded_at_top); - top_bound.item_references.first().unwrap() - }) + Some(top_bound.item_references.first().unwrap().clone()) } - pub fn bottom_bound(&self) -> Option<&I> { - self.bottom_bounded_identifier.map(|v| { - let bottom_bound = self.dense_segment.get(&v).unwrap(); + pub async fn bottom_bound(&self) -> Option { + let index = self.bottom_bounded_identifier?; + let read_handle = self.dense_segments.read().await; + let bottom_bound = read_handle.get(&index).unwrap(); - assert!(bottom_bound.is_bounded_at_bottom); + assert!(bottom_bound.is_bounded_at_bottom); - bottom_bound.item_references.last().unwrap() - }) + Some(bottom_bound.item_references.last().unwrap().clone()) } - pub fn get(&self, index: AsyncListIndex) -> Option { - unimplemented!() + pub async fn get(&self, index: AsyncListIndex) -> Option { + let read_handle = self.dense_segments.read().await; + + for segment in read_handle.values() { + if let Some(value) = segment.get(index.clone()) { + return Some(value) + } + } + + return None; + } + + /// 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 async fn insert_detached(&self, item: I) { + let mut mutation_handle = self.dense_segments.write().await; + + mutation_handle.insert(rand::random(), CacheReferencesSlice { + is_bounded_at_top: false, + is_bounded_at_bottom: false, + + item_references: vec![item], + }); + } + + pub async fn insert(&self, index: AsyncListIndex, item: I,) { + // 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![]; + + let read_handle = self.dense_segments.read().await; + + for (i, segment) in read_handle.iter() { + if let Some(position) = segment.can_insert(index.clone()) { + segments.push((position, i)); + } + } + + if segments.len() == 0 { + let mut mutation_handle = self.dense_segments.write().await; + + mutation_handle.insert(rand::random(), CacheReferencesSlice { + is_bounded_at_top: is_first, + is_bounded_at_bottom: is_last, + + item_references: vec![item], + }); + } else if segments.len() == 1 { + let mut mutation_handle = self.dense_segments.write().await; + + mutation_handle.get_mut(segments[0].1).unwrap().insert(index.clone(), item); + } else if segments.len() == 2 { + 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 mut mutation_handle = self.dense_segments.write().await; + + let (left, right) = if li < ri { + let right = mutation_handle.remove(&ri).unwrap(); + let left = mutation_handle.remove(&li).unwrap(); + + (left, right) + } else { + let left = mutation_handle.remove(&li).unwrap(); + let right = mutation_handle.remove(&ri).unwrap(); + + (left, right) + }; + + let mut merged = left.item_references; + + merged.push(item); + + merged.extend(right.item_references.into_iter()); + + mutation_handle.insert(rand::random(), CacheReferencesSlice { + is_bounded_at_top: left.is_bounded_at_top, + is_bounded_at_bottom: right.is_bounded_at_bottom, + + item_references: merged, + }); + } else { + panic!("Impossible state") + } } } diff --git a/src/cache/src/async_list/refcacheslice.rs b/src/cache/src/async_list/refcacheslice.rs new file mode 100644 index 0000000..7497268 --- /dev/null +++ b/src/cache/src/async_list/refcacheslice.rs @@ -0,0 +1,87 @@ +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, +} + +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) - (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 get(&self, index: AsyncListIndex) -> Option { + let index = self.get_index(index)?; + + if index < 0 { + return None; + } + + self.item_references.get(index as usize).cloned() + } + + 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 should occur for inserting relative to top or bottom") + } + } + + pub fn insert(&mut self, index: AsyncListIndex, value: I) { + 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 should occur for inserting relative to top or bottom") + } + } +} + +#[derive(Clone, Copy)] +pub enum Position { + Before, + Inside, + After, +} \ No newline at end of file diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index 39bc8ee..023842e 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -11,6 +11,8 @@ pub trait AsyncList { pub trait AsyncListItem: Clone { type Identifier: Eq + Hash + Clone; + + fn get_list_identifier(&self) -> Self::Identifier; } #[derive(Clone)] From 665356f3474f48d78867003c0e3cb686e3e18bcb Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Sun, 17 Nov 2024 19:15:29 -0500 Subject: [PATCH 03/16] finish async list & async list cache --- src/cache/src/async_list/mod.rs | 28 ++++-- src/cache/src/async_list/refcache.rs | 58 +++++++---- src/cache/src/async_list/refcacheslice.rs | 116 +++++++++++----------- src/chat/src/async_list.rs | 16 ++- src/discord/src/channel/mod.rs | 8 +- src/discord/src/message/mod.rs | 6 +- src/ui/src/app.rs | 1 + 7 files changed, 136 insertions(+), 97 deletions(-) diff --git a/src/cache/src/async_list/mod.rs b/src/cache/src/async_list/mod.rs index 3428528..0980f91 100644 --- a/src/cache/src/async_list/mod.rs +++ b/src/cache/src/async_list/mod.rs @@ -1,10 +1,10 @@ pub mod refcache; pub mod refcacheslice; -use std::collections::HashMap; +use std::{collections::HashMap, process::id}; use refcache::CacheReferences; -use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; +use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}; use tokio::sync::RwLock; pub struct AsyncListCache { @@ -28,38 +28,46 @@ impl AsyncList for AsyncListCache { refs_read_bound.bottom_bound().await } - async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option { + async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { let cache_read_handle = self.cache_refs.read().await; let cache_result = cache_read_handle.get(index.clone()).await; if let Some(cache_result) = cache_result { - return Some(self.cache_map.read().await.get(&cache_result).unwrap().clone()); + let content = self.cache_map.read().await.get(&cache_result).unwrap().clone(); + let is_first = cache_read_handle.top_bound().await.map(|v| v == content.get_list_identifier()).unwrap_or(false); + let is_last = cache_read_handle.bottom_bound().await.map(|v| v == content.get_list_identifier()).unwrap_or(false); + + return Some(AsyncListResult { content, is_first, is_last }); }; let authoritative = self.underlying.get(index.clone()).await; if let Some(ref authoritative) = authoritative { - let identifier = authoritative.get_list_identifier(); + let identifier = authoritative.content.get_list_identifier(); - self.cache_map.write().await.insert(identifier.clone(), authoritative.clone()); - self.cache_refs.write().await.insert(index, identifier.clone()); + self.cache_map.write().await.insert(identifier.clone(), authoritative.content.clone()); + self.cache_refs.write().await.insert(index, identifier.clone(), authoritative.is_first, authoritative.is_last).await; } authoritative } - async fn find(&self, identifier: &::Identifier) -> Option { + async fn find(&self, identifier: &::Identifier) -> Option> { let cache_read_handle = self.cache_map.read().await; let cache_result = cache_read_handle.get(identifier); if let Some(cache_result) = cache_result { - return Some(cache_result.clone()); + let content = cache_result.clone(); + let is_first = self.bounded_at_top_by().await.map(|v| v == *identifier).unwrap_or(false); + let is_last = self.bounded_at_bottom_by().await.map(|v| v == *identifier).unwrap_or(false); + + return Some(AsyncListResult { content, is_first, is_last }); }; let authoritative = self.underlying.find(identifier).await; if let Some(ref authoritative) = authoritative { - self.cache_map.write().await.insert(identifier.clone(), authoritative.clone()); + self.cache_map.write().await.insert(identifier.clone(), authoritative.content.clone()); } authoritative diff --git a/src/cache/src/async_list/refcache.rs b/src/cache/src/async_list/refcache.rs index b8335e5..87f3223 100644 --- a/src/cache/src/async_list/refcache.rs +++ b/src/cache/src/async_list/refcache.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use scope_chat::async_list::AsyncListIndex; -use tokio::{fs::read, sync::RwLock}; +use tokio::sync::RwLock; use super::refcacheslice::{self, CacheReferencesSlice}; @@ -37,10 +37,10 @@ impl CacheReferences { pub async fn get(&self, index: AsyncListIndex) -> Option { let read_handle = self.dense_segments.read().await; - + for segment in read_handle.values() { if let Some(value) = segment.get(index.clone()) { - return Some(value) + return Some(value); } } @@ -53,15 +53,18 @@ impl CacheReferences { pub async fn insert_detached(&self, item: I) { let mut mutation_handle = self.dense_segments.write().await; - mutation_handle.insert(rand::random(), CacheReferencesSlice { - is_bounded_at_top: false, - is_bounded_at_bottom: false, + mutation_handle.insert( + rand::random(), + CacheReferencesSlice { + is_bounded_at_top: false, + is_bounded_at_bottom: false, - item_references: vec![item], - }); + item_references: vec![item], + }, + ); } - pub async fn insert(&self, index: AsyncListIndex, item: I,) { + pub async 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 @@ -80,22 +83,32 @@ impl CacheReferences { if segments.len() == 0 { let mut mutation_handle = self.dense_segments.write().await; - mutation_handle.insert(rand::random(), CacheReferencesSlice { - is_bounded_at_top: is_first, - is_bounded_at_bottom: is_last, + mutation_handle.insert( + rand::random(), + CacheReferencesSlice { + is_bounded_at_top: is_top, + is_bounded_at_bottom: is_bottom, - item_references: vec![item], - }); + item_references: vec![item], + }, + ); } else if segments.len() == 1 { let mut mutation_handle = self.dense_segments.write().await; mutation_handle.get_mut(segments[0].1).unwrap().insert(index.clone(), item); + + 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 { 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)?") + _ => panic!("How are there two candidates that aren't (Before, After) or (After, Before)?"), }; let mut mutation_handle = self.dense_segments.write().await; @@ -113,17 +126,20 @@ impl CacheReferences { }; let mut merged = left.item_references; - + merged.push(item); merged.extend(right.item_references.into_iter()); - mutation_handle.insert(rand::random(), CacheReferencesSlice { - is_bounded_at_top: left.is_bounded_at_top, - is_bounded_at_bottom: right.is_bounded_at_bottom, + mutation_handle.insert( + rand::random(), + CacheReferencesSlice { + is_bounded_at_top: left.is_bounded_at_top, + is_bounded_at_bottom: right.is_bounded_at_bottom, - item_references: merged, - }); + item_references: merged, + }, + ); } else { panic!("Impossible state") } diff --git a/src/cache/src/async_list/refcacheslice.rs b/src/cache/src/async_list/refcacheslice.rs index 7497268..55c8a90 100644 --- a/src/cache/src/async_list/refcacheslice.rs +++ b/src/cache/src/async_list/refcacheslice.rs @@ -1,87 +1,85 @@ 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 is_bounded_at_top: bool, + pub is_bounded_at_bottom: bool, -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) - } - } + // 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, +} - None +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); + } } - 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) - (count as isize)) - } + None + } - AsyncListIndex::RelativeToTop(count) if self.is_bounded_at_top => { - Some(count as isize) - } + 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) - (count as isize)), - AsyncListIndex::After(item) => { - Some((self.find_index_of(item)? as isize) + 1) - } + AsyncListIndex::RelativeToTop(count) if self.is_bounded_at_top => Some(count as isize), - AsyncListIndex::Before(item) => { - Some((self.find_index_of(item)? as isize) - 1) - } + AsyncListIndex::After(item) => Some((self.find_index_of(item)? as isize) + 1), - _ => None, - } - } + AsyncListIndex::Before(item) => Some((self.find_index_of(item)? as isize) - 1), - pub fn get(&self, index: AsyncListIndex) -> Option { - let index = self.get_index(index)?; + _ => None, + } + } - if index < 0 { - return None; - } + pub fn get(&self, index: AsyncListIndex) -> Option { + let index = self.get_index(index)?; - self.item_references.get(index as usize).cloned() + if index < 0 { + return None; } - 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 }), + self.item_references.get(index as usize).cloned() + } - _ => panic!("TODO: Figure out what well-defined behaviour should occur for inserting relative to top or bottom") + 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) { - match index { - AsyncListIndex::After(item) => { - let i = self.find_index_of(item).unwrap(); + pub fn insert(&mut self, index: AsyncListIndex, value: I) { + 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 + 1, value); + } + AsyncListIndex::Before(item) => { + let i = self.find_index_of(item).unwrap(); - self.item_references.insert(i, value); - }, + self.item_references.insert(i, value); + } - _ => panic!("TODO: Figure out what well-defined behaviour should occur for inserting relative to top or bottom") - } + _ => panic!("TODO: Figure out what well-defined behaviour for what should occur for inserting relative to top or bottom"), } + } } #[derive(Clone, Copy)] pub enum Position { - Before, - Inside, - After, -} \ No newline at end of file + Before, + Inside, + After, +} diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index 023842e..324a225 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -1,11 +1,23 @@ use std::hash::Hash; +pub struct AsyncListResult { + pub content: T, + pub is_first: bool, + pub is_last: bool, +} + pub trait AsyncList { type Content: AsyncListItem; fn bounded_at_top_by(&self) -> impl std::future::Future::Identifier>>; - fn get(&self, index: AsyncListIndex<::Identifier>) -> impl std::future::Future>; - fn find(&self, identifier: &::Identifier) -> impl std::future::Future>; + fn get( + &self, + index: AsyncListIndex<::Identifier>, + ) -> impl std::future::Future>>; + fn find( + &self, + identifier: &::Identifier, + ) -> impl std::future::Future>>; fn bounded_at_bottom_by(&self) -> impl std::future::Future::Identifier>>; } diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index c63d593..f5801a1 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use scope_chat::{ - async_list::{AsyncList, AsyncListIndex, AsyncListItem}, + async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, channel::Channel, }; -use serenity::all::Timestamp; +use serenity::all::{ChannelId, GetMessages, Timestamp}; use tokio::sync::broadcast; use crate::{ @@ -69,11 +69,11 @@ impl AsyncList for DiscordChannel { unimplemented!() } - async fn find(&self, identifier: &::Identifier) -> Option { + async fn find(&self, identifier: &::Identifier) -> Option> { unimplemented!() } - async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option { + async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { unimplemented!() } diff --git a/src/discord/src/message/mod.rs b/src/discord/src/message/mod.rs index eba1591..43365f2 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -49,5 +49,9 @@ impl Message for DiscordMessage { } impl AsyncListItem for DiscordMessage { - type Identifier = String; + type Identifier = Snowflake; + + fn get_list_identifier(&self) -> Self::Identifier { + self.id + } } diff --git a/src/ui/src/app.rs b/src/ui/src/app.rs index 54449bb..4f6c11c 100644 --- a/src/ui/src/app.rs +++ b/src/ui/src/app.rs @@ -1,6 +1,7 @@ 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_chat::message::Message; use crate::channel::ChannelView; From a9a92a25d8a1d691942ed85ea8ebab8b211889dc Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Mon, 18 Nov 2024 00:52:37 -0500 Subject: [PATCH 04/16] Finished discord layer --- Cargo.lock | 2 + src/cache/src/async_list/mod.rs | 89 ++++++------ src/cache/src/async_list/refcache.rs | 98 ++++++++----- src/cache/src/async_list/refcacheslice.rs | 55 +++++-- src/cache/src/channel.rs | 107 ++++++++++++++ src/cache/src/lib.rs | 1 + src/chat/src/async_list.rs | 54 ++++--- src/chat/src/channel.rs | 2 +- src/chat/src/message.rs | 2 +- src/discord/Cargo.toml | 1 + src/discord/src/channel/mod.rs | 112 +++++++++++++-- src/discord/src/client.rs | 33 ++--- src/discord/src/message/mod.rs | 25 +++- src/ui/Cargo.toml | 1 + src/ui/src/channel/mod.rs | 6 +- src/ui/src/component/async_list/element.rs | 62 ++++++++ src/ui/src/component/async_list/marker.rs | 13 ++ src/ui/src/component/async_list/mod.rs | 160 +++++++++++++++++++++ src/ui/src/component/mod.rs | 1 + 19 files changed, 675 insertions(+), 149 deletions(-) create mode 100644 src/cache/src/channel.rs create mode 100644 src/ui/src/component/async_list/element.rs create mode 100644 src/ui/src/component/async_list/marker.rs create mode 100644 src/ui/src/component/async_list/mod.rs create mode 100644 src/ui/src/component/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e3ce5c8..5c66798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5440,6 +5440,7 @@ dependencies = [ "random-string", "reqwest_client", "rust-embed", + "scope-backend-cache", "scope-backend-discord", "scope-chat", "scope-util", @@ -5463,6 +5464,7 @@ version = "0.1.0" dependencies = [ "chrono", "gpui", + "scope-backend-cache", "scope-chat", "serenity", "tokio", diff --git a/src/cache/src/async_list/mod.rs b/src/cache/src/async_list/mod.rs index 0980f91..c25ac5e 100644 --- a/src/cache/src/async_list/mod.rs +++ b/src/cache/src/async_list/mod.rs @@ -1,75 +1,72 @@ pub mod refcache; pub mod refcacheslice; -use std::{collections::HashMap, process::id}; +use std::collections::HashMap; use refcache::CacheReferences; +use refcacheslice::Exists; use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}; -use tokio::sync::RwLock; pub struct AsyncListCache { - underlying: L, - cache_refs: RwLock::Identifier>>, - cache_map: RwLock::Identifier, L::Content>>, + cache_refs: CacheReferences<::Identifier>, + cache_map: HashMap<::Identifier, L::Content>, } -impl AsyncListCache {} - -impl AsyncList for AsyncListCache { - type Content = L::Content; - - async fn bounded_at_top_by(&self) -> Option<::Identifier> { - let refs_read_bound = self.cache_refs.read().await; - refs_read_bound.top_bound().await +impl AsyncListCache { + pub fn new() -> Self { + Self { + cache_refs: CacheReferences::new(), + cache_map: HashMap::new(), + } } - async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { - let refs_read_bound = self.cache_refs.read().await; - refs_read_bound.bottom_bound().await - } + pub fn append_bottom(&mut self, value: L::Content) { + let identifier = value.get_list_identifier(); - async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { - let cache_read_handle = self.cache_refs.read().await; - let cache_result = cache_read_handle.get(index.clone()).await; + self.cache_refs.append_bottom(identifier.clone()); + self.cache_map.insert(identifier, value); + } - if let Some(cache_result) = cache_result { - let content = self.cache_map.read().await.get(&cache_result).unwrap().clone(); - let is_first = cache_read_handle.top_bound().await.map(|v| v == content.get_list_identifier()).unwrap_or(false); - let is_last = cache_read_handle.bottom_bound().await.map(|v| v == content.get_list_identifier()).unwrap_or(false); + pub fn insert(&mut self, index: AsyncListIndex<::Identifier>, value: L::Content, is_top: bool, is_bottom: bool) { + let identifier = value.get_list_identifier(); - return Some(AsyncListResult { content, is_first, is_last }); - }; + self.cache_map.insert(identifier.clone(), value); + self.cache_refs.insert(index, identifier.clone(), is_top, is_bottom); + } - let authoritative = self.underlying.get(index.clone()).await; + pub fn insert_unlocated(&mut self, value: L::Content) { + let identifier = value.get_list_identifier(); - if let Some(ref authoritative) = authoritative { - let identifier = authoritative.content.get_list_identifier(); + self.cache_map.insert(identifier.clone(), value); + } - self.cache_map.write().await.insert(identifier.clone(), authoritative.content.clone()); - self.cache_refs.write().await.insert(index, identifier.clone(), authoritative.is_first, authoritative.is_last).await; - } + pub fn bounded_at_top_by(&self) -> Option<::Identifier> { + self.cache_refs.top_bound() + } - authoritative + pub fn bounded_at_bottom_by(&self) -> Option<::Identifier> { + self.cache_refs.bottom_bound() } - async fn find(&self, identifier: &::Identifier) -> Option> { - let cache_read_handle = self.cache_map.read().await; - let cache_result = cache_read_handle.get(identifier); + pub fn get(&self, index: AsyncListIndex<::Identifier>) -> Exists> { + let cache_result = self.cache_refs.get(index.clone()); - if let Some(cache_result) = cache_result { - let content = cache_result.clone(); - let is_first = self.bounded_at_top_by().await.map(|v| v == *identifier).unwrap_or(false); - let is_last = self.bounded_at_bottom_by().await.map(|v| v == *identifier).unwrap_or(false); + 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 Some(AsyncListResult { content, is_first, is_last }); + return Exists::Yes(AsyncListResult { content, is_top, is_bottom }); }; - let authoritative = self.underlying.find(identifier).await; - - if let Some(ref authoritative) = authoritative { - self.cache_map.write().await.insert(identifier.clone(), authoritative.content.clone()); + if let Exists::No = cache_result { + return Exists::No; } - authoritative + Exists::Unknown + } + + pub fn find(&self, identifier: &::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 index 87f3223..f2da7b2 100644 --- a/src/cache/src/async_list/refcache.rs +++ b/src/cache/src/async_list/refcache.rs @@ -1,59 +1,84 @@ use std::collections::HashMap; use scope_chat::async_list::AsyncListIndex; -use tokio::sync::RwLock; -use super::refcacheslice::{self, CacheReferencesSlice}; +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: RwLock>>, + dense_segments: HashMap>, top_bounded_identifier: Option, bottom_bounded_identifier: Option, } impl CacheReferences { - pub async fn top_bound(&self) -> Option { + 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 read_handle = self.dense_segments.read().await; - let top_bound = read_handle.get(&index).unwrap(); + 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 async fn bottom_bound(&self) -> Option { + pub fn bottom_bound(&self) -> Option { let index = self.bottom_bounded_identifier?; - let read_handle = self.dense_segments.read().await; - let bottom_bound = read_handle.get(&index).unwrap(); + 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 async fn get(&self, index: AsyncListIndex) -> Option { - let read_handle = self.dense_segments.read().await; + pub fn get(&self, index: AsyncListIndex) -> Exists { + for segment in self.dense_segments.values() { + let result = segment.get(index.clone()); - for segment in read_handle.values() { - if let Some(value) = segment.get(index.clone()) { - return Some(value); + if let Exists::Yes(value) = result { + return Exists::Yes(value); + } else if let Exists::No = result { + return Exists::No; } } - return None; + return 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 async fn insert_detached(&self, item: I) { - let mut mutation_handle = self.dense_segments.write().await; - - mutation_handle.insert( + pub fn insert_detached(&mut self, item: I) { + self.dense_segments.insert( rand::random(), CacheReferencesSlice { is_bounded_at_top: false, @@ -64,7 +89,7 @@ impl CacheReferences { ); } - pub async fn insert(&mut self, index: AsyncListIndex, item: I, is_top: bool, is_bottom: bool) { + 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 @@ -72,18 +97,14 @@ impl CacheReferences { let mut segments = vec![]; - let read_handle = self.dense_segments.read().await; - - for (i, segment) in read_handle.iter() { + for (i, segment) in self.dense_segments.iter() { if let Some(position) = segment.can_insert(index.clone()) { - segments.push((position, i)); + segments.push((position, *i)); } } if segments.len() == 0 { - let mut mutation_handle = self.dense_segments.write().await; - - mutation_handle.insert( + self.dense_segments.insert( rand::random(), CacheReferencesSlice { is_bounded_at_top: is_top, @@ -93,17 +114,18 @@ impl CacheReferences { }, ); } else if segments.len() == 1 { - let mut mutation_handle = self.dense_segments.write().await; - - mutation_handle.get_mut(segments[0].1).unwrap().insert(index.clone(), item); + 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) + self.top_bounded_identifier = Some(segments[0].1) } if is_bottom { - self.bottom_bounded_identifier = Some(*segments[0].1) + 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), @@ -111,16 +133,14 @@ impl CacheReferences { _ => panic!("How are there two candidates that aren't (Before, After) or (After, Before)?"), }; - let mut mutation_handle = self.dense_segments.write().await; - let (left, right) = if li < ri { - let right = mutation_handle.remove(&ri).unwrap(); - let left = mutation_handle.remove(&li).unwrap(); + let right = self.dense_segments.remove(&ri).unwrap(); + let left = self.dense_segments.remove(&li).unwrap(); (left, right) } else { - let left = mutation_handle.remove(&li).unwrap(); - let right = mutation_handle.remove(&ri).unwrap(); + let left = self.dense_segments.remove(&li).unwrap(); + let right = self.dense_segments.remove(&ri).unwrap(); (left, right) }; @@ -131,7 +151,7 @@ impl CacheReferences { merged.extend(right.item_references.into_iter()); - mutation_handle.insert( + self.dense_segments.insert( rand::random(), CacheReferencesSlice { is_bounded_at_top: left.is_bounded_at_top, diff --git a/src/cache/src/async_list/refcacheslice.rs b/src/cache/src/async_list/refcacheslice.rs index 55c8a90..65f0112 100644 --- a/src/cache/src/async_list/refcacheslice.rs +++ b/src/cache/src/async_list/refcacheslice.rs @@ -9,6 +9,12 @@ pub struct CacheReferencesSlice { 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..) { @@ -22,7 +28,7 @@ impl CacheReferencesSlice { 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) - (count as isize)), + 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), @@ -34,14 +40,36 @@ impl CacheReferencesSlice { } } - pub fn get(&self, index: AsyncListIndex) -> Option { - let index = self.get_index(index)?; + pub fn append_bottom(&mut self, index: I) { + assert!(self.is_bounded_at_bottom); - if index < 0 { - return None; - } + self.item_references.push(index); + } + + pub fn get(&self, index: AsyncListIndex) -> Exists { + let index = self.get_index(index); - self.item_references.get(index as usize).cloned() + 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 { @@ -59,7 +87,14 @@ impl CacheReferencesSlice { } } - pub fn insert(&mut self, index: AsyncListIndex, value: I) { + 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(); @@ -79,7 +114,9 @@ impl CacheReferencesSlice { #[derive(Clone, Copy)] pub enum Position { + /// Closer to the top Before, - Inside, + /// Closer to the bottom After, + Inside, } diff --git a/src/cache/src/channel.rs b/src/cache/src/channel.rs new file mode 100644 index 0000000..146a3eb --- /dev/null +++ b/src/cache/src/channel.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use scope_chat::{ + async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, + channel::Channel, +}; +use tokio::sync::Mutex; + +use crate::async_list::{refcacheslice::Exists, AsyncListCache}; + +pub struct CacheChannel(T, Arc>>); + +impl CacheChannel { + pub fn new(channel: T) -> Self + where + T::Message: 'static, + T: 'static, + { + let mut receiver = channel.get_receiver(); + let cache = Arc::new(Mutex::new(AsyncListCache::new())); + let cache_clone = cache.clone(); + + tokio::spawn(async move { + loop { + let message = receiver.recv().await.unwrap(); + + cache_clone.lock().await.append_bottom(message); + } + }); + + CacheChannel(channel, cache) + } +} + +impl Channel for CacheChannel { + type Message = T::Message; + + fn get_receiver(&self) -> tokio::sync::broadcast::Receiver { + self.0.get_receiver() + } + + fn send_message(&self, content: String, nonce: String) -> Self::Message { + self.0.send_message(content, nonce) + } +} + +impl AsyncList for CacheChannel { + type Content = T::Message; + + async fn bounded_at_top_by(&self) -> Option<::Identifier> { + let l = self.1.lock().await; + let v = l.bounded_at_top_by(); + + if let Some(v) = v { + return Some(v); + }; + + let i = self.0.bounded_at_top_by().await?; + + Some(i) + } + + async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { + let l = self.1.lock().await; + let v = l.bounded_at_bottom_by(); + + if let Some(v) = v { + return Some(v); + }; + + let i = self.0.bounded_at_bottom_by().await?; + + Some(i) + } + + async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { + let mut l = self.1.lock().await; + let v = l.get(index.clone()); + + if let Exists::Yes(v) = v { + return Some(v); + } else if let Exists::No = v { + return None; + } + + let authoritative = self.0.get(index.clone()).await?; + + l.insert(index, authoritative.content.clone(), authoritative.is_top, authoritative.is_bottom); + + Some(authoritative) + } + + async fn find(&self, identifier: &::Identifier) -> Option { + let mut l = self.1.lock().await; + let v = l.find(identifier); + + if let Some(v) = v { + return Some(v); + } + + let authoritative = self.0.find(identifier).await?; + + l.insert_unlocated(authoritative.clone()); + + Some(authoritative) + } +} diff --git a/src/cache/src/lib.rs b/src/cache/src/lib.rs index ce2b145..e65f511 100644 --- a/src/cache/src/lib.rs +++ b/src/cache/src/lib.rs @@ -1 +1,2 @@ pub mod async_list; +pub mod channel; diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index 324a225..18c61f3 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -1,28 +1,16 @@ -use std::hash::Hash; - -pub struct AsyncListResult { - pub content: T, - pub is_first: bool, - pub is_last: bool, -} +use std::{fmt::Debug, future::Future, hash::Hash}; pub trait AsyncList { type Content: AsyncListItem; - fn bounded_at_top_by(&self) -> impl std::future::Future::Identifier>>; - fn get( - &self, - index: AsyncListIndex<::Identifier>, - ) -> impl std::future::Future>>; - fn find( - &self, - identifier: &::Identifier, - ) -> impl std::future::Future>>; - fn bounded_at_bottom_by(&self) -> impl std::future::Future::Identifier>>; + fn bounded_at_top_by(&self) -> impl Future::Identifier>>; + fn get(&self, index: AsyncListIndex<::Identifier>) -> impl Future>>; + fn find(&self, identifier: &::Identifier) -> impl Future>; + fn bounded_at_bottom_by(&self) -> impl Future::Identifier>>; } pub trait AsyncListItem: Clone { - type Identifier: Eq + Hash + Clone; + type Identifier: Eq + Hash + Clone + Send; fn get_list_identifier(&self) -> Self::Identifier; } @@ -30,9 +18,37 @@ pub trait AsyncListItem: Clone { #[derive(Clone)] pub enum AsyncListIndex { RelativeToTop(usize), - After(I), + /// 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(()) + } +} + +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 119d847..eaff99b 100644 --- a/src/chat/src/channel.rs +++ b/src/chat/src/channel.rs @@ -2,7 +2,7 @@ use tokio::sync::broadcast; use crate::{async_list::AsyncList, message::Message}; -pub trait Channel: Clone + AsyncList { +pub trait Channel: AsyncList { type Message: Message; fn get_receiver(&self) -> broadcast::Receiver; diff --git a/src/chat/src/message.rs b/src/chat/src/message.rs index fd2db96..5ffea52 100644 --- a/src/chat/src/message.rs +++ b/src/chat/src/message.rs @@ -3,7 +3,7 @@ use gpui::Element; use crate::async_list::AsyncListItem; -pub trait Message: Clone + 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 f5801a1..8c667a3 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,11 +1,12 @@ use std::sync::Arc; +use scope_backend_cache::async_list::{refcacheslice::Exists, AsyncListCache}; use scope_chat::{ - async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, + async_list::{AsyncList, AsyncListIndex, AsyncListResult}, channel::Channel, }; -use serenity::all::{ChannelId, GetMessages, Timestamp}; -use tokio::sync::broadcast; +use serenity::all::{GetMessages, MessageId, Timestamp}; +use tokio::sync::{broadcast, Mutex}; use crate::{ client::DiscordClient, @@ -17,6 +18,7 @@ pub struct DiscordChannel { channel_id: Snowflake, receiver: broadcast::Receiver, client: Arc, + cache: Arc>>, } impl DiscordChannel { @@ -29,6 +31,7 @@ impl DiscordChannel { channel_id, receiver, client, + cache: Arc::new(Mutex::new(AsyncListCache::new())), } } } @@ -61,20 +64,106 @@ impl Channel for DiscordChannel { } impl AsyncList for DiscordChannel { - async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { - unimplemented!() + 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<::Identifier> { - unimplemented!() + 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: &::Identifier) -> Option> { - unimplemented!() + async fn find(&self, identifier: &Snowflake) -> Option { + let lock = self.cache.lock().await; + let cache_value = lock.find(identifier); + + if let Some(v) = cache_value { + return Some(v); + } + + self.client.get_specific_message(self.channel_id, *identifier).await.map(|v| DiscordMessage::from_serenity(&v)) } - async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { - unimplemented!() + async fn get(&self, index: AsyncListIndex) -> Option> { + let mut lock = self.cache.lock().await; + let cache_value = lock.get(index.clone()); + + if let Exists::Yes(v) = cache_value { + return Some(v); + } else if let Exists::No = cache_value { + return None; + } + + let mut result: Option = None; + let mut is_top = false; + let mut is_bottom = false; + + match index { + AsyncListIndex::RelativeToTop(_) => todo!("Unsupported"), + AsyncListIndex::RelativeToBottom(index) => todo!("TODO :3 have fun!!"), + AsyncListIndex::After(message) => { + // NEWEST first + let v = self.client.get_messages(self.channel_id, GetMessages::new().after(MessageId::new(message.content)).limit(50)).await; + let mut lock = self.cache.lock().await; + let mut current_index: Snowflake = message; + + let is_end = v.len() == 50; + is_bottom = is_end; + + result = Some(DiscordMessage::from_serenity(v.get(0).unwrap())); + + for message in v.iter().rev() { + lock.insert( + AsyncListIndex::After(current_index), + DiscordMessage::from_serenity(message), + false, + is_end, + ); + + current_index = Snowflake { content: message.id.get() } + } + } + AsyncListIndex::Before(message) => { + let v = self.client.get_messages(self.channel_id, GetMessages::new().after(MessageId::new(message.content)).limit(50)).await; + let mut lock = self.cache.lock().await; + let mut current_index: Snowflake = message; + + let is_end = v.len() == 50; + is_top = is_end; + + result = Some(DiscordMessage::from_serenity(v.get(0).unwrap())); + + for message in v { + lock.insert( + AsyncListIndex::Before(current_index), + DiscordMessage::from_serenity(&message), + false, + is_end, + ); + + current_index = Snowflake { content: message.id.get() } + } + } + }; + + result.map(|v| AsyncListResult { + content: v, + is_top, + is_bottom, + }) } type Content = DiscordMessage; @@ -86,6 +175,7 @@ impl Clone for DiscordChannel { channel_id: self.channel_id, receiver: self.receiver.resubscribe(), client: self.client.clone(), + cache: self.cache.clone(), } } } diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 3473ac9..d1cd3fa 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -3,7 +3,8 @@ use std::{ }; 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, Message, MessageId, Nonce, RawEventHandler}, + async_trait, }; use tokio::sync::{broadcast, RwLock}; @@ -14,7 +15,7 @@ use crate::{ content::DiscordMessageContent, DiscordMessage, }, - snowflake::{self, Snowflake}, + snowflake::Snowflake, }; #[allow(dead_code)] @@ -95,6 +96,16 @@ impl DiscordClient { .await .unwrap(); } + + pub async fn get_messages(&self, channel_id: Snowflake, builder: GetMessages) -> Vec { + // 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 { + // 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); @@ -140,23 +151,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/mod.rs b/src/discord/src/message/mod.rs index 43365f2..ee3a461 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -1,8 +1,9 @@ +use author::{DiscordMessageAuthor, DisplayName}; use chrono::{DateTime, Utc}; -use author::DiscordMessageAuthor; use content::DiscordMessageContent; use gpui::{Element, IntoElement}; use scope_chat::{async_list::AsyncListItem, message::Message}; +use serenity::all::Nonce; use crate::snowflake::Snowflake; @@ -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 diff --git a/src/ui/Cargo.toml b/src/ui/Cargo.toml index 924cc19..81f65a7 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"] } diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 62f9c1c..7a9adff 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -18,13 +18,13 @@ impl ChannelView { let async_model = state_model.clone(); let mut async_ctx = ctx.to_async(); - let channel_listener = channel.clone(); + let mut channel_listener = channel.get_receiver(); ctx .foreground_executor() .spawn(async move { loop { - let message = channel_listener.get_receiver().recv().await.unwrap(); + let message = channel_listener.recv().await.unwrap(); async_model .update(&mut async_ctx, |data, ctx| { @@ -65,7 +65,7 @@ impl ChannelView { }); let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); - let pending = channel_sender.send_message(content, nonce); + let pending = channel.send_message(content, nonce); channel_view.list_model.update(ctx, move |v, _| { v.add_pending_message(pending); diff --git a/src/ui/src/component/async_list/element.rs b/src/ui/src/component/async_list/element.rs new file mode 100644 index 0000000..a5ac023 --- /dev/null +++ b/src/ui/src/component/async_list/element.rs @@ -0,0 +1,62 @@ +use std::future::Future; + +use gpui::{div, rgb, AnyElement, Context, Model, ParentElement, Render, Styled, ViewContext}; +use scope_chat::async_list::{AsyncListItem, AsyncListResult}; + +pub enum AsyncListComponentElement { + Waiting, + Resolved(E), + None, +} + +pub struct AsyncListComponentElementView { + pub element: Model>, + renderer: Box AnyElement>, +} + +impl AsyncListComponentElementView { + pub fn new( + ctx: &mut ViewContext<'_, Self>, + renderer: impl (Fn(&E) -> AnyElement) + 'static, + future: impl Future>> + 'static, + ) -> AsyncListComponentElementView { + let model = ctx.new_model(|_| AsyncListComponentElement::Waiting); + + let mut async_ctx = ctx.to_async(); + + let model_handle = model.clone(); + + ctx + .foreground_executor() + .spawn(async move { + let result = future.await; + + async_ctx + .update_model(&model_handle, |v, cx| { + if let Some(result) = result { + *v = AsyncListComponentElement::Resolved(result.content); + } else { + *v = AsyncListComponentElement::None; + } + cx.notify(); + }) + .unwrap(); + }) + .detach(); + + AsyncListComponentElementView { + element: model, + renderer: Box::new(renderer), + } + } +} + +impl Render for AsyncListComponentElementView { + fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { + match self.element.read(cx) { + AsyncListComponentElement::Waiting => div().w_full().h_8().flex().items_center().justify_center().text_color(rgb(0xFFFFFF)).child("Waiting..."), + AsyncListComponentElement::None => div().w_full().h_8().flex().items_center().justify_center().text_color(rgb(0xFFFFFF)).child("None!"), + AsyncListComponentElement::Resolved(v) => div().w_full().h_full().child((self.renderer)(v)), + } + } +} diff --git a/src/ui/src/component/async_list/marker.rs b/src/ui/src/component/async_list/marker.rs new file mode 100644 index 0000000..f622c45 --- /dev/null +++ b/src/ui/src/component/async_list/marker.rs @@ -0,0 +1,13 @@ +use gpui::{div, IntoElement, Render}; + +pub struct Marker { + pub name: &'static str, +} + +impl Render for Marker { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + println!("Marker rendered: {}", self.name); + + div() + } +} diff --git a/src/ui/src/component/async_list/mod.rs b/src/ui/src/component/async_list/mod.rs new file mode 100644 index 0000000..07da902 --- /dev/null +++ b/src/ui/src/component/async_list/mod.rs @@ -0,0 +1,160 @@ +pub mod element; +pub mod marker; + +use std::{cell::RefCell, rc::Rc}; + +use element::{AsyncListComponentElement, AsyncListComponentElementView}; +use gpui::{ + div, list, rgb, AnyElement, AppContext, Context, Element, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, + View, VisualContext, +}; +use marker::Marker; +use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; + +pub struct AsyncListComponent +where + T::Content: 'static, +{ + list: Rc>, + cache: Rc>>>>, + alignment: ListAlignment, + overdraw: Pixels, + + // top, bottom + bounds_flags: Model<(bool, bool)>, + + renderer: Rc AnyElement>>, +} + +pub enum StartAt { + Bottom, + Top, +} + +impl AsyncListComponent +where + T: 'static, +{ + pub fn create(cx: &mut AppContext, list: T, start_at: StartAt, overdraw: Pixels, renderer: impl (Fn(&T::Content) -> AnyElement) + 'static) -> Self { + AsyncListComponent { + list: Rc::new(RefCell::new(list)), + cache: Default::default(), + alignment: if let StartAt::Bottom = start_at { + ListAlignment::Bottom + } else { + ListAlignment::Top + }, + overdraw, + bounds_flags: cx.new_model(|_| (false, false)), + renderer: Rc::new(RefCell::new(renderer)), + } + } + + fn list_state(&self, _: &mut gpui::ViewContext) -> ListState { + let handle = self.cache.clone(); + let len = self.cache.borrow().len(); + let bounds_model = self.bounds_flags.clone(); + + ListState::new( + if len == 0 { 1 } else { len + 2 }, + ListAlignment::Bottom, + self.overdraw, + move |idx, cx| { + if idx == 0 { + cx.update_model(&bounds_model, |v, _| *v = (true, v.1)); + + div().child(cx.new_view(|_| Marker { name: "Upper" })) + } else if idx == len + 1 { + cx.update_model(&bounds_model, |v, _| *v = (v.0, true)); + + div().child(cx.new_view(|_| Marker { name: "Lower" })) + } else { + div().text_color(rgb(0xFFFFFF)).child(handle.borrow().get(idx - 1).unwrap().clone()) + } + .into_any_element() + }, + ) + } + + fn update(&mut self, cx: &mut gpui::ViewContext) { + // update bottom + 'update_bottom: { + if self.bounds_flags.read(cx).0 { + println!("Updating Bottom!"); + let mut borrow = self.cache.borrow_mut(); + let last = borrow.last(); + + let index = if let Some(last) = last { + AsyncListIndex::After(if let AsyncListComponentElement::Resolved(v) = last.model.read(cx).element.read(cx) { + v.get_list_identifier() + } else { + break 'update_bottom; + }) + } else { + AsyncListIndex::RelativeToBottom(0) + }; + + println!(" {:?}", index); + + let list = self.list.clone(); + + println!("Pushed to bottom"); + + let renderer = self.renderer.clone(); + + let len = borrow.len(); + + borrow.push(cx.new_view(move |cx| { + AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { list.borrow_mut().get(index).await }) + })); + + cx.on_next_frame(|v, cx| cx.notify()); + } + } + + // update top + 'update_top: { + if self.bounds_flags.read(cx).1 { + let mut borrow = self.cache.borrow_mut(); + let first = borrow.first(); + + let index = if let Some(first) = first { + AsyncListIndex::Before(if let AsyncListComponentElement::Resolved(v) = first.model.read(cx).element.read(cx) { + v.get_list_identifier() + } else { + break 'update_top; + }) + } else { + AsyncListIndex::RelativeToTop(0) + }; + + let list = self.list.clone(); + + println!("Pushed to top"); + + let renderer = self.renderer.clone(); + + borrow.insert( + 0, + cx.new_view(move |cx| { + AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { list.borrow_mut().get(index).await }) + }), + ); + + cx.on_next_frame(|v, cx| cx.notify()); + } + } + + self.bounds_flags.update(cx, |v, _| *v = (false, false)) + } +} + +impl Render for AsyncListComponent { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + println!("Rendering"); + + self.update(cx); + + div().w_full().h_full().child(list(self.list_state(cx)).w_full().h_full()) + } +} diff --git a/src/ui/src/component/mod.rs b/src/ui/src/component/mod.rs new file mode 100644 index 0000000..ce2b145 --- /dev/null +++ b/src/ui/src/component/mod.rs @@ -0,0 +1 @@ +pub mod async_list; From 9d790b7d5738bcfa1a475796bfde93411e336676 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Mon, 18 Nov 2024 04:51:13 -0500 Subject: [PATCH 05/16] fix rendering bugs --- src/chat/src/async_list.rs | 2 +- src/discord/src/channel/mod.rs | 41 ++++++++++++-- src/discord/src/client.rs | 2 + src/ui/src/app.rs | 7 +-- src/ui/src/channel/message_list.rs | 85 ---------------------------- src/ui/src/channel/mod.rs | 91 +++++++++++++++--------------- src/ui/src/main.rs | 1 + 7 files changed, 89 insertions(+), 140 deletions(-) delete mode 100644 src/ui/src/channel/message_list.rs diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index 18c61f3..14f4967 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -10,7 +10,7 @@ pub trait AsyncList { } pub trait AsyncListItem: Clone { - type Identifier: Eq + Hash + Clone + Send; + type Identifier: Eq + Hash + Clone + Send + Debug; fn get_list_identifier(&self) -> Self::Identifier; } diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index 8c667a3..6ab40a1 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use scope_backend_cache::async_list::{refcacheslice::Exists, AsyncListCache}; use scope_chat::{ - async_list::{AsyncList, AsyncListIndex, AsyncListResult}, + async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, channel::Channel, }; use serenity::all::{GetMessages, MessageId, Timestamp}; @@ -98,6 +98,8 @@ impl AsyncList for DiscordChannel { } async fn get(&self, index: AsyncListIndex) -> Option> { + println!("Start"); + let mut lock = self.cache.lock().await; let cache_value = lock.get(index.clone()); @@ -113,11 +115,41 @@ impl AsyncList for DiscordChannel { match index { AsyncListIndex::RelativeToTop(_) => todo!("Unsupported"), - AsyncListIndex::RelativeToBottom(index) => todo!("TODO :3 have fun!!"), + AsyncListIndex::RelativeToBottom(index) => { + if index != 0 { + unimplemented!() + } + + let v = self.client.get_messages(self.channel_id, GetMessages::new().limit(50)).await; + + let is_end = v.len() == 50; + is_bottom = true; + is_top = v.len() == 1; + + result = v.get(0).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(50)).await; - let mut lock = self.cache.lock().await; let mut current_index: Snowflake = message; let is_end = v.len() == 50; @@ -138,7 +170,6 @@ impl AsyncList for DiscordChannel { } AsyncListIndex::Before(message) => { let v = self.client.get_messages(self.channel_id, GetMessages::new().after(MessageId::new(message.content)).limit(50)).await; - let mut lock = self.cache.lock().await; let mut current_index: Snowflake = message; let is_end = v.len() == 50; @@ -159,6 +190,8 @@ impl AsyncList for DiscordChannel { } }; + println!("End"); + result.map(|v| AsyncListResult { content: v, is_top, diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index d1cd3fa..22ab8f4 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -98,11 +98,13 @@ impl DiscordClient { } pub async fn get_messages(&self, channel_id: Snowflake, builder: GetMessages) -> Vec { + println!("Discord: get_messages"); // 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()) } diff --git a/src/ui/src/app.rs b/src/ui/src/app.rs index 4f6c11c..13b7656 100644 --- a/src/ui/src/app.rs +++ b/src/ui/src/app.rs @@ -1,12 +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_chat::message::Message; +use scope_backend_discord::{channel::DiscordChannel, client::DiscordClient, snowflake::Snowflake}; use crate::channel::ChannelView; pub struct App { - channel: Model>>>, + channel: Model>>>, } impl App { @@ -33,7 +32,7 @@ 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); diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs deleted file mode 100644 index 31216d3..0000000 --- a/src/ui/src/channel/message_list.rs +++ /dev/null @@ -1,85 +0,0 @@ -use gpui::{div, IntoElement, ListAlignment, ListState, ParentElement, Pixels}; - -use scope_chat::message::{Message, MessageAuthor}; - -use super::message::{message, MessageGroup}; - -#[derive(Clone)] -pub struct MessageList { - messages: Vec>, -} - -impl Default for MessageList { - fn default() -> Self { - Self::new() - } -} - -impl MessageList { - pub fn new() -> MessageList { - Self { messages: Vec::default() } - } - - pub fn add_external_message(&mut self, message: M) { - if let Some(nonce) = message.get_nonce() { - let mut removal_index: Option = None; - - for (group, index) in self.messages.iter_mut().zip(0..) { - let matching = group.find_matching(nonce); - - if let Some(matching) = matching { - if group.size() == 1 { - removal_index = Some(index); - } else { - group.remove(matching); - } - } - } - - if let Some(removal_index) = removal_index { - self.messages.remove(removal_index); - } - } - - let last = self.messages.last_mut(); - - 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)); - } - } - - 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); - } else { - self.messages.push(MessageGroup::new(pending_message)); - } - } else { - self.messages.push(MessageGroup::new(pending_message)); - } - } - - pub fn length(&self) -> usize { - self.messages.len() - } - - pub fn get(&self, index: usize) -> Option<&MessageGroup> { - self.messages.get(index) - } - - pub fn create_list_state(&self) -> ListState { - let clone = self.clone(); - - 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() - }) - } -} diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 7a9adff..0485074 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -1,24 +1,29 @@ 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, IntoElement, ParentElement, Pixels, Render, Styled, View, VisualContext}; +use message::{message, MessageGroup}; +use scope_chat::channel::Channel; -pub struct ChannelView { - list_state: ListState, - list_model: Model>, +use crate::component::async_list::{AsyncListComponent, StartAt}; + +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 mut channel_listener = channel.get_receiver(); - let async_model = state_model.clone(); + let list_view = ctx.new_view(|cx| { + AsyncListComponent::create(cx, channel, StartAt::Bottom, Pixels(30.), |msg| { + message(MessageGroup::new(msg.clone())).into_any_element() + }) + }); + + let async_model = list_view.clone(); let mut async_ctx = ctx.to_async(); - let mut channel_listener = channel.get_receiver(); ctx .foreground_executor() @@ -28,7 +33,8 @@ impl ChannelView { async_model .update(&mut async_ctx, |data, ctx| { - data.add_external_message(message); + // data.add_external_message(message); + todo!(); ctx.notify(); }) .unwrap(); @@ -36,13 +42,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); @@ -54,38 +53,38 @@ impl ChannelView { ctx .subscribe(&message_input, move |channel_view, 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.send_message(content, nonce); - - 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(); + // 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.send_message(content, nonce); + + // channel_view.list_model.update(ctx, move |v, _| unimplemented!()); + // 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 { - 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()) +impl Render for ChannelView { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + 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()) } } diff --git a/src/ui/src/main.rs b/src/ui/src/main.rs index 4282e3b..b0bae59 100644 --- a/src/ui/src/main.rs +++ b/src/ui/src/main.rs @@ -3,6 +3,7 @@ pub mod app; pub mod app_state; pub mod channel; pub mod menu; +pub mod component; use std::sync::Arc; From 038ede010a30117ee267682f8b619772fc12e776 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Mon, 18 Nov 2024 05:25:46 -0500 Subject: [PATCH 06/16] small changes --- src/ui/src/channel/mod.rs | 7 ++----- src/ui/src/component/async_list/mod.rs | 16 +++------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 0485074..986971e 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -16,11 +16,8 @@ impl ChannelView { pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: C) -> Self { let mut channel_listener = channel.get_receiver(); - let list_view = ctx.new_view(|cx| { - AsyncListComponent::create(cx, channel, StartAt::Bottom, Pixels(30.), |msg| { - message(MessageGroup::new(msg.clone())).into_any_element() - }) - }); + let list_view = + ctx.new_view(|cx| AsyncListComponent::create(cx, channel, Pixels(30.), |msg| message(MessageGroup::new(msg.clone())).into_any_element())); let async_model = list_view.clone(); let mut async_ctx = ctx.to_async(); diff --git a/src/ui/src/component/async_list/mod.rs b/src/ui/src/component/async_list/mod.rs index 07da902..bd5b137 100644 --- a/src/ui/src/component/async_list/mod.rs +++ b/src/ui/src/component/async_list/mod.rs @@ -5,8 +5,8 @@ use std::{cell::RefCell, rc::Rc}; use element::{AsyncListComponentElement, AsyncListComponentElementView}; use gpui::{ - div, list, rgb, AnyElement, AppContext, Context, Element, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, - View, VisualContext, + div, list, rgb, AnyElement, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, View, + VisualContext, }; use marker::Marker; use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; @@ -17,7 +17,6 @@ where { list: Rc>, cache: Rc>>>>, - alignment: ListAlignment, overdraw: Pixels, // top, bottom @@ -35,15 +34,10 @@ impl AsyncListComponent where T: 'static, { - pub fn create(cx: &mut AppContext, list: T, start_at: StartAt, overdraw: Pixels, renderer: impl (Fn(&T::Content) -> AnyElement) + 'static) -> Self { + pub fn create(cx: &mut AppContext, list: T, overdraw: Pixels, renderer: impl (Fn(&T::Content) -> AnyElement) + 'static) -> Self { AsyncListComponent { list: Rc::new(RefCell::new(list)), cache: Default::default(), - alignment: if let StartAt::Bottom = start_at { - ListAlignment::Bottom - } else { - ListAlignment::Top - }, overdraw, bounds_flags: cx.new_model(|_| (false, false)), renderer: Rc::new(RefCell::new(renderer)), @@ -94,12 +88,8 @@ where AsyncListIndex::RelativeToBottom(0) }; - println!(" {:?}", index); - let list = self.list.clone(); - println!("Pushed to bottom"); - let renderer = self.renderer.clone(); let len = borrow.len(); From 51f1ee8f3f91171ddf420e5b61941029d6c1abdb Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Mon, 18 Nov 2024 22:58:24 -0500 Subject: [PATCH 07/16] scrolling *kinda* works --- src/discord/src/channel/mod.rs | 38 ++++++++----- src/discord/src/client.rs | 2 +- src/ui/src/component/async_list/element.rs | 7 +++ src/ui/src/component/async_list/marker.rs | 2 - src/ui/src/component/async_list/mod.rs | 64 ++++++++++++++++++---- 5 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index 6ab40a1..ac4d9ab 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -63,6 +63,8 @@ impl Channel for DiscordChannel { } } +const DISCORD_MESSAGE_BATCH_SIZE: u8 = 5; + impl AsyncList for DiscordChannel { async fn bounded_at_bottom_by(&self) -> Option { let lock = self.cache.lock().await; @@ -98,8 +100,6 @@ impl AsyncList for DiscordChannel { } async fn get(&self, index: AsyncListIndex) -> Option> { - println!("Start"); - let mut lock = self.cache.lock().await; let cache_value = lock.get(index.clone()); @@ -109,7 +109,7 @@ impl AsyncList for DiscordChannel { return None; } - let mut result: Option = None; + let result: Option; let mut is_top = false; let mut is_bottom = false; @@ -120,13 +120,13 @@ impl AsyncList for DiscordChannel { unimplemented!() } - let v = self.client.get_messages(self.channel_id, GetMessages::new().limit(50)).await; + let v = self.client.get_messages(self.channel_id, GetMessages::new().limit(DISCORD_MESSAGE_BATCH_SIZE)).await; - let is_end = v.len() == 50; + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; is_bottom = true; is_top = v.len() == 1; - result = v.get(0).map(DiscordMessage::from_serenity); + result = v.first().map(DiscordMessage::from_serenity); let mut iter = v.iter(); @@ -149,13 +149,19 @@ impl AsyncList for DiscordChannel { } AsyncListIndex::After(message) => { // NEWEST first - let v = self.client.get_messages(self.channel_id, GetMessages::new().after(MessageId::new(message.content)).limit(50)).await; + 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() == 50; + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; is_bottom = is_end; - result = Some(DiscordMessage::from_serenity(v.get(0).unwrap())); + result = v.last().map(DiscordMessage::from_serenity); for message in v.iter().rev() { lock.insert( @@ -169,13 +175,19 @@ impl AsyncList for DiscordChannel { } } AsyncListIndex::Before(message) => { - let v = self.client.get_messages(self.channel_id, GetMessages::new().after(MessageId::new(message.content)).limit(50)).await; + 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() == 50; + let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; is_top = is_end; - result = Some(DiscordMessage::from_serenity(v.get(0).unwrap())); + result = v.first().map(DiscordMessage::from_serenity); for message in v { lock.insert( @@ -190,8 +202,6 @@ impl AsyncList for DiscordChannel { } }; - println!("End"); - result.map(|v| AsyncListResult { content: v, is_top, diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 22ab8f4..94eebfd 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -98,7 +98,7 @@ impl DiscordClient { } pub async fn get_messages(&self, channel_id: Snowflake, builder: GetMessages) -> Vec { - println!("Discord: get_messages"); + println!("Discord: get_messages: {:?}", builder); // FIXME: proper error handling ChannelId::new(channel_id.content).messages(self.discord().http.clone(), builder).await.unwrap() } diff --git a/src/ui/src/component/async_list/element.rs b/src/ui/src/component/async_list/element.rs index a5ac023..716a344 100644 --- a/src/ui/src/component/async_list/element.rs +++ b/src/ui/src/component/async_list/element.rs @@ -15,6 +15,13 @@ pub struct AsyncListComponentElementView { } impl AsyncListComponentElementView { + pub fn new_resolved(ctx: &mut ViewContext, renderer: impl (Fn(&E) -> AnyElement) + 'static, value: E) -> Self { + AsyncListComponentElementView { + element: ctx.new_model(|_| AsyncListComponentElement::Resolved(value)), + renderer: Box::new(renderer), + } + } + pub fn new( ctx: &mut ViewContext<'_, Self>, renderer: impl (Fn(&E) -> AnyElement) + 'static, diff --git a/src/ui/src/component/async_list/marker.rs b/src/ui/src/component/async_list/marker.rs index f622c45..ebd12ee 100644 --- a/src/ui/src/component/async_list/marker.rs +++ b/src/ui/src/component/async_list/marker.rs @@ -6,8 +6,6 @@ pub struct Marker { impl Render for Marker { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { - println!("Marker rendered: {}", self.name); - div() } } diff --git a/src/ui/src/component/async_list/mod.rs b/src/ui/src/component/async_list/mod.rs index bd5b137..5845d70 100644 --- a/src/ui/src/component/async_list/mod.rs +++ b/src/ui/src/component/async_list/mod.rs @@ -5,12 +5,17 @@ use std::{cell::RefCell, rc::Rc}; use element::{AsyncListComponentElement, AsyncListComponentElementView}; use gpui::{ - div, list, rgb, AnyElement, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, View, - VisualContext, + div, list, rgb, AnyElement, AppContext, Context, IntoElement, ListAlignment, ListOffset, ListState, Model, ParentElement, Pixels, Render, Styled, + View, VisualContext, }; use marker::Marker; use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; +#[derive(Clone, Copy)] +struct ListStateDirtyState { + pub new_items: usize, +} + pub struct AsyncListComponent where T::Content: 'static, @@ -23,6 +28,9 @@ where bounds_flags: Model<(bool, bool)>, renderer: Rc AnyElement>>, + + list_state: Model>, + list_state_dirty: Model>, } pub enum StartAt { @@ -41,6 +49,8 @@ where overdraw, bounds_flags: cx.new_model(|_| (false, false)), renderer: Rc::new(RefCell::new(renderer)), + list_state: cx.new_model(|_| None), + list_state_dirty: cx.new_model(|_| None), } } @@ -70,11 +80,39 @@ where ) } + fn get_or_refresh_list_state(&self, cx: &mut gpui::ViewContext) -> ListState { + let list_state_dirty = self.list_state_dirty.read(cx).clone(); + + if list_state_dirty.is_none() { + if let Some(list_state) = self.list_state.read(cx) { + return list_state.clone(); + } + } + + let new_list_state = self.list_state(cx); + 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 let Some(list_state_dirty) = list_state_dirty { + new_scroll_top.item_ix += list_state_dirty.new_items; + } + + new_list_state.scroll_to(new_scroll_top); + }; + + self.list_state.update(cx, |v, _| *v = Some(new_list_state.clone())); + + new_list_state + } + fn update(&mut self, cx: &mut gpui::ViewContext) { + let mut dirty = None; + // update bottom 'update_bottom: { if self.bounds_flags.read(cx).0 { - println!("Updating Bottom!"); let mut borrow = self.cache.borrow_mut(); let last = borrow.last(); @@ -92,13 +130,13 @@ where let renderer = self.renderer.clone(); - let len = borrow.len(); - borrow.push(cx.new_view(move |cx| { AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { list.borrow_mut().get(index).await }) })); - cx.on_next_frame(|v, cx| cx.notify()); + cx.on_next_frame(|_, cx| cx.notify()); + + dirty = Some(ListStateDirtyState { new_items: 1 }); } } @@ -120,8 +158,6 @@ where let list = self.list.clone(); - println!("Pushed to top"); - let renderer = self.renderer.clone(); borrow.insert( @@ -131,20 +167,24 @@ where }), ); - cx.on_next_frame(|v, cx| cx.notify()); + cx.on_next_frame(|_, cx| cx.notify()); + + dirty = dirty.or(Some(ListStateDirtyState { new_items: 0 })); } } + if dirty.is_some() { + self.list_state_dirty.update(cx, |v, _| *v = dirty); + } + self.bounds_flags.update(cx, |v, _| *v = (false, false)) } } impl Render for AsyncListComponent { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { - println!("Rendering"); - self.update(cx); - div().w_full().h_full().child(list(self.list_state(cx)).w_full().h_full()) + div().w_full().h_full().child(list(self.get_or_refresh_list_state(cx)).w_full().h_full()) } } From 818d7416de4f6f9959a839b7cbe1068d3d97c641 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 01:00:17 -0500 Subject: [PATCH 08/16] first functional commit --- src/cache/src/async_list/mod.rs | 29 +-- src/cache/src/async_list/refcache.rs | 36 +++- src/cache/src/async_list/refcacheslice.rs | 12 ++ src/cache/src/async_list/tests.rs | 237 ++++++++++++++++++++++ src/cache/src/channel.rs | 107 ---------- src/cache/src/lib.rs | 1 - src/chat/src/async_list.rs | 2 +- src/discord/src/channel/mod.rs | 21 +- src/discord/src/message/author.rs | 4 +- src/discord/src/message/content.rs | 2 +- src/discord/src/message/mod.rs | 2 +- src/ui/src/component/async_list/marker.rs | 4 +- src/ui/src/component/async_list/mod.rs | 39 +++- 13 files changed, 353 insertions(+), 143 deletions(-) create mode 100644 src/cache/src/async_list/tests.rs delete mode 100644 src/cache/src/channel.rs diff --git a/src/cache/src/async_list/mod.rs b/src/cache/src/async_list/mod.rs index c25ac5e..7a7bb90 100644 --- a/src/cache/src/async_list/mod.rs +++ b/src/cache/src/async_list/mod.rs @@ -1,18 +1,19 @@ pub mod refcache; pub mod refcacheslice; +pub mod tests; use std::collections::HashMap; use refcache::CacheReferences; use refcacheslice::Exists; -use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}; +use scope_chat::async_list::{AsyncListIndex, AsyncListItem, AsyncListResult}; -pub struct AsyncListCache { - cache_refs: CacheReferences<::Identifier>, - cache_map: HashMap<::Identifier, L::Content>, +pub struct AsyncListCache { + cache_refs: CacheReferences, + cache_map: HashMap, } -impl AsyncListCache { +impl AsyncListCache { pub fn new() -> Self { Self { cache_refs: CacheReferences::new(), @@ -20,35 +21,39 @@ impl AsyncListCache { } } - pub fn append_bottom(&mut self, value: L::Content) { + 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<::Identifier>, value: L::Content, is_top: bool, is_bottom: bool) { + 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); } - pub fn insert_unlocated(&mut self, value: L::Content) { + /// 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<::Identifier> { + pub fn bounded_at_top_by(&self) -> Option { self.cache_refs.top_bound() } - pub fn bounded_at_bottom_by(&self) -> Option<::Identifier> { + pub fn bounded_at_bottom_by(&self) -> Option { self.cache_refs.bottom_bound() } - pub fn get(&self, index: AsyncListIndex<::Identifier>) -> Exists> { + pub fn get(&self, index: AsyncListIndex) -> Exists> { let cache_result = self.cache_refs.get(index.clone()); if let Exists::Yes(cache_result) = cache_result { @@ -66,7 +71,7 @@ impl AsyncListCache { Exists::Unknown } - pub fn find(&self, identifier: &::Identifier) -> Option { + 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 index f2da7b2..5c518ef 100644 --- a/src/cache/src/async_list/refcache.rs +++ b/src/cache/src/async_list/refcache.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Debug}; use scope_chat::async_list::AsyncListIndex; @@ -104,8 +104,10 @@ impl CacheReferences { } if segments.len() == 0 { + let id = rand::random(); + self.dense_segments.insert( - rand::random(), + id, CacheReferencesSlice { is_bounded_at_top: is_top, is_bounded_at_bottom: is_bottom, @@ -113,6 +115,14 @@ impl CacheReferences { 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); @@ -151,8 +161,10 @@ impl CacheReferences { merged.extend(right.item_references.into_iter()); + let id = rand::random(); + self.dense_segments.insert( - rand::random(), + id, CacheReferencesSlice { is_bounded_at_top: left.is_bounded_at_top, is_bounded_at_bottom: right.is_bounded_at_bottom, @@ -160,8 +172,26 @@ impl CacheReferences { 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 index 65f0112..0a7bfea 100644 --- a/src/cache/src/async_list/refcacheslice.rs +++ b/src/cache/src/async_list/refcacheslice.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use scope_chat::async_list::AsyncListIndex; pub struct CacheReferencesSlice { @@ -112,6 +114,16 @@ impl CacheReferencesSlice { } } +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 diff --git a/src/cache/src/async_list/tests.rs b/src/cache/src/async_list/tests.rs new file mode 100644 index 0000000..6b385b3 --- /dev/null +++ b/src/cache/src/async_list/tests.rs @@ -0,0 +1,237 @@ +use std::fmt::Debug; + +use scope_chat::async_list::{AsyncListIndex, AsyncListItem, AsyncListResult}; + +use crate::async_list::{refcacheslice::Exists, AsyncListCache}; + +#[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 + } +} + +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/channel.rs b/src/cache/src/channel.rs deleted file mode 100644 index 146a3eb..0000000 --- a/src/cache/src/channel.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::sync::Arc; - -use scope_chat::{ - async_list::{AsyncList, AsyncListIndex, AsyncListItem, AsyncListResult}, - channel::Channel, -}; -use tokio::sync::Mutex; - -use crate::async_list::{refcacheslice::Exists, AsyncListCache}; - -pub struct CacheChannel(T, Arc>>); - -impl CacheChannel { - pub fn new(channel: T) -> Self - where - T::Message: 'static, - T: 'static, - { - let mut receiver = channel.get_receiver(); - let cache = Arc::new(Mutex::new(AsyncListCache::new())); - let cache_clone = cache.clone(); - - tokio::spawn(async move { - loop { - let message = receiver.recv().await.unwrap(); - - cache_clone.lock().await.append_bottom(message); - } - }); - - CacheChannel(channel, cache) - } -} - -impl Channel for CacheChannel { - type Message = T::Message; - - fn get_receiver(&self) -> tokio::sync::broadcast::Receiver { - self.0.get_receiver() - } - - fn send_message(&self, content: String, nonce: String) -> Self::Message { - self.0.send_message(content, nonce) - } -} - -impl AsyncList for CacheChannel { - type Content = T::Message; - - async fn bounded_at_top_by(&self) -> Option<::Identifier> { - let l = self.1.lock().await; - let v = l.bounded_at_top_by(); - - if let Some(v) = v { - return Some(v); - }; - - let i = self.0.bounded_at_top_by().await?; - - Some(i) - } - - async fn bounded_at_bottom_by(&self) -> Option<::Identifier> { - let l = self.1.lock().await; - let v = l.bounded_at_bottom_by(); - - if let Some(v) = v { - return Some(v); - }; - - let i = self.0.bounded_at_bottom_by().await?; - - Some(i) - } - - async fn get(&self, index: AsyncListIndex<::Identifier>) -> Option> { - let mut l = self.1.lock().await; - let v = l.get(index.clone()); - - if let Exists::Yes(v) = v { - return Some(v); - } else if let Exists::No = v { - return None; - } - - let authoritative = self.0.get(index.clone()).await?; - - l.insert(index, authoritative.content.clone(), authoritative.is_top, authoritative.is_bottom); - - Some(authoritative) - } - - async fn find(&self, identifier: &::Identifier) -> Option { - let mut l = self.1.lock().await; - let v = l.find(identifier); - - if let Some(v) = v { - return Some(v); - } - - let authoritative = self.0.find(identifier).await?; - - l.insert_unlocated(authoritative.clone()); - - Some(authoritative) - } -} diff --git a/src/cache/src/lib.rs b/src/cache/src/lib.rs index e65f511..ce2b145 100644 --- a/src/cache/src/lib.rs +++ b/src/cache/src/lib.rs @@ -1,2 +1 @@ pub mod async_list; -pub mod channel; diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index 14f4967..b45c0a8 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -9,7 +9,7 @@ pub trait AsyncList { fn bounded_at_bottom_by(&self) -> impl Future::Identifier>>; } -pub trait AsyncListItem: Clone { +pub trait AsyncListItem: Clone + Debug { type Identifier: Eq + Hash + Clone + Send + Debug; fn get_list_identifier(&self) -> Self::Identifier; diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index ac4d9ab..dd7e78e 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -6,7 +6,7 @@ use scope_chat::{ channel::Channel, }; use serenity::all::{GetMessages, MessageId, Timestamp}; -use tokio::sync::{broadcast, Mutex}; +use tokio::sync::{broadcast, Mutex, Semaphore}; use crate::{ client::DiscordClient, @@ -18,7 +18,8 @@ pub struct DiscordChannel { channel_id: Snowflake, receiver: broadcast::Receiver, client: Arc, - cache: Arc>>, + cache: Arc>>, + blocker: Semaphore, } impl DiscordChannel { @@ -32,6 +33,7 @@ impl DiscordChannel { receiver, client, cache: Arc::new(Mutex::new(AsyncListCache::new())), + blocker: Semaphore::new(1), } } } @@ -89,6 +91,8 @@ impl AsyncList for DiscordChannel { } 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); @@ -96,10 +100,16 @@ impl AsyncList for DiscordChannel { return Some(v); } - self.client.get_specific_message(self.channel_id, *identifier).await.map(|v| DiscordMessage::from_serenity(&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.clone()); @@ -179,7 +189,7 @@ impl AsyncList for DiscordChannel { .client .get_messages( self.channel_id, - GetMessages::new().after(MessageId::new(message.content)).limit(DISCORD_MESSAGE_BATCH_SIZE), + GetMessages::new().before(MessageId::new(message.content)).limit(DISCORD_MESSAGE_BATCH_SIZE), ) .await; let mut current_index: Snowflake = message; @@ -202,6 +212,8 @@ impl AsyncList for DiscordChannel { } }; + drop(permit); + result.map(|v| AsyncListResult { content: v, is_top, @@ -219,6 +231,7 @@ impl Clone for DiscordChannel { receiver: self.receiver.resubscribe(), client: self.client.clone(), cache: self.cache.clone(), + blocker: Semaphore::new(1), } } } 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 ee3a461..b1eb8d4 100644 --- a/src/discord/src/message/mod.rs +++ b/src/discord/src/message/mod.rs @@ -10,7 +10,7 @@ use crate::snowflake::Snowflake; pub mod author; pub mod content; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DiscordMessage { pub content: DiscordMessageContent, pub author: DiscordMessageAuthor, diff --git a/src/ui/src/component/async_list/marker.rs b/src/ui/src/component/async_list/marker.rs index ebd12ee..e36cfbb 100644 --- a/src/ui/src/component/async_list/marker.rs +++ b/src/ui/src/component/async_list/marker.rs @@ -1,4 +1,4 @@ -use gpui::{div, IntoElement, Render}; +use gpui::{div, IntoElement, ParentElement, Render}; pub struct Marker { pub name: &'static str, @@ -6,6 +6,6 @@ pub struct Marker { impl Render for Marker { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { - div() + div().child(self.name.to_owned()) } } diff --git a/src/ui/src/component/async_list/mod.rs b/src/ui/src/component/async_list/mod.rs index 5845d70..f676764 100644 --- a/src/ui/src/component/async_list/mod.rs +++ b/src/ui/src/component/async_list/mod.rs @@ -16,6 +16,11 @@ struct ListStateDirtyState { pub new_items: usize, } +struct BoundFlags { + pub before: bool, + pub after: bool, +} + pub struct AsyncListComponent where T::Content: 'static, @@ -25,7 +30,7 @@ where overdraw: Pixels, // top, bottom - bounds_flags: Model<(bool, bool)>, + bounds_flags: Model, renderer: Rc AnyElement>>, @@ -47,7 +52,7 @@ where list: Rc::new(RefCell::new(list)), cache: Default::default(), overdraw, - bounds_flags: cx.new_model(|_| (false, false)), + bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), renderer: Rc::new(RefCell::new(renderer)), list_state: cx.new_model(|_| None), list_state_dirty: cx.new_model(|_| None), @@ -64,12 +69,18 @@ where ListAlignment::Bottom, self.overdraw, move |idx, cx| { + if len == 0 { + cx.update_model(&bounds_model, |v, _| v.after = true); + + return div().child(cx.new_view(|_| Marker { name: "Empty" })).into_any_element(); + } + if idx == 0 { - cx.update_model(&bounds_model, |v, _| *v = (true, v.1)); + cx.update_model(&bounds_model, |v, _| v.before = true); div().child(cx.new_view(|_| Marker { name: "Upper" })) } else if idx == len + 1 { - cx.update_model(&bounds_model, |v, _| *v = (v.0, true)); + cx.update_model(&bounds_model, |v, _| v.after = true); div().child(cx.new_view(|_| Marker { name: "Lower" })) } else { @@ -112,7 +123,7 @@ where // update bottom 'update_bottom: { - if self.bounds_flags.read(cx).0 { + if self.bounds_flags.read(cx).after { let mut borrow = self.cache.borrow_mut(); let last = borrow.last(); @@ -142,7 +153,7 @@ where // update top 'update_top: { - if self.bounds_flags.read(cx).1 { + if self.bounds_flags.read(cx).before { let mut borrow = self.cache.borrow_mut(); let first = borrow.first(); @@ -153,17 +164,24 @@ where break 'update_top; }) } else { - AsyncListIndex::RelativeToTop(0) + break 'update_top; }; let list = self.list.clone(); let renderer = self.renderer.clone(); + println!("Inserting at top, aka {:?}", index); + borrow.insert( 0, cx.new_view(move |cx| { - AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { list.borrow_mut().get(index).await }) + AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { + let result = list.borrow_mut().get(index.clone()).await; + println!("{:?} resolved to {:?}", index, result); + + result + }) }), ); @@ -177,7 +195,10 @@ where self.list_state_dirty.update(cx, |v, _| *v = dirty); } - self.bounds_flags.update(cx, |v, _| *v = (false, false)) + self.bounds_flags.update(cx, |v, _| { + v.after = false; + v.before = false; + }) } } From a28486e9f2646b91b6d4d720c13cd4adc0f24d3b Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 01:30:27 -0500 Subject: [PATCH 09/16] de-genericize async_list --- src/discord/src/client.rs | 7 +- .../mod.rs => channel/message_list.rs} | 132 ++++++++++-------- src/ui/src/channel/mod.rs | 9 +- src/ui/src/component/async_list/element.rs | 69 --------- src/ui/src/component/async_list/marker.rs | 11 -- src/ui/src/component/mod.rs | 1 - src/ui/src/main.rs | 1 - 7 files changed, 79 insertions(+), 151 deletions(-) rename src/ui/src/{component/async_list/mod.rs => channel/message_list.rs} (52%) delete mode 100644 src/ui/src/component/async_list/element.rs delete mode 100644 src/ui/src/component/async_list/marker.rs delete mode 100644 src/ui/src/component/mod.rs diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 94eebfd..07b07eb 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -1,9 +1,12 @@ use std::{ - collections::HashMap, sync::{Arc, OnceLock} + collections::HashMap, + sync::{Arc, OnceLock}, }; use serenity::{ - all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, GetMessages, Message, MessageId, Nonce, RawEventHandler}, + all::{ + Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, GetMessages, Http, Message, MessageId, Nonce, RawEventHandler, + }, async_trait, }; use tokio::sync::{broadcast, RwLock}; diff --git a/src/ui/src/component/async_list/mod.rs b/src/ui/src/channel/message_list.rs similarity index 52% rename from src/ui/src/component/async_list/mod.rs rename to src/ui/src/channel/message_list.rs index f676764..6787392 100644 --- a/src/ui/src/component/async_list/mod.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,15 +1,10 @@ -pub mod element; -pub mod marker; - use std::{cell::RefCell, rc::Rc}; -use element::{AsyncListComponentElement, AsyncListComponentElementView}; -use gpui::{ - div, list, rgb, AnyElement, AppContext, Context, IntoElement, ListAlignment, ListOffset, ListState, Model, ParentElement, Pixels, Render, Styled, - View, VisualContext, +use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled}; +use scope_chat::{ + async_list::{AsyncListIndex, AsyncListItem}, + channel::Channel, }; -use marker::Marker; -use scope_chat::async_list::{AsyncList, AsyncListIndex, AsyncListItem}; #[derive(Clone, Copy)] struct ListStateDirtyState { @@ -21,19 +16,22 @@ struct BoundFlags { pub after: bool, } -pub struct AsyncListComponent +pub enum Element { + Unresolved, + Resolved(T), +} + +pub struct MessageListComponent where - T::Content: 'static, + C::Content: 'static, { - list: Rc>, - cache: Rc>>>>, + list: Rc>, + cache: Model>>>, overdraw: Pixels, // top, bottom bounds_flags: Model, - renderer: Rc AnyElement>>, - list_state: Model>, list_state_dirty: Model>, } @@ -43,25 +41,23 @@ pub enum StartAt { Top, } -impl AsyncListComponent +impl MessageListComponent where T: 'static, { - pub fn create(cx: &mut AppContext, list: T, overdraw: Pixels, renderer: impl (Fn(&T::Content) -> AnyElement) + 'static) -> Self { - AsyncListComponent { + pub fn create(cx: &mut AppContext, list: T, overdraw: Pixels) -> Self { + MessageListComponent { list: Rc::new(RefCell::new(list)), - cache: Default::default(), + cache: cx.new_model(|_| Default::default()), overdraw, bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), - renderer: Rc::new(RefCell::new(renderer)), list_state: cx.new_model(|_| None), list_state_dirty: cx.new_model(|_| None), } } - fn list_state(&self, _: &mut gpui::ViewContext) -> ListState { - let handle = self.cache.clone(); - let len = self.cache.borrow().len(); + fn list_state(&self, cx: &mut gpui::ViewContext) -> ListState { + let len = self.cache.read(cx).len(); let bounds_model = self.bounds_flags.clone(); ListState::new( @@ -72,19 +68,19 @@ where if len == 0 { cx.update_model(&bounds_model, |v, _| v.after = true); - return div().child(cx.new_view(|_| Marker { name: "Empty" })).into_any_element(); + return div().into_any_element(); } if idx == 0 { cx.update_model(&bounds_model, |v, _| v.before = true); - div().child(cx.new_view(|_| Marker { name: "Upper" })) + div() } else if idx == len + 1 { cx.update_model(&bounds_model, |v, _| v.after = true); - div().child(cx.new_view(|_| Marker { name: "Lower" })) + div() } else { - div().text_color(rgb(0xFFFFFF)).child(handle.borrow().get(idx - 1).unwrap().clone()) + div().text_color(rgb(0xFFFFFF)).child("Chottomatte") } .into_any_element() }, @@ -122,73 +118,85 @@ where let mut dirty = None; // update bottom - 'update_bottom: { - if self.bounds_flags.read(cx).after { - let mut borrow = self.cache.borrow_mut(); + if self.bounds_flags.read(cx).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 AsyncListComponentElement::Resolved(v) = last.model.read(cx).element.read(cx) { + AsyncListIndex::After(if let Element::Resolved(Some(v)) = last { v.get_list_identifier() } else { - break 'update_bottom; + return; }) } else { AsyncListIndex::RelativeToBottom(0) }; - let list = self.list.clone(); + borrow.push(Element::Unresolved); - let renderer = self.renderer.clone(); + let insert_index = borrow.len(); + let mut async_ctx = cx.to_async(); - borrow.push(cx.new_view(move |cx| { - AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { list.borrow_mut().get(index).await }) - })); + cx.foreground_executor() + .spawn(async move { + let v = list_handle.borrow().get(index.clone()).await; - cx.on_next_frame(|_, cx| cx.notify()); + 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 }); - } + }); } // update top - 'update_top: { - if self.bounds_flags.read(cx).before { - let mut borrow = self.cache.borrow_mut(); + if self.bounds_flags.read(cx).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 AsyncListComponentElement::Resolved(v) = first.model.read(cx).element.read(cx) { + AsyncListIndex::Before(if let Element::Resolved(Some(v)) = first { v.get_list_identifier() } else { - break 'update_top; + return; }) } else { - break 'update_top; + return; }; - let list = self.list.clone(); - - let renderer = self.renderer.clone(); - println!("Inserting at top, aka {:?}", index); - borrow.insert( - 0, - cx.new_view(move |cx| { - AsyncListComponentElementView::new(cx, move |rf| (renderer.borrow())(rf), async move { - let result = list.borrow_mut().get(index.clone()).await; - println!("{:?} resolved to {:?}", index, result); + borrow.insert(0, Element::Unresolved); - result - }) - }), - ); + let insert_index = 0; + let mut async_ctx = cx.to_async(); - cx.on_next_frame(|_, cx| cx.notify()); + cx.foreground_executor() + .spawn(async move { + let v = list_handle.borrow().get(index.clone()).await; + + cache_model + .update(&mut async_ctx, |borrow, cx| { + borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); + cx.notify(); + }) + .unwrap(); + }) + .detach(); dirty = dirty.or(Some(ListStateDirtyState { new_items: 0 })); - } + }); } if dirty.is_some() { @@ -202,7 +210,7 @@ where } } -impl Render for AsyncListComponent { +impl Render for MessageListComponent { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { self.update(cx); diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 986971e..3bf711f 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -1,14 +1,14 @@ pub mod message; +pub mod message_list; use components::input::{InputEvent, TextInput}; use gpui::{div, IntoElement, ParentElement, Pixels, Render, Styled, View, VisualContext}; use message::{message, MessageGroup}; +use message_list::MessageListComponent; use scope_chat::channel::Channel; -use crate::component::async_list::{AsyncListComponent, StartAt}; - pub struct ChannelView { - list_view: View>, + list_view: View>, message_input: View, } @@ -16,8 +16,7 @@ impl ChannelView { pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: C) -> Self { let mut channel_listener = channel.get_receiver(); - let list_view = - ctx.new_view(|cx| AsyncListComponent::create(cx, channel, Pixels(30.), |msg| message(MessageGroup::new(msg.clone())).into_any_element())); + 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(); diff --git a/src/ui/src/component/async_list/element.rs b/src/ui/src/component/async_list/element.rs deleted file mode 100644 index 716a344..0000000 --- a/src/ui/src/component/async_list/element.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::future::Future; - -use gpui::{div, rgb, AnyElement, Context, Model, ParentElement, Render, Styled, ViewContext}; -use scope_chat::async_list::{AsyncListItem, AsyncListResult}; - -pub enum AsyncListComponentElement { - Waiting, - Resolved(E), - None, -} - -pub struct AsyncListComponentElementView { - pub element: Model>, - renderer: Box AnyElement>, -} - -impl AsyncListComponentElementView { - pub fn new_resolved(ctx: &mut ViewContext, renderer: impl (Fn(&E) -> AnyElement) + 'static, value: E) -> Self { - AsyncListComponentElementView { - element: ctx.new_model(|_| AsyncListComponentElement::Resolved(value)), - renderer: Box::new(renderer), - } - } - - pub fn new( - ctx: &mut ViewContext<'_, Self>, - renderer: impl (Fn(&E) -> AnyElement) + 'static, - future: impl Future>> + 'static, - ) -> AsyncListComponentElementView { - let model = ctx.new_model(|_| AsyncListComponentElement::Waiting); - - let mut async_ctx = ctx.to_async(); - - let model_handle = model.clone(); - - ctx - .foreground_executor() - .spawn(async move { - let result = future.await; - - async_ctx - .update_model(&model_handle, |v, cx| { - if let Some(result) = result { - *v = AsyncListComponentElement::Resolved(result.content); - } else { - *v = AsyncListComponentElement::None; - } - cx.notify(); - }) - .unwrap(); - }) - .detach(); - - AsyncListComponentElementView { - element: model, - renderer: Box::new(renderer), - } - } -} - -impl Render for AsyncListComponentElementView { - fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { - match self.element.read(cx) { - AsyncListComponentElement::Waiting => div().w_full().h_8().flex().items_center().justify_center().text_color(rgb(0xFFFFFF)).child("Waiting..."), - AsyncListComponentElement::None => div().w_full().h_8().flex().items_center().justify_center().text_color(rgb(0xFFFFFF)).child("None!"), - AsyncListComponentElement::Resolved(v) => div().w_full().h_full().child((self.renderer)(v)), - } - } -} diff --git a/src/ui/src/component/async_list/marker.rs b/src/ui/src/component/async_list/marker.rs deleted file mode 100644 index e36cfbb..0000000 --- a/src/ui/src/component/async_list/marker.rs +++ /dev/null @@ -1,11 +0,0 @@ -use gpui::{div, IntoElement, ParentElement, Render}; - -pub struct Marker { - pub name: &'static str, -} - -impl Render for Marker { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { - div().child(self.name.to_owned()) - } -} diff --git a/src/ui/src/component/mod.rs b/src/ui/src/component/mod.rs deleted file mode 100644 index ce2b145..0000000 --- a/src/ui/src/component/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod async_list; diff --git a/src/ui/src/main.rs b/src/ui/src/main.rs index b0bae59..4282e3b 100644 --- a/src/ui/src/main.rs +++ b/src/ui/src/main.rs @@ -3,7 +3,6 @@ pub mod app; pub mod app_state; pub mod channel; pub mod menu; -pub mod component; use std::sync::Arc; From 461874fe921251ed32322419db9c64109a9af537 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 01:45:20 -0500 Subject: [PATCH 10/16] message grouping works again --- src/ui/src/channel/message_list.rs | 35 +++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index 6787392..f4c2a78 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -4,8 +4,11 @@ use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, List use scope_chat::{ async_list::{AsyncListIndex, AsyncListItem}, channel::Channel, + message::Message, }; +use super::message::{message, MessageGroup}; + #[derive(Clone, Copy)] struct ListStateDirtyState { pub new_items: usize, @@ -57,9 +60,31 @@ where } fn list_state(&self, cx: &mut gpui::ViewContext) -> ListState { - let len = self.cache.read(cx).len(); let bounds_model = self.bounds_flags.clone(); + let mut groups = vec![]; + + for item in self.cache.read(cx) { + 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)) => { + 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 { + groups.push(Element::Resolved(Some(MessageGroup::new(m.clone())))); + } + } + }, + } + } + + let len = groups.len(); + ListState::new( if len == 0 { 1 } else { len + 2 }, ListAlignment::Bottom, @@ -80,7 +105,11 @@ where div() } else { - div().text_color(rgb(0xFFFFFF)).child("Chottomatte") + 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() }, @@ -137,7 +166,7 @@ where borrow.push(Element::Unresolved); - let insert_index = borrow.len(); + let insert_index = borrow.len() - 1; let mut async_ctx = cx.to_async(); cx.foreground_executor() From 7687ce554b5d13f719bdf362d1553af76c80eb69 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 03:16:38 -0500 Subject: [PATCH 11/16] Fixed random scrolling up block --- Cargo.lock | 10 ++++++++++ src/chat/src/async_list.rs | 6 +++++- src/chat/src/channel.rs | 2 +- src/discord/src/channel/mod.rs | 20 +++++++++++-------- src/ui/Cargo.toml | 1 + src/ui/src/app.rs | 2 +- src/ui/src/channel/message_list.rs | 31 ++++++++++++++++++++++-------- src/ui/src/channel/mod.rs | 31 +++++++++++++++--------------- 8 files changed, 68 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c66798..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", diff --git a/src/chat/src/async_list.rs b/src/chat/src/async_list.rs index b45c0a8..c94cb83 100644 --- a/src/chat/src/async_list.rs +++ b/src/chat/src/async_list.rs @@ -4,7 +4,10 @@ pub trait AsyncList { type Content: AsyncListItem; fn bounded_at_top_by(&self) -> impl Future::Identifier>>; - fn get(&self, index: AsyncListIndex<::Identifier>) -> impl Future>>; + 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>>; } @@ -41,6 +44,7 @@ impl Debug for AsyncListIndex { } } +#[derive(Clone)] pub struct AsyncListResult { pub content: T, pub is_top: bool, diff --git a/src/chat/src/channel.rs b/src/chat/src/channel.rs index eaff99b..24a798c 100644 --- a/src/chat/src/channel.rs +++ b/src/chat/src/channel.rs @@ -2,7 +2,7 @@ use tokio::sync::broadcast; use crate::{async_list::AsyncList, message::Message}; -pub trait Channel: AsyncList { +pub trait Channel: AsyncList + Send + Sync { type Message: Message; fn get_receiver(&self) -> broadcast::Receiver; diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index dd7e78e..b4488ca 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use scope_backend_cache::async_list::{refcacheslice::Exists, AsyncListCache}; use scope_chat::{ @@ -96,6 +96,8 @@ impl AsyncList for DiscordChannel { let lock = self.cache.lock().await; let cache_value = lock.find(identifier); + drop(lock); + if let Some(v) = cache_value { return Some(v); } @@ -109,7 +111,6 @@ impl AsyncList for DiscordChannel { 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.clone()); @@ -169,16 +170,16 @@ impl AsyncList for DiscordChannel { let mut current_index: Snowflake = message; let is_end = v.len() == DISCORD_MESSAGE_BATCH_SIZE as usize; - is_bottom = is_end; + is_bottom = is_end && v.len() == 1; result = v.last().map(DiscordMessage::from_serenity); - for message in v.iter().rev() { + for (message, index) in v.iter().rev().zip(0..) { lock.insert( AsyncListIndex::After(current_index), DiscordMessage::from_serenity(message), false, - is_end, + is_end && index == (v.len() - 1), ); current_index = Snowflake { content: message.id.get() } @@ -194,17 +195,19 @@ impl AsyncList for DiscordChannel { .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; + is_top = is_end && v.len() == 1; result = v.first().map(DiscordMessage::from_serenity); - for message in v { + for (message, index) in v.iter().zip(0..) { lock.insert( AsyncListIndex::Before(current_index), DiscordMessage::from_serenity(&message), false, - is_end, + is_end && index == (v.len() - 1), ); current_index = Snowflake { content: message.id.get() } @@ -213,6 +216,7 @@ impl AsyncList for DiscordChannel { }; drop(permit); + drop(lock); result.map(|v| AsyncListResult { content: v, diff --git a/src/ui/Cargo.toml b/src/ui/Cargo.toml index 81f65a7..f252266 100644 --- a/src/ui/Cargo.toml +++ b/src/ui/Cargo.toml @@ -30,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 13b7656..7ed7c6c 100644 --- a/src/ui/src/app.rs +++ b/src/ui/src/app.rs @@ -37,7 +37,7 @@ impl App { async_channel.update(&mut context, |a, b| { *a = Some(view); b.notify() - }) + }); }) .detach(); diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index f4c2a78..e8be665 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::sync::Arc; use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled}; use scope_chat::{ @@ -6,6 +6,7 @@ use scope_chat::{ channel::Channel, message::Message, }; +use tokio::sync::RwLock; use super::message::{message, MessageGroup}; @@ -28,7 +29,7 @@ pub struct MessageListComponent where C::Content: 'static, { - list: Rc>, + list: Arc>, cache: Model>>>, overdraw: Pixels, @@ -50,7 +51,7 @@ where { pub fn create(cx: &mut AppContext, list: T, overdraw: Pixels) -> Self { MessageListComponent { - list: Rc::new(RefCell::new(list)), + list: Arc::new(RwLock::new(list)), cache: cx.new_model(|_| Default::default()), overdraw, bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), @@ -165,13 +166,20 @@ where }; borrow.push(Element::Unresolved); + cx.notify(); let insert_index = borrow.len() - 1; let mut async_ctx = cx.to_async(); cx.foreground_executor() .spawn(async move { - let v = list_handle.borrow().get(index.clone()).await; + 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| { @@ -204,16 +212,21 @@ where return; }; - println!("Inserting at top, aka {:?}", index); - borrow.insert(0, Element::Unresolved); + cx.notify(); let insert_index = 0; let mut async_ctx = cx.to_async(); cx.foreground_executor() .spawn(async move { - let v = list_handle.borrow().get(index.clone()).await; + 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| { @@ -229,7 +242,9 @@ where } if dirty.is_some() { - self.list_state_dirty.update(cx, |v, _| *v = dirty); + self.list_state_dirty.update(cx, |v, cx| { + *v = dirty; + }); } self.bounds_flags.update(cx, |v, _| { diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 3bf711f..4b3c043 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -21,22 +21,21 @@ impl ChannelView { let async_model = list_view.clone(); let mut async_ctx = ctx.to_async(); - ctx - .foreground_executor() - .spawn(async move { - loop { - let message = channel_listener.recv().await.unwrap(); - - async_model - .update(&mut async_ctx, |data, ctx| { - // data.add_external_message(message); - todo!(); - ctx.notify(); - }) - .unwrap(); - } - }) - .detach(); + // ctx + // .foreground_executor() + // .spawn(async move { + // loop { + // let message = channel_listener.recv().await.unwrap(); + // async_model + // .update(&mut async_ctx, |data, ctx| { + // // data.add_external_message(message); + // todo!(); + // ctx.notify(); + // }) + // .unwrap(); + // } + // }) + // .detach(); let message_input = ctx.new_view(|cx| { let mut input = components::input::TextInput::new(cx); From 634003259617cfab841ec1beef47f2afed90b271 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 03:44:25 -0500 Subject: [PATCH 12/16] scrolling updates --- src/ui/src/channel/message_list.rs | 58 +++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index e8be665..54473d8 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled}; +use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, ViewContext}; use scope_chat::{ async_list::{AsyncListIndex, AsyncListItem}, channel::Channel, @@ -13,6 +13,7 @@ use super::message::{message, MessageGroup}; #[derive(Clone, Copy)] struct ListStateDirtyState { pub new_items: usize, + pub shift: usize, } struct BoundFlags { @@ -49,14 +50,27 @@ impl MessageListComponent where T: 'static, { - pub fn create(cx: &mut AppContext, list: T, overdraw: Pixels) -> Self { + pub fn create(cx: &mut ViewContext, list: T, overdraw: Pixels) -> Self { + let cache = cx.new_model(|_| Default::default()); + let list_state_dirty = cx.new_model(|_| None); + + cx.observe(&cache, |_, _, cx| { + cx.notify(); + }) + .detach(); + + cx.observe(&list_state_dirty, |_, _, cx| { + cx.notify(); + }) + .detach(); + MessageListComponent { list: Arc::new(RwLock::new(list)), - cache: cx.new_model(|_| Default::default()), + cache, overdraw, bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), list_state: cx.new_model(|_| None), - list_state_dirty: cx.new_model(|_| None), + list_state_dirty, } } @@ -133,9 +147,23 @@ where let mut new_scroll_top = old_list_state.logical_scroll_top(); if let Some(list_state_dirty) = list_state_dirty { - new_scroll_top.item_ix += list_state_dirty.new_items; + if old_list_state.logical_scroll_top().item_ix == old_list_state.item_count() { + new_scroll_top.item_ix += list_state_dirty.new_items; + + if list_state_dirty.new_items > 0 { + new_scroll_top.offset_in_item = Pixels(0.); + } + } + + new_scroll_top.item_ix += list_state_dirty.shift; } + println!( + "List states:\n Old: {:?}\n New: {:?}", + old_list_state.logical_scroll_top(), + new_scroll_top + ); + new_list_state.scroll_to(new_scroll_top); }; @@ -166,7 +194,6 @@ where }; borrow.push(Element::Unresolved); - cx.notify(); let insert_index = borrow.len() - 1; let mut async_ctx = cx.to_async(); @@ -190,7 +217,7 @@ where }) .detach(); - dirty = Some(ListStateDirtyState { new_items: 1 }); + dirty = Some(ListStateDirtyState { new_items: 1, shift: 0 }); }); } @@ -213,7 +240,6 @@ where }; borrow.insert(0, Element::Unresolved); - cx.notify(); let insert_index = 0; let mut async_ctx = cx.to_async(); @@ -237,14 +263,22 @@ where }) .detach(); - dirty = dirty.or(Some(ListStateDirtyState { new_items: 0 })); + 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() { - self.list_state_dirty.update(cx, |v, cx| { - *v = dirty; - }); + cx.notify(); } self.bounds_flags.update(cx, |v, _| { From 644a0d60efd3b578bd37d9f5112f0fc4fec62519 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 04:38:05 -0500 Subject: [PATCH 13/16] update the way that list state is invalidated --- src/ui/src/channel/message_list.rs | 120 +++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index 54473d8..129958b 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -16,11 +16,13 @@ struct ListStateDirtyState { pub shift: usize, } +#[derive(Clone, Copy)] struct BoundFlags { pub before: bool, pub after: bool, } +#[derive(Debug)] pub enum Element { Unresolved, Resolved(T), @@ -52,14 +54,27 @@ where { 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); - cx.observe(&cache, |_, _, cx| { + 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(); - cx.observe(&list_state_dirty, |_, _, cx| { + 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(); @@ -69,7 +84,7 @@ where cache, overdraw, bounds_flags: cx.new_model(|_| BoundFlags { before: false, after: false }), - list_state: cx.new_model(|_| None), + list_state, list_state_dirty, } } @@ -77,30 +92,57 @@ where 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).clone(); + + 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 in self.cache.read(cx) { + 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 { + remaining_gap_new_items -= 1; + } } let len = groups.len(); - ListState::new( + let new_list_state = ListState::new( if len == 0 { 1 } else { len + 2 }, ListAlignment::Bottom, self.overdraw, @@ -128,36 +170,23 @@ where } .into_any_element() }, - ) - } - - fn get_or_refresh_list_state(&self, cx: &mut gpui::ViewContext) -> ListState { - let list_state_dirty = self.list_state_dirty.read(cx).clone(); + ); - if list_state_dirty.is_none() { - if let Some(list_state) = self.list_state.read(cx) { - return list_state.clone(); - } - } - - let new_list_state = self.list_state(cx); 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 let Some(list_state_dirty) = list_state_dirty { - if old_list_state.logical_scroll_top().item_ix == old_list_state.item_count() { - new_scroll_top.item_ix += list_state_dirty.new_items; + if old_list_state.logical_scroll_top().item_ix == old_list_state.item_count() { + new_scroll_top.item_ix += added_elements_bottom; - if list_state_dirty.new_items > 0 { - new_scroll_top.offset_in_item = Pixels(0.); - } + if added_elements_bottom > 0 { + new_scroll_top.offset_in_item = Pixels(0.); } - - new_scroll_top.item_ix += list_state_dirty.shift; } + new_scroll_top.item_ix += shift; + println!( "List states:\n Old: {:?}\n New: {:?}", old_list_state.logical_scroll_top(), @@ -175,8 +204,10 @@ where fn update(&mut self, cx: &mut gpui::ViewContext) { let mut dirty = None; + let mut flags = *self.bounds_flags.read(cx); + // update bottom - if self.bounds_flags.read(cx).after { + if flags.after { let cache_model = self.cache.clone(); let list_handle = self.list.clone(); @@ -187,6 +218,7 @@ where AsyncListIndex::After(if let Element::Resolved(Some(v)) = last { v.get_list_identifier() } else { + flags.after = false; return; }) } else { @@ -211,6 +243,7 @@ where cache_model .update(&mut async_ctx, |borrow, cx| { borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); + cx.notify(); }) .unwrap(); @@ -222,20 +255,24 @@ where } // update top - if self.bounds_flags.read(cx).before { + 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(); + println!("First: {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; }; @@ -254,12 +291,18 @@ where let v = receiver.await.unwrap(); + println!("Got"); + cache_model .update(&mut async_ctx, |borrow, cx| { + println!("Updating cache"); + borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); cx.notify(); }) .unwrap(); + + println!("Updated"); }) .detach(); @@ -282,8 +325,13 @@ where } self.bounds_flags.update(cx, |v, _| { - v.after = false; - v.before = false; + if flags.after { + v.after = false; + } + + if flags.before { + v.before = false; + } }) } } @@ -292,6 +340,18 @@ impl Render for MessageListComponent { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { self.update(cx); - div().w_full().h_full().child(list(self.get_or_refresh_list_state(cx)).w_full().h_full()) + 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()) } } From a38f8851f13f663f5fc030ca621e431ddd8519db Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 05:07:50 -0500 Subject: [PATCH 14/16] finalize --- src/chat/src/channel.rs | 2 +- src/discord/src/channel/mod.rs | 2 +- src/discord/src/client.rs | 5 +- src/ui/src/app.rs | 10 ++-- src/ui/src/channel/message_list.rs | 28 +++++++++- src/ui/src/channel/mod.rs | 88 +++++++++++++++++++----------- 6 files changed, 92 insertions(+), 43 deletions(-) diff --git a/src/chat/src/channel.rs b/src/chat/src/channel.rs index 24a798c..12d2a3b 100644 --- a/src/chat/src/channel.rs +++ b/src/chat/src/channel.rs @@ -2,7 +2,7 @@ use tokio::sync::broadcast; use crate::{async_list::AsyncList, message::Message}; -pub trait Channel: AsyncList + Send + Sync { +pub trait Channel: AsyncList + Send + Sync + Clone { type Message: Message; fn get_receiver(&self) -> broadcast::Receiver; diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index b4488ca..e0611e7 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use scope_backend_cache::async_list::{refcacheslice::Exists, AsyncListCache}; use scope_chat::{ diff --git a/src/discord/src/client.rs b/src/discord/src/client.rs index 07b07eb..b3d0c9c 100644 --- a/src/discord/src/client.rs +++ b/src/discord/src/client.rs @@ -4,9 +4,7 @@ use std::{ }; use serenity::{ - all::{ - Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, GetMessages, Http, Message, MessageId, Nonce, RawEventHandler, - }, + all::{Cache, ChannelId, Context, CreateMessage, Event, EventHandler, GatewayIntents, GetMessages, Http, Message, MessageId, RawEventHandler}, async_trait, }; use tokio::sync::{broadcast, RwLock}; @@ -15,7 +13,6 @@ use crate::{ channel::DiscordChannel, message::{ author::{DiscordMessageAuthor, DisplayName}, - content::DiscordMessageContent, DiscordMessage, }, snowflake::Snowflake, diff --git a/src/ui/src/app.rs b/src/ui/src/app.rs index 7ed7c6c..407ab44 100644 --- a/src/ui/src/app.rs +++ b/src/ui/src/app.rs @@ -34,10 +34,12 @@ impl App { 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 129958b..5da0e88 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{div, list, rgb, AppContext, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, ViewContext}; +use gpui::{div, list, rgb, Context, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, Styled, ViewContext}; use scope_chat::{ async_list::{AsyncListIndex, AsyncListItem}, channel::Channel, @@ -89,6 +89,32 @@ where } } + 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(Element::Resolved(None)) = borrow.last() { + borrow.pop(); + } + + borrow.push(Element::Resolved(Some(message))); + borrow.push(Element::Resolved(None)); + + cx.update_model(&self.list_state_dirty, |v, _| *v = Some(ListStateDirtyState { new_items: 1, shift: 0 })); + + cx.notify(); + }); + } + fn list_state(&self, cx: &mut gpui::ViewContext) -> ListState { let bounds_model = self.bounds_flags.clone(); diff --git a/src/ui/src/channel/mod.rs b/src/ui/src/channel/mod.rs index 4b3c043..f61e279 100644 --- a/src/ui/src/channel/mod.rs +++ b/src/ui/src/channel/mod.rs @@ -2,8 +2,7 @@ pub mod message; pub mod message_list; use components::input::{InputEvent, TextInput}; -use gpui::{div, IntoElement, ParentElement, Pixels, Render, Styled, View, VisualContext}; -use message::{message, MessageGroup}; +use gpui::{div, ParentElement, Pixels, Render, Styled, View, VisualContext}; use message_list::MessageListComponent; use scope_chat::channel::Channel; @@ -14,28 +13,37 @@ pub struct ChannelView { impl ChannelView { pub fn create(ctx: &mut gpui::ViewContext<'_, ChannelView>, channel: C) -> Self { - let mut channel_listener = channel.get_receiver(); + let channel_listener = channel.get_receiver(); + + 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(); - // ctx - // .foreground_executor() - // .spawn(async move { - // loop { - // let message = channel_listener.recv().await.unwrap(); - // async_model - // .update(&mut async_ctx, |data, ctx| { - // // data.add_external_message(message); - // todo!(); - // ctx.notify(); - // }) - // .unwrap(); - // } - // }) - // .detach(); + ctx + .foreground_executor() + .spawn(async move { + loop { + 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.append_message(ctx, message); + ctx.notify(); + }) + .unwrap(); + } + }) + .detach(); let message_input = ctx.new_view(|cx| { let mut input = components::input::TextInput::new(cx); @@ -45,24 +53,40 @@ 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(); + let content = text_input.read(ctx).text().to_string(); + if content.is_empty() { + return; + } + + text_input.update(ctx, |text_input, cx| { + text_input.set_text("", cx); + }); + + let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); + let pending = c2.send_message(content, nonce); + + let mut async_ctx = ctx.to_async(); - // text_input.update(ctx, |text_input, cx| { - // text_input.set_text("", cx); - // }); + let async_model = async_model.clone(); - // let nonce = random_string::generate(20, random_string::charsets::ALPHANUMERIC); - // let pending = channel.send_message(content, nonce); + 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, _| unimplemented!()); - // ctx.notify(); + ctx.notify(); } }) .detach(); @@ -72,7 +96,7 @@ impl ChannelView { } impl Render for ChannelView { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + fn render(&mut self, _: &mut gpui::ViewContext) -> impl gpui::IntoElement { div() .flex() .flex_col() From 5fc0323bce1275542589f8d4660808465b3eed30 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 05:09:57 -0500 Subject: [PATCH 15/16] cleanup & prepare for PR --- src/discord/src/channel/mod.rs | 2 +- src/ui/src/channel/message_list.rs | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index e0611e7..94186b8 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -65,7 +65,7 @@ impl Channel for DiscordChannel { } } -const DISCORD_MESSAGE_BATCH_SIZE: u8 = 5; +const DISCORD_MESSAGE_BATCH_SIZE: u8 = 50; impl AsyncList for DiscordChannel { async fn bounded_at_bottom_by(&self) -> Option { diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index 5da0e88..d459155 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -213,12 +213,6 @@ where new_scroll_top.item_ix += shift; - println!( - "List states:\n Old: {:?}\n New: {:?}", - old_list_state.logical_scroll_top(), - new_scroll_top - ); - new_list_state.scroll_to(new_scroll_top); }; @@ -288,8 +282,6 @@ where self.cache.update(cx, |borrow, cx| { let first = borrow.first(); - println!("First: {first:?}"); - let index = if let Some(first) = first { AsyncListIndex::Before(if let Element::Resolved(Some(v)) = first { v.get_list_identifier() @@ -317,18 +309,12 @@ where let v = receiver.await.unwrap(); - println!("Got"); - cache_model .update(&mut async_ctx, |borrow, cx| { - println!("Updating cache"); - borrow[insert_index] = Element::Resolved(v.map(|v| v.content)); cx.notify(); }) .unwrap(); - - println!("Updated"); }) .detach(); From 5b609bb1aaa4101a97bee6a7e5e5660b0246341a Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 19 Nov 2024 17:17:07 -0500 Subject: [PATCH 16/16] fix: make cargo clippy happy --- src/cache/src/async_list/mod.rs | 6 ++++++ src/cache/src/async_list/refcache.rs | 12 +++++++++--- src/cache/src/async_list/tests.rs | 4 ++++ src/discord/src/channel/mod.rs | 6 +++--- src/ui/src/channel/message_list.rs | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/cache/src/async_list/mod.rs b/src/cache/src/async_list/mod.rs index 7a7bb90..4a46823 100644 --- a/src/cache/src/async_list/mod.rs +++ b/src/cache/src/async_list/mod.rs @@ -13,6 +13,12 @@ pub struct AsyncListCache { cache_map: HashMap, } +impl Default for AsyncListCache { + fn default() -> Self { + Self::new() + } +} + impl AsyncListCache { pub fn new() -> Self { Self { diff --git a/src/cache/src/async_list/refcache.rs b/src/cache/src/async_list/refcache.rs index 5c518ef..a81d72b 100644 --- a/src/cache/src/async_list/refcache.rs +++ b/src/cache/src/async_list/refcache.rs @@ -13,6 +13,12 @@ pub struct CacheReferences { bottom_bounded_identifier: Option, } +impl Default for CacheReferences { + fn default() -> Self { + Self::new() + } +} + impl CacheReferences { pub fn new() -> Self { Self { @@ -71,7 +77,7 @@ impl CacheReferences { } } - return Exists::Unknown; + Exists::Unknown } /// you mut **KNOW** that the item you are inserting is not: @@ -103,7 +109,7 @@ impl CacheReferences { } } - if segments.len() == 0 { + if segments.is_empty() { let id = rand::random(); self.dense_segments.insert( @@ -159,7 +165,7 @@ impl CacheReferences { merged.push(item); - merged.extend(right.item_references.into_iter()); + merged.extend(right.item_references); let id = rand::random(); diff --git a/src/cache/src/async_list/tests.rs b/src/cache/src/async_list/tests.rs index 6b385b3..3970fd8 100644 --- a/src/cache/src/async_list/tests.rs +++ b/src/cache/src/async_list/tests.rs @@ -1,9 +1,12 @@ 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); @@ -15,6 +18,7 @@ impl AsyncListItem for ListItem { } } +#[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); diff --git a/src/discord/src/channel/mod.rs b/src/discord/src/channel/mod.rs index 94186b8..eeb6590 100644 --- a/src/discord/src/channel/mod.rs +++ b/src/discord/src/channel/mod.rs @@ -112,7 +112,7 @@ impl AsyncList for DiscordChannel { 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.clone()); + let cache_value = lock.get(index); if let Exists::Yes(v) = cache_value { return Some(v); @@ -149,7 +149,7 @@ impl AsyncList for DiscordChannel { lock.append_bottom(msg); for message in iter { - let msg = DiscordMessage::from_serenity(&message); + let msg = DiscordMessage::from_serenity(message); let nid = msg.get_list_identifier(); lock.insert(AsyncListIndex::Before(id), msg, false, is_end); @@ -205,7 +205,7 @@ impl AsyncList for DiscordChannel { for (message, index) in v.iter().zip(0..) { lock.insert( AsyncListIndex::Before(current_index), - DiscordMessage::from_serenity(&message), + DiscordMessage::from_serenity(message), false, is_end && index == (v.len() - 1), ); diff --git a/src/ui/src/channel/message_list.rs b/src/ui/src/channel/message_list.rs index d459155..7eef140 100644 --- a/src/ui/src/channel/message_list.rs +++ b/src/ui/src/channel/message_list.rs @@ -118,7 +118,7 @@ where 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).clone(); + let list_state_dirty = *self.list_state_dirty.read(cx); let mut added_elements_bottom = 0; let mut shift = 0;