344 lines · 11.2 KB · Rust
RawDownload
use std::collections::HashMap;use std::collections::hash_map::Entry;use std::path::{Path, PathBuf}; use ai::skills::SkillReference;use serde::{Deserialize, Serialize};use warp_util::path::LineAndColumnArg;use warpui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity, ViewHandle, WindowId}; use super::buffer_location::LocalOrRemotePath;use super::view::CodeView;use crate::ai::agent::AIAgentActionId;use crate::ai::skills::SkillOpenOrigin;use crate::code_review::code_review_view::CodeReviewView;use crate::pane_group::{PaneGroup, PaneId};use crate::workspace::PaneViewLocator; pub struct CodeEditorSummary<'a> {    pub unsaved_changes: Vec<&'a CodeEditorStatus>,} impl<'a> CodeEditorSummary<'a> {    /// Create a summary from the currently open Code Editors.    pub fn new(editors: &'a [CodeEditorStatus]) -> Self {        let unsaved_changes = editors            .iter()            .filter(|editor| editor.unsaved_changes)            .collect();         Self { unsaved_changes }    }} #[derive(Copy, Clone)]pub struct CodeEditorStatus {    unsaved_changes: bool,} impl CodeEditorStatus {    pub fn new(unsaved_changes: bool) -> Self {        Self { unsaved_changes }    }     /// Fetches all code editors open in the App.    pub fn all_editors(app: &AppContext) -> impl Iterator<Item = Self> + '_ {        app.window_ids()            .flat_map(move |window_id| Self::editors_in_window(window_id, app))    }     /// Fetches all code editors in a given window.    pub fn editors_in_window(        window_id: WindowId,        app: &AppContext,    ) -> impl Iterator<Item = Self> + '_ {        app.views_of_type::<CodeView>(window_id)            .into_iter()            .flat_map(move |editors| {                editors                    .into_iter()                    .map(move |editor| Self::editor_status(&editor, app))            })    }     /// Fetches all code editors in a given tab.    pub fn editors_in_tab<'a>(        tab: &ViewHandle<PaneGroup>,        app: &'a AppContext,    ) -> impl Iterator<Item = Self> + 'a + use<'a> {        tab.as_ref(app)            .code_panes(app)            .map(move |(_, editor)| Self::editor_status(&editor, app))    }     pub fn editor_status(editor: &ViewHandle<CodeView>, app: &AppContext) -> Self {        editor.read(app, |editor_view, ctx| Self {            unsaved_changes: editor_view.contains_unsaved_changes(ctx),        })    }     pub fn status_for_code_review(review: &ViewHandle<CodeReviewView>, app: &AppContext) -> Self {        review.read(app, |review_view, ctx| Self {            unsaved_changes: review_view.has_unsaved_changes(ctx),        })    }     /// Fetches all code review views in a given window (including panel views).    pub fn code_review_views_in_window(        window_id: WindowId,        app: &AppContext,    ) -> impl Iterator<Item = Self> + '_ {        app.views_of_type::<CodeReviewView>(window_id)            .into_iter()            .flat_map(move |editors| {                editors                    .into_iter()                    .map(move |editor| Self::status_for_code_review(&editor, app))            })    }} #[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)]pub enum CodeSource {    /// A new code pane not attached to an existing file.    New {        /// When the new file is saved, open the file picker to this directory.        default_directory: Option<PathBuf>,    },    /// Opened from file links.    Link {        path: PathBuf,        range_start: Option<LineAndColumnArg>,        range_end: Option<LineAndColumnArg>,    },    /// Opened from an active AI agent conversation.    AIAction { id: AIAgentActionId },    /// Opened from project rules (WARP.md) file.    ProjectRules { location: LocalOrRemotePath },    /// Opened from file tree (local or remote).    FileTree { location: LocalOrRemotePath },    /// Opened from command palette file search (local or remote).    CommandPalette { location: LocalOrRemotePath },    /// Opened from macOS Finder via "Open With".    Finder { path: PathBuf },    /// Opened from a skill.    Skill {        reference: SkillReference,        location: LocalOrRemotePath,        origin: SkillOpenOrigin,    },} impl CodeSource {    pub fn default_directory(&self) -> Option<&PathBuf> {        match self {            Self::New {                default_directory, ..            } => default_directory.as_ref(),            Self::Link { .. }            | Self::AIAction { .. }            | Self::ProjectRules { .. }            | Self::FileTree { .. }            | Self::CommandPalette { .. }            | Self::Finder { .. }            | Self::Skill { .. } => None,        }    }     pub fn path(&self) -> Option<PathBuf> {        match self {            Self::New { .. } | Self::AIAction { .. } => None,            Self::FileTree { location, .. } | Self::CommandPalette { location, .. } => {                match location {                    LocalOrRemotePath::Local(path) => Some(path.clone()),                    LocalOrRemotePath::Remote(_) => None,                }            }            Self::Link { path, .. } | Self::Finder { path } => Some(path.clone()),            Self::ProjectRules { location } | Self::Skill { location, .. } => {                location.to_local_path().map(Path::to_path_buf)            }        }    }     /// Returns the `LocalOrRemotePath` for file tree sources.    pub fn file_location(&self) -> Option<&LocalOrRemotePath> {        match self {            Self::FileTree { location } | Self::CommandPalette { location } => Some(location),            _ => None,        }    }     /// Returns the `LocalOrRemotePath` for any source that has a backing file.    ///    /// Unlike `path()` (which only returns local paths) and `file_location()`    /// (which only covers `FileTree`), this covers every variant that maps to    /// a file — local or remote.    pub fn location(&self) -> Option<LocalOrRemotePath> {        match self {            Self::New { .. } | Self::AIAction { .. } => None,            Self::FileTree { location } | Self::CommandPalette { location } => {                Some(location.clone())            }            Self::Link { path, .. } | Self::Finder { path } => {                Some(LocalOrRemotePath::Local(path.clone()))            }            Self::ProjectRules { location } | Self::Skill { location, .. } => {                Some(location.clone())            }        }    }     /// Returns true if this is a bundled skill that should be read-only.    pub fn is_bundled_skill(&self) -> bool {        matches!(            self,            Self::Skill {                reference: SkillReference::BundledSkillId(_),                ..            }        )    }     pub fn omit_line_col(&self) -> CodeSource {        if let CodeSource::Link { path, .. } = self {            CodeSource::Link {                path: path.clone(),                range_start: None,                range_end: None,            }        } else {            self.clone()        }    }     /// Returns the variant name as a string for telemetry purposes.    pub fn telemetry_source_name(&self) -> &'static str {        match self {            Self::New { .. } => "new",            Self::Link { .. } => "link",            Self::AIAction { .. } => "ai_action",            Self::ProjectRules { .. } => "project_rules",            Self::FileTree {                location: LocalOrRemotePath::Remote(_),            } => "remote_file_tree",            Self::FileTree { .. } => "file_tree",            Self::CommandPalette {                location: LocalOrRemotePath::Remote(_),            } => "remote_command_palette",            Self::CommandPalette { .. } => "command_palette",            Self::Finder { .. } => "finder",            Self::Skill { .. } => "skill",        }    }     /// Returns `true` if this source should be restored across app restarts.    ///    /// `AIAction` is ephemeral (tied to a live conversation) and should not    /// be restored.    pub fn is_restorable(&self) -> bool {        !matches!(            self,            Self::AIAction { .. }                | Self::FileTree {                    location: LocalOrRemotePath::Remote(_),                }                | Self::CommandPalette {                    location: LocalOrRemotePath::Remote(_),                }                | Self::ProjectRules {                    location: LocalOrRemotePath::Remote(_),                }                | Self::Skill {                    location: LocalOrRemotePath::Remote(_),                    ..                }        )    }} struct CodePaneData {    #[allow(unused)]    window_id: WindowId,    #[allow(unused)]    locator: PaneViewLocator,} // Allow dead_code here for wasm compilation#[allow(dead_code)]pub enum CodeManagerEvent {    EditCompleted { action_id: AIAgentActionId },} /// Singleton model for managing the state of open code panes. It is responsible for/// 1) Allow caller to find an open code pane if exists./// 2) Allow other sources to listen for events emitted when code pane is closed.#[derive(Default)]pub struct CodeManager {    source_to_pane_data: HashMap<CodeSource, CodePaneData>,} impl CodeManager {    /// Register a new pane in the code manager.    pub fn register_pane(        &mut self,        pane_group_id: EntityId,        window_id: WindowId,        pane_id: PaneId,        source: CodeSource,    ) {        let entry = self.source_to_pane_data.entry(source.omit_line_col());        if let Entry::Vacant(entry) = entry {            entry.insert(CodePaneData {                window_id,                locator: PaneViewLocator {                    pane_group_id,                    pane_id,                },            });        } else {            log::warn!("Ignoring duplicate code pane registration");        }    }     /// De-register an open code pane when it's removed from a pane group.    pub fn deregister_pane(&mut self, source: &CodeSource) {        self.source_to_pane_data.remove(&source.omit_line_col());    }     /// Returns the locator for a code pane that already has the given `LocalOrRemotePath`    /// open in the given pane group. Works for both local and remote files.    pub fn get_locator_for_location_in_tab(        &self,        pane_group_id: EntityId,        location: &LocalOrRemotePath,    ) -> Option<PaneViewLocator> {        self.source_to_pane_data            .iter()            .find(|(source, data)| {                data.locator.pane_group_id == pane_group_id                    && source.location().as_ref() == Some(location)            })            .map(|(_, data)| data.locator)    }     // Allow dead_code here for wasm compilation    #[allow(dead_code)]    pub fn complete_pending_diffs(&mut self, source: CodeSource, ctx: &mut ModelContext<Self>) {        if !self.source_to_pane_data.contains_key(&source) {            log::warn!("Trying to complete an edit on a source that doesn't exist");        }         let CodeSource::AIAction { id } = source else {            return;        };         ctx.emit(CodeManagerEvent::EditCompleted { action_id: id })    }} impl Entity for CodeManager {    type Event = CodeManagerEvent;} impl SingletonEntity for CodeManager {}