1689 lines · 58.6 KB · Rust
RawDownload
use std::collections::HashSet;use std::path::PathBuf;use std::sync::Arc; use crate::agent_connection_store::AgentConnectionStore; use crate::thread_metadata_store::{    ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths,};use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread}; use agent::ThreadStore;use agent_client_protocol::schema::v1 as acp;use agent_settings::AgentSettings;use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};use collections::HashMap;use editor::Editor;use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    ListState, Render, SharedString, Subscription, Task, TaskExt, WeakEntity, Window, list,    prelude::*, px,};use itertools::Itertools as _;use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};use picker::{    Picker, PickerDelegate,    highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},};use project::{AgentId, AgentServerStore};use settings::Settings as _;use theme::ActiveTheme;use ui::{    AgentThreadStatus, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, ScrollAxes,    Scrollbars, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*,    utils::platform_title_bar_height,};use util::ResultExt;use util::paths::PathExt;use workspace::{    CloseWindow, ModalView, PathList, RecentWorkspace, SerializedWorkspaceLocation, Workspace,    WorkspaceDb, WorkspaceId,}; use zed_actions::agents_sidebar::FocusSidebarFilter;use zed_actions::editor::{MoveDown, MoveUp}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]enum ThreadFilter {    #[default]    All,    ArchivedOnly,} #[derive(Clone)]enum ArchiveListItem {    BucketSeparator(TimeBucket),    Entry {        thread: ThreadMetadata,        highlight_positions: Vec<usize>,    },} #[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TimeBucket {    Today,    Yesterday,    ThisWeek,    PastWeek,    Older,} impl TimeBucket {    fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {        if date == reference {            return TimeBucket::Today;        }        if date == reference - TimeDelta::days(1) {            return TimeBucket::Yesterday;        }        let week = date.iso_week();        if reference.iso_week() == week {            return TimeBucket::ThisWeek;        }        let last_week = (reference - TimeDelta::days(7)).iso_week();        if week == last_week {            return TimeBucket::PastWeek;        }        TimeBucket::Older    }     fn label(&self) -> &'static str {        match self {            TimeBucket::Today => "Today",            TimeBucket::Yesterday => "Yesterday",            TimeBucket::ThisWeek => "This Week",            TimeBucket::PastWeek => "Past Week",            TimeBucket::Older => "Older",        }    }} pub fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> {    let query_chars: Vec<char> = query.chars().collect();    if query_chars.is_empty() {        return Some(Vec::new());    }     let candidate_chars: Vec<(usize, char)> = candidate.char_indices().collect();    let window_count = candidate_chars.len().checked_sub(query_chars.len() - 1)?;     'outer: for window_start in 0..window_count {        for (qi, &query_char) in query_chars.iter().enumerate() {            let (_, cand_char) = candidate_chars[window_start + qi];            if !cand_char.eq_ignore_ascii_case(&query_char) {                continue 'outer;            }        }        return Some(            (0..query_chars.len())                .map(|qi| candidate_chars[window_start + qi].0)                .collect(),        );    }     None} pub enum ThreadsArchiveViewEvent {    Close,    Activate { thread: ThreadMetadata },    CancelRestore { thread_id: ThreadId },    Import,    NewThread,} impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {} pub struct ThreadsArchiveView {    _history_subscription: Subscription,    focus_handle: FocusHandle,    list_state: ListState,    items: Vec<ArchiveListItem>,    selection: Option<usize>,    hovered_index: Option<usize>,    preserve_selection_on_next_update: bool,    filter_editor: Entity<Editor>,    _subscriptions: Vec<gpui::Subscription>,    _refresh_history_task: Task<()>,    workspace: WeakEntity<Workspace>,    agent_connection_store: WeakEntity<AgentConnectionStore>,    agent_server_store: WeakEntity<AgentServerStore>,    restoring: HashSet<ThreadId>,    archived_thread_ids: HashSet<ThreadId>,    archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>,    _load_branch_names_task: Task<()>,    thread_filter: ThreadFilter,} impl ThreadsArchiveView {    pub fn new(        workspace: WeakEntity<Workspace>,        agent_connection_store: WeakEntity<AgentConnectionStore>,        agent_server_store: WeakEntity<AgentServerStore>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();         let filter_editor = cx.new(|cx| {            let mut editor = Editor::single_line(window, cx);            editor.set_placeholder_text("Search all threads…", window, cx);            editor        });         let filter_editor_subscription =            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {                if let editor::EditorEvent::BufferEdited = event {                    this.update_items(cx);                }            });         let filter_focus_handle = filter_editor.read(cx).focus_handle(cx);        cx.on_focus_in(            &filter_focus_handle,            window,            |this: &mut Self, _window, cx| {                if this.selection.is_some() {                    this.selection = None;                    cx.notify();                }            },        )        .detach();         let thread_metadata_store_subscription = cx.observe(            &ThreadMetadataStore::global(cx),            |this: &mut Self, _, cx| {                this.update_items(cx);                this.reload_branch_names_if_threads_changed(cx);            },        );         cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, _window, cx| {            this.selection = None;            cx.notify();        })        .detach();         let mut this = Self {            _history_subscription: Subscription::new(|| {}),            focus_handle,            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),            items: Vec::new(),            selection: None,            hovered_index: None,            preserve_selection_on_next_update: false,            filter_editor,            _subscriptions: vec![                filter_editor_subscription,                thread_metadata_store_subscription,            ],            _refresh_history_task: Task::ready(()),            workspace,            agent_connection_store,            agent_server_store,            restoring: HashSet::default(),            archived_thread_ids: HashSet::default(),            archived_branch_names: HashMap::default(),            _load_branch_names_task: Task::ready(()),            thread_filter: ThreadFilter::All,        };         this.update_items(cx);        this.reload_branch_names_if_threads_changed(cx);        this    }     pub fn has_selection(&self) -> bool {        self.selection.is_some()    }     pub fn clear_selection(&mut self) {        self.selection = None;    }     pub fn mark_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {        self.restoring.insert(*thread_id);        cx.notify();    }     pub fn clear_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {        self.restoring.remove(thread_id);        cx.notify();    }     pub fn focus_filter_editor(&self, window: &mut Window, cx: &mut App) {        let handle = self.filter_editor.read(cx).focus_handle(cx);        handle.focus(window, cx);    }     pub fn is_filter_editor_focused(&self, window: &Window, cx: &App) -> bool {        self.filter_editor            .read(cx)            .focus_handle(cx)            .is_focused(window)    }     fn update_items(&mut self, cx: &mut Context<Self>) {        let store = ThreadMetadataStore::global(cx).read(cx);         // If we're filtering to archived threads but none remain (e.g. the        // user just deleted the last one), fall back to showing all threads        // so they aren't stranded with an empty list and a disabled toggle.        if self.thread_filter == ThreadFilter::ArchivedOnly            && store.archived_entries().next().is_none()        {            self.thread_filter = ThreadFilter::All;        }         let thread_filter = self.thread_filter;        let sessions = store            .entries()            .filter(|t| match thread_filter {                ThreadFilter::All => true,                ThreadFilter::ArchivedOnly => t.archived,            })            .sorted_by_cached_key(|t| t.created_at.unwrap_or(t.updated_at))            .rev()            .cloned()            .collect::<Vec<_>>();         let query = self.filter_editor.read(cx).text(cx).to_lowercase();        let today = Local::now().naive_local().date();         let mut items = Vec::with_capacity(sessions.len() + 5);        let mut current_bucket: Option<TimeBucket> = None;         for session in sessions {            let highlight_positions = if !query.is_empty() {                let title = session                    .title                    .as_ref()                    .map(|t| t.as_ref())                    .unwrap_or(DEFAULT_THREAD_TITLE);                if let Some(positions) = fuzzy_match_positions(&query, title) {                    positions                } else {                    // If title didn't match, also try matching the project name                    // (the basename of any of the thread's worktree paths), so                    // typing a project name surfaces its threads here too.                    let worktree_matched = session.folder_paths().paths().iter().any(|p| {                        p.as_path()                            .file_name()                            .and_then(|name| name.to_str())                            .is_some_and(|name| fuzzy_match_positions(&query, name).is_some())                    });                    if !worktree_matched {                        continue;                    }                    Vec::new()                }            } else {                Vec::new()            };             let entry_bucket = {                let entry_date = session                    .created_at                    .unwrap_or(session.updated_at)                    .with_timezone(&Local)                    .naive_local()                    .date();                TimeBucket::from_dates(today, entry_date)            };             if Some(entry_bucket) != current_bucket {                current_bucket = Some(entry_bucket);                items.push(ArchiveListItem::BucketSeparator(entry_bucket));            }             items.push(ArchiveListItem::Entry {                thread: session,                highlight_positions,            });        }         let preserve = self.preserve_selection_on_next_update;        self.preserve_selection_on_next_update = false;         let saved_scroll = self.list_state.logical_scroll_top();         self.list_state.reset(items.len());        self.items = items;         if let Some(ix) = self.hovered_index {            if ix >= self.items.len() || !self.is_selectable_item(ix) {                self.hovered_index = None;            }        }         self.list_state.scroll_to(saved_scroll);         if preserve {            if let Some(ix) = self.selection {                let next = self.find_next_selectable(ix).or_else(|| {                    ix.checked_sub(1)                        .and_then(|i| self.find_previous_selectable(i))                });                self.selection = next;                if let Some(next) = next {                    self.list_state.scroll_to_reveal_item(next);                }            }        } else {            self.selection = None;        }         cx.notify();    }     fn reload_branch_names_if_threads_changed(&mut self, cx: &mut Context<Self>) {        let current_ids: HashSet<ThreadId> = self            .items            .iter()            .filter_map(|item| match item {                ArchiveListItem::Entry { thread, .. } => Some(thread.thread_id),                _ => None,            })            .collect();         if current_ids != self.archived_thread_ids {            self.archived_thread_ids = current_ids;            self.load_archived_branch_names(cx);        }    }     fn load_archived_branch_names(&mut self, cx: &mut Context<Self>) {        let task = ThreadMetadataStore::global(cx)            .read(cx)            .get_all_archived_branch_names(cx);        self._load_branch_names_task = cx.spawn(async move |this, cx| {            if let Some(branch_names) = task.await.log_err() {                this.update(cx, |this, cx| {                    this.archived_branch_names = branch_names;                    cx.notify();                })                .log_err();            }        });    }     fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.filter_editor.update(cx, |editor, cx| {            editor.set_text("", window, cx);        });    }     fn archive_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {        self.preserve_selection_on_next_update = true;        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.archive(thread_id, None, cx));    }     fn archive_selected_thread(        &mut self,        _: &ArchiveSelectedThread,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(ix) = self.selection else { return };        let Some(ArchiveListItem::Entry { thread, .. }) = self.items.get(ix) else {            return;        };         if thread.archived {            return;        }         self.archive_thread(thread.thread_id, cx);    }     fn unarchive_thread(        &mut self,        thread: ThreadMetadata,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if self.restoring.contains(&thread.thread_id) {            return;        }         if thread.folder_paths().is_empty() {            self.show_project_picker_for_thread(thread, window, cx);            return;        }         self.mark_restoring(&thread.thread_id, cx);        self.selection = None;        self.reset_filter_editor_text(window, cx);        cx.emit(ThreadsArchiveViewEvent::Activate { thread });    }     fn show_project_picker_for_thread(        &mut self,        thread: ThreadMetadata,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(workspace) = self.workspace.upgrade() else {            return;        };         let archive_view = cx.weak_entity();        let fs = workspace.read(cx).app_state().fs.clone();        let current_workspace_id = workspace.read(cx).database_id();        let sibling_workspace_ids: HashSet<WorkspaceId> = workspace            .read(cx)            .multi_workspace()            .and_then(|mw| mw.upgrade())            .map(|mw| {                mw.read(cx)                    .workspaces()                    .filter_map(|ws| ws.read(cx).database_id())                    .collect()            })            .unwrap_or_default();         workspace.update(cx, |workspace, cx| {            workspace.toggle_modal(window, cx, |window, cx| {                ProjectPickerModal::new(                    thread,                    fs,                    archive_view,                    current_workspace_id,                    sibling_workspace_ids,                    window,                    cx,                )            });

Lines 1–500 of 1689.