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 + '_ { 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 + '_ { app.views_of_type::(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, app: &'a AppContext, ) -> impl Iterator + 'a + use<'a> { tab.as_ref(app) .code_panes(app) .map(move |(_, editor)| Self::editor_status(&editor, app)) } pub fn editor_status(editor: &ViewHandle, 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, 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 + '_ { app.views_of_type::(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, }, /// Opened from file links. Link { path: PathBuf, range_start: Option, range_end: Option, }, /// 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 { 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 { 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, } 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 { 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) { 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 {}