@@ -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::<Editor>(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::<TerminalView>(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::<AgentPanel>(cx) else {
+ return;
+ };
- let has_terminal_panel_selection =
- workspace.panel::<TerminalPanel>(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::<AgentPanel>(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::<AgentPanel>(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<Subscription>,
_draft_editor_observation: Option<Subscription>,
_thread_metadata_store_subscription: Subscription,
+ last_context_source: Option<AgentContextSource>,
}
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
@@ -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<Buffer>, Range<text::Anchor>)>),
+ Terminal(Vec<String>),
+}
+
+#[derive(Clone)]
+pub(crate) enum AgentContextSource {
+ Editor(WeakEntity<Editor>),
+ TerminalView(WeakEntity<TerminalView>),
+ TerminalPanel,
+}
+
+impl AgentContextSource {
+ pub(crate) fn read_selection(
+ &self,
+ workspace: &Workspace,
+ include_current_line: bool,
+ cx: &mut App,
+ ) -> Option<AgentContextSelection> {
+ 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::<TerminalPanel>(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<Self> {
+ if let Some(agent_panel) = workspace.panel::<AgentPanel>(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::<Editor>(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::<TerminalView>(cx)
+ && terminal_view.focus_handle(cx).is_focused(window)
+ {
+ return Some(Self::TerminalView(terminal_view.downgrade()));
+ }
+ }
+
+ if let Some(panel) = workspace.panel::<TerminalPanel>(cx)
+ && panel.focus_handle(cx).contains_focused(window, cx)
+ {
+ return Some(Self::TerminalPanel);
+ }
+
+ None
+ }
+
+ pub(crate) fn from_active(workspace: &Workspace, cx: &App) -> Option<Self> {
+ if let Some(active_item) = workspace.active_item(cx) {
+ if let Some(editor) = active_item.act_as::<Editor>(cx) {
+ return Some(Self::Editor(editor.downgrade()));
+ } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(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::<TerminalPanel>(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<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
// 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<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
source_range: Range<Anchor>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
+ selection: Option<AgentContextSelection>,
) -> Option<Completion> {
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<String> = 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::<Vec<_>>();
-
- 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<usize>)> = 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<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
}
- let has_editor_selection = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| item.downcast::<Editor>())
- .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<Workspace>, cx: &App) -> Vec<String> {
- 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<TerminalView>, cx: &App) -> Option<String> {
+ terminal_view
.read(cx)
- .active_item(cx)
- .and_then(|item| item.act_as::<TerminalView>(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::<TerminalPanel>(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<Workspace>,
+fn editor_selection_ranges(
+ editor: &Entity<Editor>,
+ include_current_line: bool,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
- let Some(editor) = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(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<u32> = 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::<Vec<_>>()
+ 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<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync>;
+
+fn completion_text_for_editor_selections(
+ source_range: Range<Anchor>,
+ editor: WeakEntity<Editor>,
+ mention_set: WeakEntity<MentionSet>,
+ editor_selections: Vec<(Entity<Buffer>, Range<text::Anchor>)>,
+) -> (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::<Vec<_>>();
+
+ 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<Anchor>,
+ editor: WeakEntity<Editor>,
+ mention_set: WeakEntity<MentionSet>,
+ terminal_selections: Vec<String>,
+) -> (String, ConfirmCallback) {
+ const TERMINAL_PLACEHOLDER: &str = "terminal ";
+
+ let mut new_text = String::new();
+ let terminal_ranges: Vec<(String, std::ops::Range<usize>)> = 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());
+ });
+ }
}
@@ -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<Self>) {
+ pub(crate) fn insert_selection(
+ &self,
+ selection: AgentContextSelection,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
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| {
@@ -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<Self>) {
+ pub(crate) fn insert_selections(
+ &mut self,
+ selection: AgentContextSelection,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
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::<MessageEditorCompletionDelegate>::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();