diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7a2ee6d00c09ddae77988ac87b9343750fed3472..e60a4834ae2b063ed7a65de2450fae373937392e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -32,6 +32,7 @@ use zed_actions::{ use crate::ExpandMessageEditor; use crate::ManageProfiles; use crate::agent_connection_store::AgentConnectionStore; +use crate::completion_provider::AgentContextSource; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent}; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, @@ -67,10 +68,7 @@ use language_model::LanguageModelRegistry; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; -use settings::TerminalDockPosition; use settings::{Settings, update_settings_file}; -use terminal::terminal_settings::TerminalSettings; -use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use theme_settings::ThemeSettings; use ui::{ Button, ContextMenu, ContextMenuEntry, IconButton, PopoverMenu, PopoverMenuHandle, Tab, @@ -413,61 +411,36 @@ pub fn init(cx: &mut App) { ) .register_action( |workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| { - let active_editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)); - let has_editor_selection = active_editor.is_some_and(|editor| { - editor.update(cx, |editor, cx| { - editor.has_non_empty_selection(&editor.display_snapshot(cx)) - }) - }); - - let has_terminal_selection = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - .is_some_and(|terminal_view| { - terminal_view - .read(cx) - .terminal() - .read(cx) - .last_content - .selection_text - .as_ref() - .is_some_and(|text| !text.is_empty()) - }); + let Some(agent_panel) = workspace.panel::(cx) else { + return; + }; - let has_terminal_panel_selection = - workspace.panel::(cx).is_some_and(|panel| { - let position = match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - }; - let dock_is_open = - workspace.dock_at_position(position).read(cx).is_open(); - dock_is_open && !panel.read(cx).terminal_selections(cx).is_empty() - }); + let source = AgentContextSource::from_focused(workspace, window, cx); + let source = source.or_else(|| { + let cached = agent_panel.read(cx).last_context_source.clone()?; + cached.exists(workspace, cx).then_some(cached) + }); + let source = + source.or_else(|| AgentContextSource::from_active(workspace, cx)); - if !has_editor_selection - && !has_terminal_selection - && !has_terminal_panel_selection - { + let Some(source) = source else { return; - } + }; - let Some(panel) = workspace.panel::(cx) else { + let Some(selection) = source.read_selection(workspace, true, cx) else { return; }; - if !panel.focus_handle(cx).contains_focused(window, cx) { + if !agent_panel.focus_handle(cx).contains_focused(window, cx) { workspace.toggle_panel_focus::(window, cx); } - panel.update(cx, |_, cx| { + agent_panel.update(cx, |panel, cx| { + panel.last_context_source = Some(source); cx.defer_in(window, move |panel, window, cx| { if let Some(conversation_view) = panel.active_conversation_view() { conversation_view.update(cx, |conversation_view, cx| { - conversation_view.insert_selections(window, cx); + conversation_view.insert_selection(selection, window, cx); }); } }); @@ -707,6 +680,7 @@ pub struct AgentPanel { _base_view_observation: Option, _draft_editor_observation: Option, _thread_metadata_store_subscription: Subscription, + last_context_source: Option, } impl AgentPanel { @@ -1065,6 +1039,7 @@ impl AgentPanel { _base_view_observation: None, _draft_editor_observation: None, _thread_metadata_store_subscription, + last_context_source: None, }; // Initial sync of agent servers from extensions diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index 59a6cb4c924add6a477ccc9f1a574cc5fc1d69aa..32f98d7fc570973ac31da84ef92a4c3617d9d606 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -12,7 +12,7 @@ use anyhow::Result; use editor::{CompletionProvider, Editor, code_context_menus::COMPLETION_MENU_MAX_WIDTH}; use futures::FutureExt as _; use fuzzy::{PathMatch, StringMatch, StringMatchCandidate}; -use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, BackgroundExecutor, Entity, Focusable, SharedString, Task, WeakEntity, Window}; use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId}; use lsp::CompletionContext; use multi_buffer::ToOffset as _; @@ -24,7 +24,7 @@ use project::{ }; use prompt_store::{PromptStore, UserPromptId}; use rope::Point; -use settings::{Settings, TerminalDockPosition}; +use settings::Settings; use terminal::terminal_settings::TerminalSettings; use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use text::{Anchor, ToOffset as _, ToPoint as _}; @@ -35,11 +35,108 @@ use util::paths::PathStyle; use util::rel_path::RelPath; use util::truncate_and_remove_front; use workspace::Workspace; -use workspace::dock::DockPosition; use crate::AgentPanel; use crate::mention_set::MentionSet; +#[derive(Clone)] +pub(crate) enum AgentContextSelection { + Editor(Vec<(Entity, Range)>), + Terminal(Vec), +} + +#[derive(Clone)] +pub(crate) enum AgentContextSource { + Editor(WeakEntity), + TerminalView(WeakEntity), + TerminalPanel, +} + +impl AgentContextSource { + pub(crate) fn read_selection( + &self, + workspace: &Workspace, + include_current_line: bool, + cx: &mut App, + ) -> Option { + match self { + Self::Editor(handle) => { + let editor = handle.upgrade()?; + let ranges = editor_selection_ranges(&editor, include_current_line, cx); + (!ranges.is_empty()).then_some(AgentContextSelection::Editor(ranges)) + } + Self::TerminalView(handle) => { + let terminal_view = handle.upgrade()?; + terminal_view_selection(&terminal_view, cx) + .map(|text| AgentContextSelection::Terminal(vec![text])) + } + Self::TerminalPanel => { + let panel = workspace.panel::(cx)?; + let selections = panel.read(cx).terminal_selections(cx); + (!selections.is_empty()).then_some(AgentContextSelection::Terminal(selections)) + } + } + } + + pub(crate) fn from_focused(workspace: &Workspace, window: &Window, cx: &App) -> Option { + if let Some(agent_panel) = workspace.panel::(cx) + && agent_panel.focus_handle(cx).contains_focused(window, cx) + { + return None; + } + + if let Some(active_item) = workspace.active_item(cx) { + if let Some(editor) = active_item.act_as::(cx) { + if editor.focus_handle(cx).is_focused(window) { + return Some(Self::Editor(editor.downgrade())); + } + } else if let Some(terminal_view) = active_item.act_as::(cx) + && terminal_view.focus_handle(cx).is_focused(window) + { + return Some(Self::TerminalView(terminal_view.downgrade())); + } + } + + if let Some(panel) = workspace.panel::(cx) + && panel.focus_handle(cx).contains_focused(window, cx) + { + return Some(Self::TerminalPanel); + } + + None + } + + pub(crate) fn from_active(workspace: &Workspace, cx: &App) -> Option { + if let Some(active_item) = workspace.active_item(cx) { + if let Some(editor) = active_item.act_as::(cx) { + return Some(Self::Editor(editor.downgrade())); + } else if let Some(terminal_view) = active_item.act_as::(cx) { + return Some(Self::TerminalView(terminal_view.downgrade())); + } + } + if terminal_panel_dock_is_open(workspace, cx) { + return Some(Self::TerminalPanel); + } + None + } + + pub(crate) fn exists(&self, workspace: &Workspace, cx: &App) -> bool { + match self { + Self::Editor(handle) => handle.upgrade().is_some(), + Self::TerminalView(handle) => handle.upgrade().is_some(), + Self::TerminalPanel => terminal_panel_dock_is_open(workspace, cx), + } + } +} + +fn terminal_panel_dock_is_open(workspace: &Workspace, cx: &App) -> bool { + if workspace.panel::(cx).is_none() { + return false; + } + let position = TerminalSettings::get_global(cx).dock.into(); + workspace.dock_at_position(position).read(cx).is_open() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PromptContextEntry { Mode(PromptContextType), @@ -267,14 +364,13 @@ impl PromptCompletionProvider { // inserted confirm: Some(Arc::new(|_, _, _| true)), }), - PromptContextEntry::Action(action) => Self::completion_for_action( - action, - source_range, - editor, - mention_set, - workspace, - cx, - ), + PromptContextEntry::Action(action) => { + let selection = workspace.update(cx, |workspace, cx| { + AgentContextSource::from_active(workspace, cx)? + .read_selection(workspace, false, cx) + }); + Self::completion_for_action(action, source_range, editor, mention_set, selection) + } } } @@ -542,136 +638,27 @@ impl PromptCompletionProvider { source_range: Range, editor: WeakEntity, mention_set: WeakEntity, - workspace: &Entity, - cx: &mut App, + selection: Option, ) -> Option { let (new_text, on_action) = match action { - PromptContextAction::AddSelections => { - // Collect non-empty editor selections - let editor_selections: Vec<_> = selection_ranges(workspace, cx) - .into_iter() - .filter(|(buffer, range)| { - let snapshot = buffer.read(cx).snapshot(); - range.start.to_offset(&snapshot) != range.end.to_offset(&snapshot) - }) - .collect(); - - // Collect terminal selections from all terminal views if the terminal panel is visible - let terminal_selections: Vec = terminal_selections(workspace, cx); - - const EDITOR_PLACEHOLDER: &str = "selection "; - const TERMINAL_PLACEHOLDER: &str = "terminal "; - - let selections = editor_selections - .into_iter() - .enumerate() - .map(|(ix, (buffer, range))| { - ( - buffer, - range, - (EDITOR_PLACEHOLDER.len() * ix) - ..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1), - ) - }) - .collect::>(); - - let mut new_text: String = EDITOR_PLACEHOLDER.repeat(selections.len()); - - // Add terminal placeholders for each terminal selection - let terminal_ranges: Vec<(String, std::ops::Range)> = terminal_selections - .into_iter() - .map(|text| { - let start = new_text.len(); - new_text.push_str(TERMINAL_PLACEHOLDER); - (text, start..(new_text.len() - 1)) - }) - .collect(); - - let callback = Arc::new({ - let source_range = source_range.clone(); - move |_: CompletionIntent, window: &mut Window, cx: &mut App| { - let editor = editor.clone(); - let selections = selections.clone(); - let mention_set = mention_set.clone(); - let source_range = source_range.clone(); - let terminal_ranges = terminal_ranges.clone(); - window.defer(cx, move |window, cx| { - if let Some(editor) = editor.upgrade() { - // Insert editor selections - if !selections.is_empty() { - mention_set - .update(cx, |store, cx| { - store.confirm_mention_for_selection( - source_range.clone(), - selections, - editor.clone(), - window, - cx, - ) - }) - .ok(); - } - - // Insert terminal selections - for (terminal_text, terminal_range) in terminal_ranges { - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let Some(start) = - snapshot.anchor_in_excerpt(source_range.start) - else { - return; - }; - let offset = start.to_offset(&snapshot); - - let line_count = terminal_text.lines().count() as u32; - let mention_uri = MentionUri::TerminalSelection { line_count }; - let range = snapshot.anchor_after(offset + terminal_range.start) - ..snapshot.anchor_after(offset + terminal_range.end); - - let crease = crate::mention_set::crease_for_mention( - mention_uri.name().into(), - mention_uri.icon_path(cx), - None, - range, - editor.downgrade(), - ); - - let crease_id = editor.update(cx, |editor, cx| { - let crease_ids = - editor.insert_creases(vec![crease.clone()], cx); - editor.fold_creases(vec![crease], false, window, cx); - crease_ids.first().copied().unwrap() - }); - - mention_set - .update(cx, |mention_set, _| { - mention_set.insert_mention( - crease_id, - mention_uri.clone(), - gpui::Task::ready(Ok( - crate::mention_set::Mention::Text { - content: terminal_text, - tracked_buffers: vec![], - }, - )) - .shared(), - ); - }) - .ok(); - } - } - }); - false - } - }); - - ( - new_text, - callback - as Arc< - dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync, - >, - ) - } + PromptContextAction::AddSelections => match selection? { + AgentContextSelection::Editor(editor_selections) => { + completion_text_for_editor_selections( + source_range.clone(), + editor, + mention_set, + editor_selections, + ) + } + AgentContextSelection::Terminal(terminal_selections) => { + completion_text_for_terminal_selections( + source_range.clone(), + editor, + mention_set, + terminal_selections, + ) + } + }, }; Some(Completion { @@ -1166,19 +1153,12 @@ impl PromptCompletionProvider { entries.push(PromptContextEntry::Mode(PromptContextType::Thread)); } - let has_editor_selection = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.downcast::()) - .is_some_and(|editor| { - editor.update(cx, |editor, cx| { - editor.has_non_empty_selection(&editor.display_snapshot(cx)) - }) - }); - - let has_terminal_selection = !terminal_selections(workspace, cx).is_empty(); - - if has_editor_selection || has_terminal_selection { + let has_active_selection = workspace.update(cx, |workspace, cx| { + AgentContextSource::from_active(workspace, cx) + .and_then(|source| source.read_selection(workspace, false, cx)) + .is_some() + }); + if has_active_selection { entries.push(PromptContextEntry::Action( PromptContextAction::AddSelections, )); @@ -2168,81 +2148,219 @@ fn build_code_label_for_path( label.build() } -fn terminal_selections(workspace: &Entity, cx: &App) -> Vec { - let mut selections = Vec::new(); - - // Check if the active item is a terminal (in a panel or not) - if let Some(terminal_view) = workspace +fn terminal_view_selection(terminal_view: &Entity, cx: &App) -> Option { + terminal_view .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - if let Some(text) = terminal_view - .read(cx) - .terminal() - .read(cx) - .last_content - .selection_text - .clone() - .filter(|text| !text.is_empty()) - { - selections.push(text); - } - } - - if let Some(panel) = workspace.read(cx).panel::(cx) { - let position = match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - }; - let dock_is_open = workspace - .read(cx) - .dock_at_position(position) - .read(cx) - .is_open(); - if dock_is_open { - selections.extend(panel.read(cx).terminal_selections(cx)); - } - } - - selections + .terminal() + .read(cx) + .last_content + .selection_text + .clone() + .filter(|text| !text.is_empty()) } -fn selection_ranges( - workspace: &Entity, +fn editor_selection_ranges( + editor: &Entity, + include_current_line: bool, cx: &mut App, ) -> Vec<(Entity, Range)> { - let Some(editor) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return Vec::new(); - }; - editor.update(cx, |editor, cx| { let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx)); - let buffer = editor.buffer().clone().read(cx); - let snapshot = buffer.snapshot(cx); + let multi_buffer = editor.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); - selections - .into_iter() + let non_empty_rows: collections::HashSet = selections + .iter() .filter(|s| !s.is_empty()) - .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end)) - .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)?; + .flat_map(|s| s.start.row..=s.end.row) + .collect(); + + let mut seen_current_line_rows = collections::HashSet::default(); + let mut results = Vec::new(); + + for s in selections { + if s.is_empty() { + if !include_current_line + || non_empty_rows.contains(&s.start.row) + || !seen_current_line_rows.insert(s.start.row) + { + continue; + } + let Some((buffer, anchor)) = multi_buffer.text_anchor_for_position(s.start, cx) + else { + continue; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + let row = anchor.to_point(&buffer_snapshot).row; + let line_start = text::Point::new(row, 0); + let line_end = text::Point::new(row, buffer_snapshot.line_len(row)); + let start = buffer_snapshot.anchor_after(line_start); + let end = buffer_snapshot.anchor_before(line_end); + if start.to_offset(&buffer_snapshot) == end.to_offset(&buffer_snapshot) { + continue; + } + results.push((buffer, start..end)); + } else { + let mb_start = multi_buffer_snapshot.anchor_after(s.start); + let mb_end = multi_buffer_snapshot.anchor_before(s.end); + let Some((start_buffer, start)) = + multi_buffer.text_anchor_for_position(mb_start, cx) + else { + continue; + }; + let Some((end_buffer, end)) = multi_buffer.text_anchor_for_position(mb_end, cx) + else { + continue; + }; if start_buffer != end_buffer { - return None; + continue; } - Some((start_buffer, start..end)) - }) - .collect::>() + let buffer_snapshot = start_buffer.read(cx).snapshot(); + if start.to_offset(&buffer_snapshot) == end.to_offset(&buffer_snapshot) { + continue; + } + results.push((start_buffer, start..end)); + } + } + + results }) } +type ConfirmCallback = Arc bool + Send + Sync>; + +fn completion_text_for_editor_selections( + source_range: Range, + editor: WeakEntity, + mention_set: WeakEntity, + editor_selections: Vec<(Entity, Range)>, +) -> (String, ConfirmCallback) { + const EDITOR_PLACEHOLDER: &str = "selection "; + + let selections = editor_selections + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (EDITOR_PLACEHOLDER.len() * ix)..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::>(); + + let new_text = EDITOR_PLACEHOLDER.repeat(selections.len()); + + let callback: ConfirmCallback = Arc::new({ + move |_: CompletionIntent, window: &mut Window, cx: &mut App| { + let editor = editor.clone(); + let selections = selections.clone(); + let mention_set = mention_set.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + if let Some(editor) = editor.upgrade() + && !selections.is_empty() + { + mention_set + .update(cx, |store, cx| { + store.confirm_mention_for_selection( + source_range.clone(), + selections, + editor.clone(), + window, + cx, + ) + }) + .ok(); + } + }); + false + } + }); + + (new_text, callback) +} + +fn completion_text_for_terminal_selections( + source_range: Range, + editor: WeakEntity, + mention_set: WeakEntity, + terminal_selections: Vec, +) -> (String, ConfirmCallback) { + const TERMINAL_PLACEHOLDER: &str = "terminal "; + + let mut new_text = String::new(); + let terminal_ranges: Vec<(String, std::ops::Range)> = terminal_selections + .into_iter() + .map(|text| { + let start = new_text.len(); + new_text.push_str(TERMINAL_PLACEHOLDER); + (text, start..(new_text.len() - 1)) + }) + .collect(); + + let callback: ConfirmCallback = Arc::new({ + move |_: CompletionIntent, window: &mut Window, cx: &mut App| { + let editor = editor.clone(); + let mention_set = mention_set.clone(); + let source_range = source_range.clone(); + let terminal_ranges = terminal_ranges.clone(); + window.defer(cx, move |window, cx| { + let Some(editor) = editor.upgrade() else { + return; + }; + for (terminal_text, terminal_range) in terminal_ranges { + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let Some(start) = snapshot.anchor_in_excerpt(source_range.start) else { + return; + }; + let offset = start.to_offset(&snapshot); + + let line_count = terminal_text.lines().count() as u32; + let mention_uri = MentionUri::TerminalSelection { line_count }; + let range = snapshot.anchor_after(offset + terminal_range.start) + ..snapshot.anchor_after(offset + terminal_range.end); + + let crease = crate::mention_set::crease_for_mention( + mention_uri.name().into(), + mention_uri.icon_path(cx), + None, + range, + editor.downgrade(), + ); + + let Some(crease_id) = editor.update(cx, |editor, cx| { + let crease_ids = editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + crease_ids.first().copied() + }) else { + log::error!("insert_creases returned no ids for terminal selection"); + continue; + }; + + mention_set + .update(cx, |mention_set, _| { + mention_set.insert_mention( + crease_id, + mention_uri.clone(), + Task::ready(Ok(crate::mention_set::Mention::Text { + content: terminal_text, + tracked_buffers: vec![], + })) + .shared(), + ); + }) + .ok(); + } + }); + false + } + }); + + (new_text, callback) +} + #[cfg(test)] mod tests { use super::*; @@ -2652,4 +2770,71 @@ mod tests { "dir1/a.txt should be second" ); } + + #[gpui::test] + async fn test_source_read_selection_editor_whole_line(cx: &mut TestAppContext) { + use editor::Editor; + use project::Project; + use serde_json::json; + use text::ToOffset as _; + use util::path; + use workspace::{AppState, MultiWorkspace}; + + crate::conversation_view::tests::init_test(cx); + + let app_state = cx.update(AppState::test); + + app_state + .fs + .as_fake() + .insert_tree(path!("/root"), json!({ "a.txt": "" })) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + let buffer = cx.new(|cx| language::Buffer::local("abc\ndef\nghi", cx)); + let editor = + cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([text::Point::new(1, 1)..text::Point::new(1, 1)]); + }); + }); + + let source = AgentContextSource::Editor(editor.downgrade()); + + workspace.update(cx, |workspace, cx| { + let selection = source + .read_selection(workspace, true, cx) + .expect("editor source with cursor on a line should yield a selection"); + assert!( + matches!(selection, AgentContextSelection::Editor(_)), + "expected Editor variant" + ); + if let AgentContextSelection::Editor(ranges) = selection { + assert_eq!( + ranges.len(), + 1, + "expected exactly one range for whole-line fallback" + ); + let (range_buffer, range) = &ranges[0]; + let snapshot = range_buffer.read(cx).snapshot(); + let start_offset = range.start.to_offset(&snapshot); + let end_offset = range.end.to_offset(&snapshot); + assert_eq!( + &snapshot.text()[start_offset..end_offset], + "def", + "whole-line fallback should capture the current row" + ); + } + + // With include_current_line = false and no non-empty selection, the + // fallback is suppressed and read_selection should return None. + assert!(source.read_selection(workspace, false, cx).is_none()); + }); + } } diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 9dd97975a184eeae2feb27383af282897687bd23..00cc74a9b87db451c5af3521644294755f6a26b3 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -80,6 +80,7 @@ use crate::agent_connection_store::{ AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore, }; use crate::agent_diff::AgentDiff; +use crate::completion_provider::AgentContextSelection; use crate::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent}; use crate::profile_selector::{ProfileProvider, ProfileSelector}; @@ -2760,11 +2761,16 @@ impl ConversationView { /// Inserts the selected text into the message editor or the message being /// edited, if any. - pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { + pub(crate) fn insert_selection( + &self, + selection: AgentContextSelection, + window: &mut Window, + cx: &mut Context, + ) { if let Some(active_thread) = self.active_thread() { active_thread.update(cx, |thread, cx| { thread.active_editor(cx).update(cx, |editor, cx| { - editor.insert_selections(window, cx); + editor.insert_selections(selection, window, cx); }) }); } @@ -2974,6 +2980,7 @@ pub(crate) mod tests { use workspace::{Item, MultiWorkspace}; use crate::agent_panel; + use crate::completion_provider::AgentContextSource; use crate::thread_metadata_store::ThreadMetadataStore; use super::*; @@ -5903,7 +5910,14 @@ pub(crate) mod tests { .and_then(|active| active.read(cx).editing_message), Some(0) ); - view.insert_selections(window, cx); + let workspace = workspace.upgrade().unwrap(); + let selection = workspace + .update(cx, |workspace, cx| { + AgentContextSource::from_active(workspace, cx)? + .read_selection(workspace, false, cx) + }) + .unwrap(); + view.insert_selection(selection, window, cx); }); user_message_editor.read_with(cx, |editor, cx| { @@ -5966,7 +5980,14 @@ pub(crate) mod tests { .and_then(|active| active.read(cx).editing_message), None ); - view.insert_selections(window, cx); + let workspace = view.workspace.upgrade().unwrap(); + let selection = workspace + .update(cx, |workspace, cx| { + AgentContextSource::from_active(workspace, cx)? + .read_selection(workspace, false, cx) + }) + .unwrap(); + view.insert_selection(selection, window, cx); }); message_editor.read_with(cx, |editor, cx| { diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index c6fd040f7e91e9e60d4f45a46c001c18ee012c37..ec966f2af548992d1f76b6d75c3cf17f319a43b5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -3,8 +3,8 @@ use crate::SendImmediately; use crate::{ ChatWithFollow, completion_provider::{ - PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction, - PromptContextType, SlashCommandCompletion, + AgentContextSelection, PromptCompletionProvider, PromptCompletionProviderDelegate, + PromptContextAction, PromptContextType, SlashCommandCompletion, }, mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention}, }; @@ -1365,7 +1365,12 @@ impl MessageEditor { .detach_and_log_err(cx); } - pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context) { + pub(crate) fn insert_selections( + &mut self, + selection: AgentContextSelection, + window: &mut Window, + cx: &mut Context, + ) { let editor = self.editor.read(cx); let editor_buffer = editor.buffer().read(cx); let Some(buffer) = editor_buffer.as_singleton() else { @@ -1376,17 +1381,13 @@ impl MessageEditor { let anchor = buffer.update(cx, |buffer, _cx| { buffer.anchor_before(cursor_offset.0.min(buffer.len())) }); - let Some(workspace) = self.workspace.upgrade() else { - return; - }; let Some(completion) = PromptCompletionProvider::::completion_for_action( PromptContextAction::AddSelections, anchor..anchor, self.editor.downgrade(), self.mention_set.downgrade(), - &workspace, - cx, + Some(selection), ) else { return; @@ -2010,7 +2011,7 @@ mod tests { use util::{path, paths::PathStyle, rel_path::rel_path}; use workspace::{AppState, Item, MultiWorkspace}; - use crate::completion_provider::PromptContextType; + use crate::completion_provider::{AgentContextSelection, PromptContextType}; use crate::{ conversation_view::tests::init_test, mention_set::insert_crease_for_mention, @@ -3731,11 +3732,17 @@ mod tests { }) }); - // Now let's insert the selection in the Agent Panel's editor and - // confirm that, after the insertion, the cursor is now in the visible - // range. + let text_editor_selection = editor.update(&mut cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx); + let buffer = multibuffer.as_singleton().unwrap(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let start = buffer_snapshot.anchor_before(0); + let end = buffer_snapshot.anchor_after(5); + AgentContextSelection::Editor(vec![(buffer, start..end)]) + }); + message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.insert_selections(window, cx); + message_editor.insert_selections(text_editor_selection, window, cx); }); cx.run_until_parked(); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 4ad40b06e676166ca6af3ce914541d2a8f1641b0..34ec1eddcc8c4b1ac5d876a0140256ca1c71e081 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1539,11 +1539,7 @@ impl Focusable for TerminalPanel { impl Panel for TerminalPanel { fn position(&self, _window: &Window, cx: &App) -> DockPosition { - match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - } + TerminalSettings::get_global(cx).dock.into() } fn position_is_valid(&self, _: DockPosition) -> bool { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 1983b2921ffcc510f9376c733cb8adb8bbc473b8..461726757d73f8ab37eb2e469a91d0ee4f3c6a4b 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -13,7 +13,7 @@ use gpui::{ px, }; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, TerminalDockPosition}; use std::sync::Arc; use ui::{ ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*, @@ -301,6 +301,16 @@ impl Into for DockPosition { } } +impl From for DockPosition { + fn from(value: TerminalDockPosition) -> Self { + match value { + TerminalDockPosition::Left => DockPosition::Left, + TerminalDockPosition::Bottom => DockPosition::Bottom, + TerminalDockPosition::Right => DockPosition::Right, + } + } +} + impl DockPosition { fn label(&self) -> &'static str { match self {