diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index f93af157c1..245a4801e6 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -30,7 +30,7 @@ use language_model::{ }; use markdown::parser::{CodeBlockKind, CodeBlockMetadata}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown}; -use project::ProjectItem as _; +use project::{ProjectEntryId, ProjectItem as _}; use rope::Point; use settings::{Settings as _, update_settings_file}; use std::path::Path; @@ -44,7 +44,7 @@ use ui::{ }; use util::ResultExt as _; use util::markdown::MarkdownString; -use workspace::{OpenOptions, Workspace}; +use workspace::Workspace; use zed_actions::assistant::OpenRulesLibrary; pub struct ActiveThread { @@ -3039,21 +3039,30 @@ impl ActiveThread { return; }; - let abs_paths = project_context + let project_entry_ids = project_context .worktrees .iter() .flat_map(|worktree| worktree.rules_file.as_ref()) - .map(|rules_file| rules_file.abs_path.to_path_buf()) + .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) .collect::>(); - if let Ok(task) = self.workspace.update(cx, move |workspace, cx| { - // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules - // files clear. For example, if rules file 1 is already open but rules file 2 is not, - // this would open and focus rules file 2 in a tab that is not next to rules file 1. - workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx) - }) { - task.detach(); - } + self.workspace + .update(cx, move |workspace, cx| { + // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules + // files clear. For example, if rules file 1 is already open but rules file 2 is not, + // this would open and focus rules file 2 in a tab that is not next to rules file 1. + let project = workspace.project().read(cx); + let project_paths = project_entry_ids + .into_iter() + .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) + .collect::>(); + for project_path in project_paths { + workspace + .open_path(project_path, None, true, window, cx) + .detach_and_log_err(cx); + } + }) + .ok(); } fn dismiss_notifications(&mut self, cx: &mut Context) { diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 6f7ee0b7c9..9b42737be6 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -11,7 +11,6 @@ use chrono::{DateTime, Utc}; use collections::HashMap; use context_server::manager::ContextServerManager; use context_server::{ContextServerFactoryRegistry, ContextServerTool}; -use fs::Fs; use futures::channel::{mpsc, oneshot}; use futures::future::{self, BoxFuture, Shared}; use futures::{FutureExt as _, StreamExt as _}; @@ -22,7 +21,7 @@ use gpui::{ use heed::Database; use heed::types::SerdeBincode; use language_model::{LanguageModelToolUseId, Role, TokenUsage}; -use project::{Project, Worktree}; +use project::{Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{ ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext, UserRulesContext, WorktreeContext, @@ -207,15 +206,15 @@ impl ThreadStore { prompt_store: Option>, cx: &mut Context, ) -> Task<()> { - let project = self.project.read(cx); - let worktree_tasks = project + let worktrees = self + .project + .read(cx) .visible_worktrees(cx) + .collect::>(); + let worktree_tasks = worktrees + .into_iter() .map(|worktree| { - Self::load_worktree_info_for_system_prompt( - project.fs().clone(), - worktree.read(cx), - cx, - ) + Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx) }) .collect::>(); let default_user_rules_task = match prompt_store { @@ -276,13 +275,13 @@ impl ThreadStore { } fn load_worktree_info_for_system_prompt( - fs: Arc, - worktree: &Worktree, - cx: &App, + worktree: Entity, + project: Entity, + cx: &mut App, ) -> Task<(WorktreeContext, Option)> { - let root_name = worktree.root_name().into(); + let root_name = worktree.read(cx).root_name().into(); - let rules_task = Self::load_worktree_rules_file(fs, worktree, cx); + let rules_task = Self::load_worktree_rules_file(worktree, project, cx); let Some(rules_task) = rules_task else { return Task::ready(( WorktreeContext { @@ -312,33 +311,44 @@ impl ThreadStore { } fn load_worktree_rules_file( - fs: Arc, - worktree: &Worktree, - cx: &App, + worktree: Entity, + project: Entity, + cx: &mut App, ) -> Option>> { + let worktree_ref = worktree.read(cx); + let worktree_id = worktree_ref.id(); let selected_rules_file = RULES_FILE_NAMES .into_iter() .filter_map(|name| { - worktree + worktree_ref .entry_for_path(name) .filter(|entry| entry.is_file()) - .map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path))) + .map(|entry| entry.path.clone()) }) .next(); // Note that Cline supports `.clinerules` being a directory, but that is not currently // supported. This doesn't seem to occur often in GitHub repositories. - selected_rules_file.map(|(path_in_worktree, abs_path)| { - let fs = fs.clone(); + selected_rules_file.map(|path_in_worktree| { + let project_path = ProjectPath { + worktree_id, + path: path_in_worktree.clone(), + }; + let buffer_task = + project.update(cx, |project, cx| project.open_buffer(project_path, cx)); + let rope_task = cx.spawn(async move |cx| { + buffer_task.await?.read_with(cx, |buffer, cx| { + let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; + anyhow::Ok((project_entry_id, buffer.as_rope().clone())) + })? + }); + // Build a string from the rope on a background thread. cx.background_spawn(async move { - let abs_path = abs_path?; - let text = fs.load(&abs_path).await.with_context(|| { - format!("Failed to load assistant rules file {:?}", abs_path) - })?; + let (project_entry_id, rope) = rope_task.await?; anyhow::Ok(RulesFileContext { path_in_worktree, - abs_path: abs_path.into(), - text: text.trim().to_string(), + text: rope.to_string().trim().to_string(), + project_entry_id: project_entry_id.to_usize(), }) }) }) diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 35430e8f06..0a53957e36 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -64,8 +64,11 @@ pub struct WorktreeContext { #[derive(Debug, Clone, Serialize)] pub struct RulesFileContext { pub path_in_worktree: Arc, - pub abs_path: Arc, pub text: String, + // This used for opening rules files. TODO: Since it isn't related to prompt templating, this + // should be moved elsewhere. + #[serde(skip)] + pub project_entry_id: usize, } #[derive(Serialize)] @@ -403,8 +406,8 @@ mod test { root_name: "path".into(), rules_file: Some(RulesFileContext { path_in_worktree: Path::new(".rules").into(), - abs_path: Path::new("/some/path/.rules").into(), text: "".into(), + project_entry_id: 0, }), }]; let default_user_rules = vec![UserRulesContext { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b00b1d6b63..a114f384ba 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -5549,6 +5549,10 @@ impl ProjectEntryId { self.0 as u64 } + pub fn from_usize(id: usize) -> Self { + ProjectEntryId(id) + } + pub fn to_usize(&self) -> usize { self.0 }