diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index ed7c4ee073..bab8d72f57 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -756,6 +756,10 @@ impl ActiveThread { this } + pub fn context_store(&self) -> &Entity { + &self.context_store + } + pub fn thread(&self) -> &Entity { &self.thread } @@ -3145,28 +3149,21 @@ pub(crate) fn open_context( .start .to_point(&snapshot); - let open_task = workspace.update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) - }); - window - .spawn(cx, async move |cx| { - if let Some(active_editor) = open_task - .await - .log_err() - .and_then(|item| item.downcast::()) - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - target_position, - window, - cx, - ); - }) - .log_err(); - } - }) + open_editor_at_position(project_path, target_position, &workspace, window, cx) + .detach(); + } + } + AssistantContext::Excerpt(excerpt_context) => { + if let Some(project_path) = excerpt_context + .context_buffer + .buffer + .read(cx) + .project_path(cx) + { + let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot(); + let target_position = excerpt_context.range.start.to_point(&snapshot); + + open_editor_at_position(project_path, target_position, &workspace, window, cx) .detach(); } } @@ -3187,3 +3184,29 @@ pub(crate) fn open_context( } } } + +fn open_editor_at_position( + project_path: project::ProjectPath, + target_position: Point, + workspace: &Entity, + window: &mut Window, + cx: &mut App, +) -> Task<()> { + let open_task = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }); + window.spawn(cx, async move |cx| { + if let Some(active_editor) = open_task + .await + .log_err() + .and_then(|item| item.downcast::()) + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point(target_position, window, cx); + }) + .log_err(); + } + }) +} diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index 6b7ac9e849..f6b0915072 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -1,3 +1,4 @@ +use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -12,7 +13,7 @@ use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use client::zed_urls; -use editor::{Editor, EditorEvent, MultiBuffer}; +use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity, @@ -112,7 +113,9 @@ enum ActiveView { change_title_editor: Entity, _subscriptions: Vec, }, - PromptEditor, + PromptEditor { + context_editor: Entity, + }, History, Configuration, } @@ -184,7 +187,6 @@ pub struct AssistantPanel { message_editor: Entity, _active_thread_subscriptions: Vec, context_store: Entity, - context_editor: Option>, configuration: Option>, configuration_subscription: Option, local_timezone: UtcOffset, @@ -316,7 +318,6 @@ impl AssistantPanel { message_editor_subscription, ], context_store, - context_editor: None, configuration: None, configuration_subscription: None, local_timezone: UtcOffset::from_whole_seconds( @@ -453,8 +454,6 @@ impl AssistantPanel { } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { - self.set_active_view(ActiveView::PromptEditor, window, cx); - let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -462,7 +461,7 @@ impl AssistantPanel { .log_err() .flatten(); - self.context_editor = Some(cx.new(|cx| { + let context_editor = cx.new(|cx| { let mut editor = ContextEditor::for_context( context, self.fs.clone(), @@ -474,11 +473,16 @@ impl AssistantPanel { ); editor.insert_default_prompt(window, cx); editor - })); + }); - if let Some(context_editor) = self.context_editor.as_ref() { - context_editor.focus_handle(cx).focus(window); - } + self.set_active_view( + ActiveView::PromptEditor { + context_editor: context_editor.clone(), + }, + window, + cx, + ); + context_editor.focus_handle(cx).focus(window); } fn deploy_prompt_library( @@ -545,8 +549,13 @@ impl AssistantPanel { cx, ) }); - this.set_active_view(ActiveView::PromptEditor, window, cx); - this.context_editor = Some(editor); + this.set_active_view( + ActiveView::PromptEditor { + context_editor: editor, + }, + window, + cx, + ); anyhow::Ok(()) })??; @@ -777,8 +786,15 @@ impl AssistantPanel { .update(cx, |this, cx| this.delete_thread(thread_id, cx)) } + pub(crate) fn has_active_thread(&self) -> bool { + matches!(self.active_view, ActiveView::Thread { .. }) + } + pub(crate) fn active_context_editor(&self) -> Option> { - self.context_editor.clone() + match &self.active_view { + ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()), + _ => None, + } } pub(crate) fn delete_context( @@ -816,16 +832,10 @@ impl AssistantPanel { impl Focusable for AssistantPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.active_view { + match &self.active_view { ActiveView::Thread { .. } => self.message_editor.focus_handle(cx), ActiveView::History => self.history.focus_handle(cx), - ActiveView::PromptEditor => { - if let Some(context_editor) = self.context_editor.as_ref() { - context_editor.focus_handle(cx) - } else { - cx.focus_handle() - } - } + ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -949,15 +959,8 @@ impl AssistantPanel { .into_any_element() } } - ActiveView::PromptEditor => { - let title = self - .context_editor - .as_ref() - .map(|context_editor| { - SharedString::from(context_editor.read(cx).title(cx).to_string()) - }) - .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER)); - + ActiveView::PromptEditor { context_editor } => { + let title = SharedString::from(context_editor.read(cx).title(cx).to_string()); Label::new(title).ml_2().truncate().into_any_element() } ActiveView::History => Label::new("History").truncate().into_any_element(), @@ -984,7 +987,7 @@ impl AssistantPanel { let show_token_count = match &self.active_view { ActiveView::Thread { .. } => !is_empty, - ActiveView::PromptEditor => self.context_editor.is_some(), + ActiveView::PromptEditor { .. } => true, _ => false, }; @@ -1156,7 +1159,7 @@ impl AssistantPanel { let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count(); - match self.active_view { + match &self.active_view { ActiveView::Thread { .. } => { if total_token_usage.total == 0 { return None; @@ -1229,9 +1232,8 @@ impl AssistantPanel { Some(token_count) } - ActiveView::PromptEditor => { - let editor = self.context_editor.as_ref()?; - let element = render_remaining_tokens(editor, cx)?; + ActiveView::PromptEditor { context_editor } => { + let element = render_remaining_tokens(context_editor, cx)?; Some(element.into_any_element()) } @@ -1769,7 +1771,7 @@ impl AssistantPanel { fn key_context(&self) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("AgentPanel"); - if matches!(self.active_view, ActiveView::PromptEditor) { + if matches!(self.active_view, ActiveView::PromptEditor { .. }) { key_context.add("prompt_editor"); } key_context @@ -1797,13 +1799,13 @@ impl Render for AssistantPanel { .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::go_back)) .child(self.render_toolbar(window, cx)) - .map(|parent| match self.active_view { + .map(|parent| match &self.active_view { ActiveView::Thread { .. } => parent .child(self.render_active_thread_or_empty_state(window, cx)) .child(h_flex().child(self.message_editor.clone())) .children(self.render_last_error(cx)), ActiveView::History => parent.child(self.history.clone()), - ActiveView::PromptEditor => parent.children(self.context_editor.clone()), + ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()), ActiveView::Configuration => parent.children(self.configuration.clone()), }) } @@ -1868,7 +1870,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { cx: &mut Context, ) -> Option> { let panel = workspace.panel::(cx)?; - panel.update(cx, |panel, _cx| panel.context_editor.clone()) + panel.read(cx).active_context_editor() } fn open_saved_context( @@ -1900,7 +1902,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { fn quote_selection( &self, workspace: &mut Workspace, - creases: Vec<(String, String)>, + selection_ranges: Vec>, + buffer: Entity, window: &mut Window, cx: &mut Context, ) { @@ -1916,9 +1919,40 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(context) = panel.active_context_editor() { - context.update(cx, |context, cx| context.quote_creases(creases, window, cx)); - }; + if panel.has_active_thread() { + panel.thread.update(cx, |thread, cx| { + thread.context_store().update(cx, |store, cx| { + let buffer = buffer.read(cx); + let selection_ranges = selection_ranges + .into_iter() + .flat_map(|range| { + let (start_buffer, start) = + buffer.text_anchor_for_position(range.start, cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(range.end, cx)?; + if start_buffer != end_buffer { + return None; + } + Some((start_buffer, start..end)) + }) + .collect::>(); + + for (buffer, range) in selection_ranges { + store.add_excerpt(range, buffer, cx).detach_and_log_err(cx); + } + }) + }) + } else if let Some(context_editor) = panel.active_context_editor() { + let snapshot = buffer.read(cx).snapshot(cx); + let selection_ranges = selection_ranges + .into_iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(); + + context_editor.update(cx, |context_editor, cx| { + context_editor.quote_ranges(selection_ranges, snapshot, window, cx) + }); + } }); }); } diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index c2ad806145..0770c5b4d7 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -4,6 +4,7 @@ use gpui::{App, Entity, SharedString}; use language::{Buffer, File}; use language_model::LanguageModelRequestMessage; use project::{ProjectPath, Worktree}; +use rope::Point; use serde::{Deserialize, Serialize}; use text::{Anchor, BufferId}; use ui::IconName; @@ -23,6 +24,7 @@ pub enum ContextKind { File, Directory, Symbol, + Excerpt, FetchedUrl, Thread, } @@ -33,6 +35,7 @@ impl ContextKind { ContextKind::File => IconName::File, ContextKind::Directory => IconName::Folder, ContextKind::Symbol => IconName::Code, + ContextKind::Excerpt => IconName::Code, ContextKind::FetchedUrl => IconName::Globe, ContextKind::Thread => IconName::MessageBubbles, } @@ -46,6 +49,7 @@ pub enum AssistantContext { Symbol(SymbolContext), FetchedUrl(FetchedUrlContext), Thread(ThreadContext), + Excerpt(ExcerptContext), } impl AssistantContext { @@ -56,6 +60,7 @@ impl AssistantContext { Self::Symbol(symbol) => symbol.id, Self::FetchedUrl(url) => url.id, Self::Thread(thread) => thread.id, + Self::Excerpt(excerpt) => excerpt.id, } } } @@ -155,6 +160,14 @@ pub struct ContextSymbolId { pub range: Range, } +#[derive(Debug, Clone)] +pub struct ExcerptContext { + pub id: ContextId, + pub range: Range, + pub line_range: Range, + pub context_buffer: ContextBuffer, +} + /// Formats a collection of contexts into a string representation pub fn format_context_as_string<'a>( contexts: impl Iterator, @@ -163,6 +176,7 @@ pub fn format_context_as_string<'a>( let mut file_context = Vec::new(); let mut directory_context = Vec::new(); let mut symbol_context = Vec::new(); + let mut excerpt_context = Vec::new(); let mut fetch_context = Vec::new(); let mut thread_context = Vec::new(); @@ -171,6 +185,7 @@ pub fn format_context_as_string<'a>( AssistantContext::File(context) => file_context.push(context), AssistantContext::Directory(context) => directory_context.push(context), AssistantContext::Symbol(context) => symbol_context.push(context), + AssistantContext::Excerpt(context) => excerpt_context.push(context), AssistantContext::FetchedUrl(context) => fetch_context.push(context), AssistantContext::Thread(context) => thread_context.push(context), } @@ -179,6 +194,7 @@ pub fn format_context_as_string<'a>( if file_context.is_empty() && directory_context.is_empty() && symbol_context.is_empty() + && excerpt_context.is_empty() && fetch_context.is_empty() && thread_context.is_empty() { @@ -216,6 +232,15 @@ pub fn format_context_as_string<'a>( result.push_str("\n"); } + if !excerpt_context.is_empty() { + result.push_str("\n"); + for context in excerpt_context { + result.push_str(&context.context_buffer.text); + result.push('\n'); + } + result.push_str("\n"); + } + if !fetch_context.is_empty() { result.push_str("\n"); for context in &fetch_context { diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index fcea841685..a44d0caedf 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future}; use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity}; use language::{Buffer, File}; use project::{Project, ProjectItem, ProjectPath, Worktree}; -use rope::Rope; +use rope::{Point, Rope}; use text::{Anchor, BufferId, OffsetRangeExt}; use util::{ResultExt as _, maybe}; use crate::ThreadStore; use crate::context::{ AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext, - FetchedUrlContext, FileContext, SymbolContext, ThreadContext, + ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext, }; use crate::context_strip::SuggestedContext; use crate::thread::{Thread, ThreadId}; @@ -110,7 +110,7 @@ impl ContextStore { } let (buffer_info, text_task) = - this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??; + this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??; let text = text_task.await; @@ -129,7 +129,7 @@ impl ContextStore { ) -> Task> { cx.spawn(async move |this, cx| { let (buffer_info, text_task) = - this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??; + this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??; let text = text_task.await; @@ -206,7 +206,7 @@ impl ContextStore { // Skip all binary files and other non-UTF8 files for buffer in buffers.into_iter().flatten() { if let Some((buffer_info, text_task)) = - collect_buffer_info_and_text(buffer, None, cx).log_err() + collect_buffer_info_and_text(buffer, cx).log_err() { buffer_infos.push(buffer_info); text_tasks.push(text_task); @@ -290,11 +290,14 @@ impl ContextStore { } } - let (buffer_info, collect_content_task) = - match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) { - Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task), - Err(err) => return Task::ready(Err(err)), - }; + let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range( + buffer, + symbol_enclosing_range.clone(), + cx, + ) { + Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task), + Err(err) => return Task::ready(Err(err)), + }; cx.spawn(async move |this, cx| { let content = collect_content_task.await; @@ -416,6 +419,49 @@ impl ContextStore { cx.notify(); } + pub fn add_excerpt( + &mut self, + range: Range, + buffer: Entity, + cx: &mut Context, + ) -> Task> { + cx.spawn(async move |this, cx| { + let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| { + collect_buffer_info_and_text_for_range(buffer, range.clone(), cx) + })??; + + let text = text_task.await; + + this.update(cx, |this, cx| { + this.insert_excerpt( + make_context_buffer(buffer_info, text), + range, + line_range, + cx, + ) + })?; + + anyhow::Ok(()) + }) + } + + fn insert_excerpt( + &mut self, + context_buffer: ContextBuffer, + range: Range, + line_range: Range, + cx: &mut Context, + ) { + let id = self.next_context_id.post_inc(); + self.context.push(AssistantContext::Excerpt(ExcerptContext { + id, + range, + line_range, + context_buffer, + })); + cx.notify(); + } + pub fn accept_suggested_context( &mut self, suggested: &SuggestedContext, @@ -465,6 +511,7 @@ impl ContextStore { self.symbol_buffers.remove(&symbol.context_symbol.id); self.symbols.retain(|_, context_id| *context_id != id); } + AssistantContext::Excerpt(_) => {} AssistantContext::FetchedUrl(_) => { self.fetched_urls.retain(|_, context_id| *context_id != id); } @@ -592,6 +639,7 @@ impl ContextStore { } AssistantContext::Directory(_) | AssistantContext::Symbol(_) + | AssistantContext::Excerpt(_) | AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => None, }) @@ -643,41 +691,78 @@ fn make_context_symbol( } } +fn collect_buffer_info_and_text_for_range( + buffer: Entity, + range: Range, + cx: &App, +) -> Result<(Range, BufferInfo, Task)> { + let content = buffer + .read(cx) + .text_for_range(range.clone()) + .collect::(); + + let line_range = range.to_point(&buffer.read(cx).snapshot()); + + let buffer_info = collect_buffer_info(buffer, cx)?; + let full_path = buffer_info.file.full_path(cx); + + let text_task = cx.background_spawn({ + let line_range = line_range.clone(); + async move { to_fenced_codeblock(&full_path, content, Some(line_range)) } + }); + + Ok((line_range, buffer_info, text_task)) +} + fn collect_buffer_info_and_text( buffer: Entity, - range: Option>, cx: &App, ) -> Result<(BufferInfo, Task)> { + let content = buffer.read(cx).as_rope().clone(); + + let buffer_info = collect_buffer_info(buffer, cx)?; + let full_path = buffer_info.file.full_path(cx); + + let text_task = + cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) }); + + Ok((buffer_info, text_task)) +} + +fn collect_buffer_info(buffer: Entity, cx: &App) -> Result { let buffer_ref = buffer.read(cx); let file = buffer_ref.file().context("file context must have a path")?; // Important to collect version at the same time as content so that staleness logic is correct. let version = buffer_ref.version(); - let content = if let Some(range) = range { - buffer_ref.text_for_range(range).collect::() - } else { - buffer_ref.as_rope().clone() - }; - let buffer_info = BufferInfo { + Ok(BufferInfo { buffer, id: buffer_ref.remote_id(), file: file.clone(), version, - }; - - let full_path = file.full_path(cx); - let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) }); - - Ok((buffer_info, text_task)) + }) } -fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString { +fn to_fenced_codeblock( + path: &Path, + content: Rope, + line_range: Option>, +) -> SharedString { + let line_range_text = line_range.map(|range| { + if range.start.row == range.end.row { + format!(":{}", range.start.row + 1) + } else { + format!(":{}-{}", range.start.row + 1, range.end.row + 1) + } + }); + let path_extension = path.extension().and_then(|ext| ext.to_str()); let path_string = path.to_string_lossy(); let capacity = 3 + path_extension.map_or(0, |extension| extension.len() + 1) + path_string.len() + + line_range_text.as_ref().map_or(0, |text| text.len()) + 1 + content.len() + 5; @@ -691,6 +776,10 @@ fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString { } buffer.push_str(&path_string); + if let Some(line_range_text) = line_range_text { + buffer.push_str(&line_range_text); + } + buffer.push('\n'); for chunk in content.chunks() { buffer.push_str(&chunk); @@ -769,6 +858,14 @@ pub fn refresh_context_store_text( return refresh_symbol_text(context_store, symbol_context, cx); } } + AssistantContext::Excerpt(excerpt_context) => { + if changed_buffers.is_empty() + || changed_buffers.contains(&excerpt_context.context_buffer.buffer) + { + let context_store = context_store.clone(); + return refresh_excerpt_text(context_store, excerpt_context, cx); + } + } AssistantContext::Thread(thread_context) => { if changed_buffers.is_empty() { let context_store = context_store.clone(); @@ -880,6 +977,34 @@ fn refresh_symbol_text( } } +fn refresh_excerpt_text( + context_store: Entity, + excerpt_context: &ExcerptContext, + cx: &App, +) -> Option> { + let id = excerpt_context.id; + let range = excerpt_context.range.clone(); + let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx); + if let Some(task) = task { + Some(cx.spawn(async move |cx| { + let (line_range, context_buffer) = task.await; + context_store + .update(cx, |context_store, _| { + let new_excerpt_context = ExcerptContext { + id, + range, + line_range, + context_buffer, + }; + context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context)); + }) + .ok(); + })) + } else { + None + } +} + fn refresh_thread_text( context_store: Entity, thread_context: &ThreadContext, @@ -908,13 +1033,29 @@ fn refresh_context_buffer( let buffer = context_buffer.buffer.read(cx); if buffer.version.changed_since(&context_buffer.version) { let (buffer_info, text_task) = - collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?; + collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?; Some(text_task.map(move |text| make_context_buffer(buffer_info, text))) } else { None } } +fn refresh_context_excerpt( + context_buffer: &ContextBuffer, + range: Range, + cx: &App, +) -> Option, ContextBuffer)> + use<>> { + let buffer = context_buffer.buffer.read(cx); + if buffer.version.changed_since(&context_buffer.version) { + let (line_range, buffer_info, text_task) = + collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx) + .log_err()?; + Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text)))) + } else { + None + } +} + fn refresh_context_symbol( context_symbol: &ContextSymbol, cx: &App, @@ -922,9 +1063,9 @@ fn refresh_context_symbol( let buffer = context_symbol.buffer.read(cx); let project_path = buffer.project_path(cx)?; if buffer.version.changed_since(&context_symbol.buffer_version) { - let (buffer_info, text_task) = collect_buffer_info_and_text( + let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range( context_symbol.buffer.clone(), - Some(context_symbol.enclosing_range.clone()), + context_symbol.enclosing_range.clone(), cx, ) .log_err()?; diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index a0d6f99ea0..62c43b877e 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -725,6 +725,12 @@ impl Thread { cx, ); } + AssistantContext::Excerpt(excerpt_context) => { + log.buffer_added_as_context( + excerpt_context.context_buffer.buffer.clone(), + cx, + ); + } AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {} } } diff --git a/crates/agent/src/ui/context_pill.rs b/crates/agent/src/ui/context_pill.rs index a50b54f4de..da07522745 100644 --- a/crates/agent/src/ui/context_pill.rs +++ b/crates/agent/src/ui/context_pill.rs @@ -299,6 +299,39 @@ impl AddedContext { summarizing: false, }, + AssistantContext::Excerpt(excerpt_context) => { + let full_path = excerpt_context.context_buffer.file.full_path(cx); + let mut full_path_string = full_path.to_string_lossy().into_owned(); + let mut name = full_path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| full_path_string.clone()); + + let line_range_text = format!( + " ({}-{})", + excerpt_context.line_range.start.row + 1, + excerpt_context.line_range.end.row + 1 + ); + + full_path_string.push_str(&line_range_text); + name.push_str(&line_range_text); + + let parent = full_path + .parent() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().into_owned().into()); + + AddedContext { + id: excerpt_context.id, + kind: ContextKind::File, // Use File icon for excerpts + name: name.into(), + parent, + tooltip: Some(full_path_string.into()), + icon_path: FileIcons::get_icon(&full_path, cx), + summarizing: false, + } + } + AssistantContext::FetchedUrl(fetched_url_context) => AddedContext { id: fetched_url_context.id, kind: ContextKind::FetchedUrl, diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 085b788e8e..317780e727 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -13,7 +13,7 @@ use assistant_context_editor::{ use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_slash_command::SlashCommandWorkingSet; use client::{Client, Status, proto}; -use editor::{Editor, EditorEvent}; +use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use fs::Fs; use gpui::{ Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, @@ -28,9 +28,12 @@ use language_model::{ use project::Project; use prompt_library::{PromptLibrary, open_prompt_library}; use prompt_store::PromptBuilder; + use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; + +use std::ops::Range; use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*}; @@ -1413,7 +1416,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { fn quote_selection( &self, workspace: &mut Workspace, - creases: Vec<(String, String)>, + selection_ranges: Vec>, + buffer: Entity, window: &mut Window, cx: &mut Context, ) { @@ -1425,6 +1429,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { workspace.toggle_panel_focus::(window, cx); } + let snapshot = buffer.read(cx).snapshot(cx); + let selection_ranges = selection_ranges + .into_iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(); + panel.update(cx, |_, cx| { // Wait to create a new context until the workspace is no longer // being updated. @@ -1433,7 +1443,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { .active_context_editor(cx) .or_else(|| panel.new_context(window, cx)) { - context.update(cx, |context, cx| context.quote_creases(creases, window, cx)); + context.update(cx, |context, cx| { + context.quote_ranges(selection_ranges, snapshot, window, cx) + }); }; }); }); diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 723ce2acbd..080ce42221 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -8,8 +8,8 @@ use assistant_slash_commands::{ use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ - Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation, - ProposedChangesEditor, RowExt, ToOffset as _, ToPoint, + Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot, + ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint, actions::{MoveToEndOfLine, Newline, ShowCompletions}, display_map::{ BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, @@ -155,7 +155,8 @@ pub trait AssistantPanelDelegate { fn quote_selection( &self, workspace: &mut Workspace, - creases: Vec<(String, String)>, + selection_ranges: Vec>, + buffer: Entity, window: &mut Window, cx: &mut Context, ); @@ -1800,23 +1801,42 @@ impl ContextEditor { return; }; - let Some(creases) = selections_creases(workspace, cx) else { + let Some((selections, buffer)) = maybe!({ + let editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx))?; + + let buffer = editor.read(cx).buffer().clone(); + let snapshot = buffer.read(cx).snapshot(cx); + let selections = editor.update(cx, |editor, cx| { + editor + .selections + .all_adjusted(cx) + .into_iter() + .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end)) + .collect::>() + }); + Some((selections, buffer)) + }) else { return; }; - if creases.is_empty() { + if selections.is_empty() { return; } - assistant_panel_delegate.quote_selection(workspace, creases, window, cx); + assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx); } - pub fn quote_creases( + pub fn quote_ranges( &mut self, - creases: Vec<(String, String)>, + ranges: Vec>, + snapshot: MultiBufferSnapshot, window: &mut Window, cx: &mut Context, ) { + let creases = selections_creases(ranges, snapshot, cx); + self.editor.update(cx, |editor, cx| { editor.insert("\n", window, cx); for (text, crease_title) in creases { diff --git a/crates/assistant_slash_commands/src/selection_command.rs b/crates/assistant_slash_commands/src/selection_command.rs index 0d643b44ba..c5f01ee94c 100644 --- a/crates/assistant_slash_commands/src/selection_command.rs +++ b/crates/assistant_slash_commands/src/selection_command.rs @@ -3,10 +3,12 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; -use editor::Editor; +use editor::{Editor, MultiBufferSnapshot}; use futures::StreamExt; -use gpui::{App, Context, SharedString, Task, WeakEntity, Window}; +use gpui::{App, SharedString, Task, WeakEntity, Window}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; +use rope::Point; +use std::ops::Range; use std::sync::Arc; use std::sync::atomic::AtomicBool; use ui::IconName; @@ -69,7 +71,22 @@ impl SlashCommand for SelectionCommand { let mut events = vec![]; let Some(creases) = workspace - .update(cx, selections_creases) + .update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx))?; + + editor.update(cx, |editor, cx| { + let selection_ranges = editor + .selections + .all_adjusted(cx) + .iter() + .map(|selection| selection.range()) + .collect::>(); + let snapshot = editor.buffer().read(cx).snapshot(cx); + Some(selections_creases(selection_ranges, snapshot, cx)) + }) + }) .unwrap_or_else(|e| { events.push(Err(e)); None @@ -102,94 +119,82 @@ impl SlashCommand for SelectionCommand { } pub fn selections_creases( - workspace: &mut workspace::Workspace, - cx: &mut Context, -) -> Option> { - let editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx))?; - - let mut creases = vec![]; - editor.update(cx, |editor, cx| { - let selections = editor.selections.all_adjusted(cx); - let buffer = editor.buffer().read(cx).snapshot(cx); - for selection in selections { - let range = editor::ToOffset::to_offset(&selection.start, &buffer) - ..editor::ToOffset::to_offset(&selection.end, &buffer); - let selected_text = buffer.text_for_range(range.clone()).collect::(); - if selected_text.is_empty() { - continue; - } - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.code_fence_block_name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or(""); - let filename = buffer - .file_at(selection.start) - .map(|file| file.full_path(cx)); - let text = if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - let start_symbols = buffer - .symbols_containing(selection.start, None) - .map(|(_, symbols)| symbols); - let end_symbols = buffer - .symbols_containing(selection.end, None) - .map(|(_, symbols)| symbols); - - let outline_text = - if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) { - Some( - start_symbols - .into_iter() - .zip(end_symbols) - .take_while(|(a, b)| a == b) - .map(|(a, _)| a.text) - .collect::>() - .join(" > "), - ) - } else { - None - }; - - let line_comment_prefix = start_language - .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned()); - - let fence = codeblock_fence_for_path( - filename.as_deref(), - Some(selection.start.row..=selection.end.row), - ); - - if let Some((line_comment_prefix, outline_text)) = - line_comment_prefix.zip(outline_text) - { - let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n"); - format!("{fence}{breadcrumb}{selected_text}\n```") - } else { - format!("{fence}{selected_text}\n```") - } - }; - let crease_title = if let Some(path) = filename { - let start_line = selection.start.row + 1; - let end_line = selection.end.row + 1; - if start_line == end_line { - format!("{}, Line {}", path.display(), start_line) - } else { - format!("{}, Lines {} to {}", path.display(), start_line, end_line) - } - } else { - "Quoted selection".to_string() - }; - creases.push((text, crease_title)); + selection_ranges: Vec>, + snapshot: MultiBufferSnapshot, + cx: &App, +) -> Vec<(String, String)> { + let mut creases = Vec::new(); + for range in selection_ranges { + let selected_text = snapshot.text_for_range(range.clone()).collect::(); + if selected_text.is_empty() { + continue; } - }); - Some(creases) + let start_language = snapshot.language_at(range.start); + let end_language = snapshot.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.code_fence_block_name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or(""); + let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx)); + let text = if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + let start_symbols = snapshot + .symbols_containing(range.start, None) + .map(|(_, symbols)| symbols); + let end_symbols = snapshot + .symbols_containing(range.end, None) + .map(|(_, symbols)| symbols); + + let outline_text = + if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) { + Some( + start_symbols + .into_iter() + .zip(end_symbols) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.text) + .collect::>() + .join(" > "), + ) + } else { + None + }; + + let line_comment_prefix = start_language + .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned()); + + let fence = codeblock_fence_for_path( + filename.as_deref(), + Some(range.start.row..=range.end.row), + ); + + if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) + { + let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n"); + format!("{fence}{breadcrumb}{selected_text}\n```") + } else { + format!("{fence}{selected_text}\n```") + } + }; + let crease_title = if let Some(path) = filename { + let start_line = range.start.row + 1; + let end_line = range.end.row + 1; + if start_line == end_line { + format!("{}, Line {}", path.display(), start_line) + } else { + format!("{}, Lines {} to {}", path.display(), start_line, end_line) + } + } else { + "Quoted selection".to_string() + }; + creases.push((text, crease_title)); + } + creases }