parent
e5588fc9ea
commit
51d678d33b
5 changed files with 228 additions and 311 deletions
|
@ -1406,10 +1406,9 @@ mod tests {
|
||||||
history: &Entity<HistoryStore>,
|
history: &Entity<HistoryStore>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<(HistoryEntryId, String)> {
|
) -> Vec<(HistoryEntryId, String)> {
|
||||||
history.read_with(cx, |history, cx| {
|
history.read_with(cx, |history, _| {
|
||||||
history
|
history
|
||||||
.entries(cx)
|
.entries()
|
||||||
.iter()
|
|
||||||
.map(|e| (e.id(), e.title().to_string()))
|
.map(|e| (e.id(), e.title().to_string()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
|
|
|
@ -86,6 +86,7 @@ enum SerializedRecentOpen {
|
||||||
|
|
||||||
pub struct HistoryStore {
|
pub struct HistoryStore {
|
||||||
threads: Vec<DbThreadMetadata>,
|
threads: Vec<DbThreadMetadata>,
|
||||||
|
entries: Vec<HistoryEntry>,
|
||||||
context_store: Entity<assistant_context::ContextStore>,
|
context_store: Entity<assistant_context::ContextStore>,
|
||||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
|
@ -97,7 +98,7 @@ impl HistoryStore {
|
||||||
context_store: Entity<assistant_context::ContextStore>,
|
context_store: Entity<assistant_context::ContextStore>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
|
let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let entries = Self::load_recently_opened_entries(cx).await;
|
let entries = Self::load_recently_opened_entries(cx).await;
|
||||||
|
@ -116,6 +117,7 @@ impl HistoryStore {
|
||||||
context_store,
|
context_store,
|
||||||
recently_opened_entries: VecDeque::default(),
|
recently_opened_entries: VecDeque::default(),
|
||||||
threads: Vec::default(),
|
threads: Vec::default(),
|
||||||
|
entries: Vec::default(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_save_recently_opened_entries_task: Task::ready(()),
|
_save_recently_opened_entries_task: Task::ready(()),
|
||||||
}
|
}
|
||||||
|
@ -181,20 +183,18 @@ impl HistoryStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.threads = threads;
|
this.threads = threads;
|
||||||
cx.notify();
|
this.update_entries(cx);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> {
|
fn update_entries(&mut self, cx: &mut Context<Self>) {
|
||||||
let mut history_entries = Vec::new();
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||||
return history_entries;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut history_entries = Vec::new();
|
||||||
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
|
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
|
||||||
history_entries.extend(
|
history_entries.extend(
|
||||||
self.context_store
|
self.context_store
|
||||||
|
@ -205,17 +205,12 @@ impl HistoryStore {
|
||||||
);
|
);
|
||||||
|
|
||||||
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
|
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
|
||||||
history_entries
|
self.entries = history_entries;
|
||||||
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self, cx: &App) -> bool {
|
pub fn is_empty(&self, _cx: &App) -> bool {
|
||||||
self.threads.is_empty()
|
self.entries.is_empty()
|
||||||
&& self
|
|
||||||
.context_store
|
|
||||||
.read(cx)
|
|
||||||
.unordered_contexts()
|
|
||||||
.next()
|
|
||||||
.is_none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
|
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
|
||||||
|
@ -356,7 +351,7 @@ impl HistoryStore {
|
||||||
self.save_recently_opened_entries(cx);
|
self.save_recently_opened_entries(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
|
||||||
self.entries(cx).into_iter().take(limit).collect()
|
self.entries.iter().cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -805,7 +805,7 @@ pub(crate) fn search_threads(
|
||||||
history_store: &Entity<HistoryStore>,
|
history_store: &Entity<HistoryStore>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Vec<HistoryEntry>> {
|
) -> Task<Vec<HistoryEntry>> {
|
||||||
let threads = history_store.read(cx).entries(cx);
|
let threads = history_store.read(cx).entries().collect();
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return Task::ready(threads);
|
return Task::ready(threads);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread};
|
||||||
use agent2::{HistoryEntry, HistoryStore};
|
use agent2::{HistoryEntry, HistoryStore};
|
||||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||||
use editor::{Editor, EditorEvent};
|
use editor::{Editor, EditorEvent};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||||
};
|
};
|
||||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
use std::{fmt::Display, ops::Range};
|
||||||
|
use text::Bias;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{
|
use ui::{
|
||||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||||
Tooltip, prelude::*,
|
Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
|
||||||
|
|
||||||
pub struct AcpThreadHistory {
|
pub struct AcpThreadHistory {
|
||||||
pub(crate) history_store: Entity<HistoryStore>,
|
pub(crate) history_store: Entity<HistoryStore>,
|
||||||
|
@ -22,38 +22,38 @@ pub struct AcpThreadHistory {
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
hovered_index: Option<usize>,
|
hovered_index: Option<usize>,
|
||||||
search_editor: Entity<Editor>,
|
search_editor: Entity<Editor>,
|
||||||
all_entries: Arc<Vec<HistoryEntry>>,
|
search_query: SharedString,
|
||||||
// When the search is empty, we display date separators between history entries
|
|
||||||
// This vector contains an enum of either a separator or an actual entry
|
visible_items: Vec<ListItemType>,
|
||||||
separated_items: Vec<ListItemType>,
|
|
||||||
// Maps entry indexes to list item indexes
|
|
||||||
separated_item_indexes: Vec<u32>,
|
|
||||||
_separated_items_task: Option<Task<()>>,
|
|
||||||
search_state: SearchState,
|
|
||||||
scrollbar_visibility: bool,
|
scrollbar_visibility: bool,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
local_timezone: UtcOffset,
|
local_timezone: UtcOffset,
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SearchState {
|
_update_task: Task<()>,
|
||||||
Empty,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
Searching {
|
|
||||||
query: SharedString,
|
|
||||||
_task: Task<()>,
|
|
||||||
},
|
|
||||||
Searched {
|
|
||||||
query: SharedString,
|
|
||||||
matches: Vec<StringMatch>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ListItemType {
|
enum ListItemType {
|
||||||
BucketSeparator(TimeBucket),
|
BucketSeparator(TimeBucket),
|
||||||
Entry {
|
Entry {
|
||||||
index: usize,
|
entry: HistoryEntry,
|
||||||
format: EntryTimeFormat,
|
format: EntryTimeFormat,
|
||||||
},
|
},
|
||||||
|
SearchResult {
|
||||||
|
entry: HistoryEntry,
|
||||||
|
positions: Vec<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListItemType {
|
||||||
|
fn history_entry(&self) -> Option<&HistoryEntry> {
|
||||||
|
match self {
|
||||||
|
ListItemType::Entry { entry, .. } => Some(entry),
|
||||||
|
ListItemType::SearchResult { entry, .. } => Some(entry),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ThreadHistoryEvent {
|
pub enum ThreadHistoryEvent {
|
||||||
|
@ -78,12 +78,15 @@ impl AcpThreadHistory {
|
||||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||||
if let EditorEvent::BufferEdited = event {
|
if let EditorEvent::BufferEdited = event {
|
||||||
let query = search_editor.read(cx).text(cx);
|
let query = search_editor.read(cx).text(cx);
|
||||||
this.search(query.into(), cx);
|
if this.search_query != query {
|
||||||
|
this.search_query = query.into();
|
||||||
|
this.update_visible_items(false, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||||
this.update_all_entries(cx);
|
this.update_visible_items(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let scroll_handle = UniformListScrollHandle::default();
|
let scroll_handle = UniformListScrollHandle::default();
|
||||||
|
@ -94,10 +97,7 @@ impl AcpThreadHistory {
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
hovered_index: None,
|
hovered_index: None,
|
||||||
search_state: SearchState::Empty,
|
visible_items: Default::default(),
|
||||||
all_entries: Default::default(),
|
|
||||||
separated_items: Default::default(),
|
|
||||||
separated_item_indexes: Default::default(),
|
|
||||||
search_editor,
|
search_editor,
|
||||||
scrollbar_visibility: true,
|
scrollbar_visibility: true,
|
||||||
scrollbar_state,
|
scrollbar_state,
|
||||||
|
@ -105,29 +105,61 @@ impl AcpThreadHistory {
|
||||||
chrono::Local::now().offset().local_minus_utc(),
|
chrono::Local::now().offset().local_minus_utc(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
search_query: SharedString::default(),
|
||||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||||
_separated_items_task: None,
|
_update_task: Task::ready(()),
|
||||||
};
|
};
|
||||||
this.update_all_entries(cx);
|
this.update_visible_items(false, cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
|
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
|
||||||
let new_entries: Arc<Vec<HistoryEntry>> = self
|
let entries = self
|
||||||
.history_store
|
.history_store
|
||||||
.update(cx, |store, cx| store.entries(cx))
|
.update(cx, |store, _| store.entries().collect());
|
||||||
.into();
|
let new_list_items = if self.search_query.is_empty() {
|
||||||
|
self.add_list_separators(entries, cx)
|
||||||
|
} else {
|
||||||
|
self.filter_search_results(entries, cx)
|
||||||
|
};
|
||||||
|
let selected_history_entry = if preserve_selected_item {
|
||||||
|
self.selected_history_entry().cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
self._separated_items_task.take();
|
self._update_task = cx.spawn(async move |this, cx| {
|
||||||
|
let new_visible_items = new_list_items.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let new_selected_index = if let Some(history_entry) = selected_history_entry {
|
||||||
|
let history_entry_id = history_entry.id();
|
||||||
|
new_visible_items
|
||||||
|
.iter()
|
||||||
|
.position(|visible_entry| {
|
||||||
|
visible_entry
|
||||||
|
.history_entry()
|
||||||
|
.is_some_and(|entry| entry.id() == history_entry_id)
|
||||||
|
})
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
let mut items = Vec::with_capacity(new_entries.len() + 1);
|
this.visible_items = new_visible_items;
|
||||||
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
|
this.set_selected_index(new_selected_index, Bias::Right, cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let bg_task = cx.background_spawn(async move {
|
fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let mut items = Vec::with_capacity(entries.len() + 1);
|
||||||
let mut bucket = None;
|
let mut bucket = None;
|
||||||
let today = Local::now().naive_local().date();
|
let today = Local::now().naive_local().date();
|
||||||
|
|
||||||
for (index, entry) in new_entries.iter().enumerate() {
|
for entry in entries.into_iter() {
|
||||||
let entry_date = entry
|
let entry_date = entry
|
||||||
.updated_at()
|
.updated_at()
|
||||||
.with_timezone(&Local)
|
.with_timezone(&Local)
|
||||||
|
@ -140,75 +172,33 @@ impl AcpThreadHistory {
|
||||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||||
}
|
}
|
||||||
|
|
||||||
indexes.push(items.len() as u32);
|
|
||||||
items.push(ListItemType::Entry {
|
items.push(ListItemType::Entry {
|
||||||
index,
|
entry,
|
||||||
format: entry_bucket.into(),
|
format: entry_bucket.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(new_entries, items, indexes)
|
items
|
||||||
});
|
})
|
||||||
|
|
||||||
let task = cx.spawn(async move |this, cx| {
|
|
||||||
let (new_entries, items, indexes) = bg_task.await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let previously_selected_entry =
|
|
||||||
this.all_entries.get(this.selected_index).map(|e| e.id());
|
|
||||||
|
|
||||||
this.all_entries = new_entries;
|
|
||||||
this.separated_items = items;
|
|
||||||
this.separated_item_indexes = indexes;
|
|
||||||
|
|
||||||
match &this.search_state {
|
|
||||||
SearchState::Empty => {
|
|
||||||
if this.selected_index >= this.all_entries.len() {
|
|
||||||
this.set_selected_entry_index(
|
|
||||||
this.all_entries.len().saturating_sub(1),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
} else if let Some(prev_id) = previously_selected_entry
|
|
||||||
&& let Some(new_ix) = this
|
|
||||||
.all_entries
|
|
||||||
.iter()
|
|
||||||
.position(|probe| probe.id() == prev_id)
|
|
||||||
{
|
|
||||||
this.set_selected_entry_index(new_ix, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
|
||||||
this.search(query.clone(), cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
});
|
|
||||||
self._separated_items_task = Some(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) {
|
fn filter_search_results(
|
||||||
if query.is_empty() {
|
&self,
|
||||||
self.search_state = SearchState::Empty;
|
entries: Vec<HistoryEntry>,
|
||||||
cx.notify();
|
cx: &App,
|
||||||
return;
|
) -> Task<Vec<ListItemType>> {
|
||||||
}
|
let query = self.search_query.clone();
|
||||||
|
cx.background_spawn({
|
||||||
let all_entries = self.all_entries.clone();
|
|
||||||
|
|
||||||
let fuzzy_search_task = cx.background_spawn({
|
|
||||||
let query = query.clone();
|
|
||||||
let executor = cx.background_executor().clone();
|
let executor = cx.background_executor().clone();
|
||||||
async move {
|
async move {
|
||||||
let mut candidates = Vec::with_capacity(all_entries.len());
|
let mut candidates = Vec::with_capacity(entries.len());
|
||||||
|
|
||||||
for (idx, entry) in all_entries.iter().enumerate() {
|
for (idx, entry) in entries.iter().enumerate() {
|
||||||
candidates.push(StringMatchCandidate::new(idx, entry.title()));
|
candidates.push(StringMatchCandidate::new(idx, entry.title()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_MATCHES: usize = 100;
|
const MAX_MATCHES: usize = 100;
|
||||||
|
|
||||||
fuzzy::match_strings(
|
let matches = fuzzy::match_strings(
|
||||||
&candidates,
|
&candidates,
|
||||||
&query,
|
&query,
|
||||||
false,
|
false,
|
||||||
|
@ -217,74 +207,61 @@ impl AcpThreadHistory {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor,
|
executor,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|search_match| ListItemType::SearchResult {
|
||||||
|
entry: entries[search_match.candidate_id].clone(),
|
||||||
|
positions: search_match.positions,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
let task = cx.spawn({
|
|
||||||
let query = query.clone();
|
|
||||||
async move |this, cx| {
|
|
||||||
let matches = fuzzy_search_task.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let SearchState::Searching {
|
|
||||||
query: current_query,
|
|
||||||
_task,
|
|
||||||
} = &this.search_state
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if &query == current_query {
|
|
||||||
this.search_state = SearchState::Searched {
|
|
||||||
query: query.clone(),
|
|
||||||
matches,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.set_selected_entry_index(0, cx);
|
|
||||||
cx.notify();
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.search_state = SearchState::Searching { query, _task: task };
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matched_count(&self) -> usize {
|
|
||||||
match &self.search_state {
|
|
||||||
SearchState::Empty => self.all_entries.len(),
|
|
||||||
SearchState::Searching { .. } => 0,
|
|
||||||
SearchState::Searched { matches, .. } => matches.len(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_item_count(&self) -> usize {
|
|
||||||
match &self.search_state {
|
|
||||||
SearchState::Empty => self.separated_items.len(),
|
|
||||||
SearchState::Searching { .. } => 0,
|
|
||||||
SearchState::Searched { matches, .. } => matches.len(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_produced_no_matches(&self) -> bool {
|
fn search_produced_no_matches(&self) -> bool {
|
||||||
match &self.search_state {
|
self.visible_items.is_empty() && !self.search_query.is_empty()
|
||||||
SearchState::Empty => false,
|
|
||||||
SearchState::Searching { .. } => false,
|
|
||||||
SearchState::Searched { matches, .. } => matches.is_empty(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
|
fn selected_history_entry(&self) -> Option<&HistoryEntry> {
|
||||||
match &self.search_state {
|
self.get_history_entry(self.selected_index)
|
||||||
SearchState::Empty => self.all_entries.get(ix),
|
}
|
||||||
SearchState::Searching { .. } => None,
|
|
||||||
SearchState::Searched { matches, .. } => matches
|
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
|
||||||
.get(ix)
|
self.visible_items.get(visible_items_ix)?.history_entry()
|
||||||
.and_then(|m| self.all_entries.get(m.candidate_id)),
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
|
||||||
|
if self.visible_items.len() == 0 {
|
||||||
|
self.selected_index = 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
while matches!(
|
||||||
|
self.visible_items.get(index),
|
||||||
|
None | Some(ListItemType::BucketSeparator(..))
|
||||||
|
) {
|
||||||
|
index = match bias {
|
||||||
|
Bias::Left => {
|
||||||
|
if index == 0 {
|
||||||
|
self.visible_items.len() - 1
|
||||||
|
} else {
|
||||||
|
index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bias::Right => {
|
||||||
|
if index >= self.visible_items.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.selected_index = index;
|
||||||
|
self.scroll_handle
|
||||||
|
.scroll_to_item(index, ScrollStrategy::Top);
|
||||||
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_previous(
|
pub fn select_previous(
|
||||||
|
@ -293,13 +270,10 @@ impl AcpThreadHistory {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let count = self.matched_count();
|
if self.selected_index == 0 {
|
||||||
if count > 0 {
|
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||||
if self.selected_index == 0 {
|
} else {
|
||||||
self.set_selected_entry_index(count - 1, cx);
|
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
|
||||||
} else {
|
|
||||||
self.set_selected_entry_index(self.selected_index - 1, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,13 +283,10 @@ impl AcpThreadHistory {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let count = self.matched_count();
|
if self.selected_index == self.visible_items.len() - 1 {
|
||||||
if count > 0 {
|
self.set_selected_index(0, Bias::Right, cx);
|
||||||
if self.selected_index == count - 1 {
|
} else {
|
||||||
self.set_selected_entry_index(0, cx);
|
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
|
||||||
} else {
|
|
||||||
self.set_selected_entry_index(self.selected_index + 1, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,35 +296,47 @@ impl AcpThreadHistory {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let count = self.matched_count();
|
self.set_selected_index(0, Bias::Right, cx);
|
||||||
if count > 0 {
|
|
||||||
self.set_selected_entry_index(0, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let count = self.matched_count();
|
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||||
if count > 0 {
|
|
||||||
self.set_selected_entry_index(count - 1, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.selected_index = entry_index;
|
self.confirm_entry(self.selected_index, cx);
|
||||||
|
}
|
||||||
|
|
||||||
let scroll_ix = match self.search_state {
|
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||||
SearchState::Empty | SearchState::Searching { .. } => self
|
let Some(entry) = self.get_history_entry(ix) else {
|
||||||
.separated_item_indexes
|
return;
|
||||||
.get(entry_index)
|
};
|
||||||
.map(|ix| *ix as usize)
|
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
|
||||||
.unwrap_or(entry_index + 1),
|
}
|
||||||
SearchState::Searched { .. } => entry_index,
|
|
||||||
|
fn remove_selected_thread(
|
||||||
|
&mut self,
|
||||||
|
_: &RemoveSelectedThread,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.remove_thread(self.selected_index, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
|
||||||
|
let Some(entry) = self.get_history_entry(visible_item_ix) else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.scroll_handle
|
let task = match entry {
|
||||||
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
|
HistoryEntry::AcpThread(thread) => self
|
||||||
|
.history_store
|
||||||
cx.notify();
|
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
|
||||||
|
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
|
||||||
|
this.delete_text_thread(context.path.clone(), cx)
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||||
|
@ -393,91 +376,33 @@ impl AcpThreadHistory {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
fn render_list_items(
|
||||||
self.confirm_entry(self.selected_index, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
|
||||||
let Some(entry) = self.get_match(ix) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_selected_thread(
|
|
||||||
&mut self,
|
|
||||||
_: &RemoveSelectedThread,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.remove_thread(self.selected_index, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) {
|
|
||||||
let Some(entry) = self.get_match(ix) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task = match entry {
|
|
||||||
HistoryEntry::AcpThread(thread) => self
|
|
||||||
.history_store
|
|
||||||
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
|
|
||||||
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
|
|
||||||
this.delete_text_thread(context.path.clone(), cx)
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
task.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_items(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Vec<AnyElement> {
|
) -> Vec<AnyElement> {
|
||||||
match &self.search_state {
|
self.visible_items
|
||||||
SearchState::Empty => self
|
.get(range.clone())
|
||||||
.separated_items
|
.into_iter()
|
||||||
.get(range)
|
.flatten()
|
||||||
.iter()
|
.enumerate()
|
||||||
.flat_map(|items| {
|
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
|
||||||
items
|
.collect()
|
||||||
.iter()
|
|
||||||
.map(|item| self.render_list_item(item, vec![], cx))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
SearchState::Searched { matches, .. } => matches[range]
|
|
||||||
.iter()
|
|
||||||
.filter_map(|m| {
|
|
||||||
let entry = self.all_entries.get(m.candidate_id)?;
|
|
||||||
Some(self.render_history_entry(
|
|
||||||
entry,
|
|
||||||
EntryTimeFormat::DateAndTime,
|
|
||||||
m.candidate_id,
|
|
||||||
m.positions.clone(),
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
SearchState::Searching { .. } => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_list_item(
|
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||||
&self,
|
|
||||||
item: &ListItemType,
|
|
||||||
highlight_positions: Vec<usize>,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> AnyElement {
|
|
||||||
match item {
|
match item {
|
||||||
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
|
ListItemType::Entry { entry, format } => self
|
||||||
Some(entry) => self
|
.render_history_entry(entry, *format, ix, Vec::default(), cx)
|
||||||
.render_history_entry(entry, *format, *index, highlight_positions, cx)
|
.into_any(),
|
||||||
.into_any(),
|
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
|
||||||
None => Empty.into_any_element(),
|
entry,
|
||||||
},
|
EntryTimeFormat::DateAndTime,
|
||||||
|
ix,
|
||||||
|
positions.clone(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
ListItemType::BucketSeparator(bucket) => div()
|
ListItemType::BucketSeparator(bucket) => div()
|
||||||
.px(DynamicSpacing::Base06.rems(cx))
|
.px(DynamicSpacing::Base06.rems(cx))
|
||||||
.pt_2()
|
.pt_2()
|
||||||
|
@ -495,12 +420,12 @@ impl AcpThreadHistory {
|
||||||
&self,
|
&self,
|
||||||
entry: &HistoryEntry,
|
entry: &HistoryEntry,
|
||||||
format: EntryTimeFormat,
|
format: EntryTimeFormat,
|
||||||
list_entry_ix: usize,
|
ix: usize,
|
||||||
highlight_positions: Vec<usize>,
|
highlight_positions: Vec<usize>,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let selected = list_entry_ix == self.selected_index;
|
let selected = ix == self.selected_index;
|
||||||
let hovered = Some(list_entry_ix) == self.hovered_index;
|
let hovered = Some(ix) == self.hovered_index;
|
||||||
let timestamp = entry.updated_at().timestamp();
|
let timestamp = entry.updated_at().timestamp();
|
||||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||||
|
|
||||||
|
@ -508,7 +433,7 @@ impl AcpThreadHistory {
|
||||||
.w_full()
|
.w_full()
|
||||||
.pb_1()
|
.pb_1()
|
||||||
.child(
|
.child(
|
||||||
ListItem::new(list_entry_ix)
|
ListItem::new(ix)
|
||||||
.rounded()
|
.rounded()
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
|
@ -530,8 +455,8 @@ impl AcpThreadHistory {
|
||||||
)
|
)
|
||||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||||
if *is_hovered {
|
if *is_hovered {
|
||||||
this.hovered_index = Some(list_entry_ix);
|
this.hovered_index = Some(ix);
|
||||||
} else if this.hovered_index == Some(list_entry_ix) {
|
} else if this.hovered_index == Some(ix) {
|
||||||
this.hovered_index = None;
|
this.hovered_index = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,16 +471,14 @@ impl AcpThreadHistory {
|
||||||
.tooltip(move |window, cx| {
|
.tooltip(move |window, cx| {
|
||||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
.on_click(
|
||||||
this.remove_thread(list_entry_ix, cx)
|
cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
|
||||||
})),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.on_click(
|
.on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
|
||||||
cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
@ -578,7 +501,7 @@ impl Render for AcpThreadHistory {
|
||||||
.on_action(cx.listener(Self::select_last))
|
.on_action(cx.listener(Self::select_last))
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::remove_selected_thread))
|
.on_action(cx.listener(Self::remove_selected_thread))
|
||||||
.when(!self.all_entries.is_empty(), |parent| {
|
.when(!self.history_store.read(cx).is_empty(cx), |parent| {
|
||||||
parent.child(
|
parent.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.h(px(41.)) // Match the toolbar perfectly
|
.h(px(41.)) // Match the toolbar perfectly
|
||||||
|
@ -604,7 +527,7 @@ impl Render for AcpThreadHistory {
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.flex_grow();
|
.flex_grow();
|
||||||
|
|
||||||
if self.all_entries.is_empty() {
|
if self.history_store.read(cx).is_empty(cx) {
|
||||||
view.justify_center()
|
view.justify_center()
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().justify_center().child(
|
h_flex().w_full().justify_center().child(
|
||||||
|
@ -623,9 +546,9 @@ impl Render for AcpThreadHistory {
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
"thread-history",
|
"thread-history",
|
||||||
self.list_item_count(),
|
self.visible_items.len(),
|
||||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||||
this.list_items(range, window, cx)
|
this.render_list_items(range, window, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.p_1()
|
.p_1()
|
||||||
|
|
|
@ -2538,9 +2538,9 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(render_history, |this| {
|
.when(render_history, |this| {
|
||||||
let recent_history = self
|
let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
|
||||||
.history_store
|
history_store.entries().take(3).collect()
|
||||||
.update(cx, |history_store, cx| history_store.recent_entries(3, cx));
|
});
|
||||||
this.justify_end().child(
|
this.justify_end().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(
|
.child(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue