diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index 6456c10660..9631fdd737 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -43,6 +43,7 @@ enum ContextPickerMode { pub(super) struct ContextPicker { mode: ContextPickerMode, workspace: WeakView, + editor: WeakView, context_store: WeakModel, thread_store: Option>, confirm_behavior: ConfirmBehavior, @@ -53,6 +54,7 @@ impl ContextPicker { workspace: WeakView, thread_store: Option>, context_store: WeakModel, + editor: WeakView, confirm_behavior: ConfirmBehavior, cx: &mut ViewContext, ) -> Self { @@ -61,6 +63,7 @@ impl ContextPicker { workspace, context_store, thread_store, + editor, confirm_behavior, } } @@ -131,6 +134,7 @@ impl ContextPicker { FileContextPicker::new( context_picker.clone(), self.workspace.clone(), + self.editor.clone(), self.context_store.clone(), self.confirm_behavior, cx, diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs index 29cab4936e..158c53370a 100644 --- a/crates/assistant2/src/context_picker/file_context_picker.rs +++ b/crates/assistant2/src/context_picker/file_context_picker.rs @@ -1,15 +1,25 @@ +use std::collections::BTreeSet; +use std::ops::Range; use std::path::Path; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use editor::actions::FoldAt; +use editor::display_map::{Crease, FoldId}; +use editor::scroll::Autoscroll; +use editor::{Anchor, Editor, FoldPlaceholder, ToPoint}; use file_icons::FileIcons; use fuzzy::PathMatch; use gpui::{ - AppContext, DismissEvent, FocusHandle, FocusableView, Stateful, Task, View, WeakModel, WeakView, + AnyElement, AppContext, DismissEvent, Empty, FocusHandle, FocusableView, Stateful, Task, View, + WeakModel, WeakView, }; +use multi_buffer::{MultiBufferPoint, MultiBufferRow}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, ProjectPath, WorktreeId}; -use ui::{prelude::*, ListItem, Tooltip}; +use rope::Point; +use text::SelectionGoal; +use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex, ListItem, Tooltip}; use util::ResultExt as _; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -24,6 +34,7 @@ impl FileContextPicker { pub fn new( context_picker: WeakView, workspace: WeakView, + editor: WeakView, context_store: WeakModel, confirm_behavior: ConfirmBehavior, cx: &mut ViewContext, @@ -31,6 +42,7 @@ impl FileContextPicker { let delegate = FileContextPickerDelegate::new( context_picker, workspace, + editor, context_store, confirm_behavior, ); @@ -55,6 +67,7 @@ impl Render for FileContextPicker { pub struct FileContextPickerDelegate { context_picker: WeakView, workspace: WeakView, + editor: WeakView, context_store: WeakModel, confirm_behavior: ConfirmBehavior, matches: Vec, @@ -65,12 +78,14 @@ impl FileContextPickerDelegate { pub fn new( context_picker: WeakView, workspace: WeakView, + editor: WeakView, context_store: WeakModel, confirm_behavior: ConfirmBehavior, ) -> Self { Self { context_picker, workspace, + editor, context_store, confirm_behavior, matches: Vec::new(), @@ -196,11 +211,100 @@ impl PickerDelegate for FileContextPickerDelegate { return; }; + let Some(file_name) = mat + .path + .file_name() + .map(|os_str| os_str.to_string_lossy().into_owned()) + else { + return; + }; + + let full_path = mat.path.display().to_string(); + let project_path = ProjectPath { worktree_id: WorktreeId::from_usize(mat.worktree_id), path: mat.path.clone(), }; + let Some(editor) = self.editor.upgrade() else { + return; + }; + + editor.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + // Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert. + { + let mut selections = editor.selections.all::(cx); + + for selection in selections.iter_mut() { + if selection.is_empty() { + let old_head = selection.head(); + let new_head = MultiBufferPoint::new( + old_head.row, + old_head.column.saturating_sub(1), + ); + selection.set_head(new_head, SelectionGoal::None); + } + } + + editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + } + + let start_anchors = { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor + .selections + .all::(cx) + .into_iter() + .map(|selection| snapshot.anchor_before(selection.start)) + .collect::>() + }; + + editor.insert(&full_path, cx); + + let end_anchors = { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor + .selections + .all::(cx) + .into_iter() + .map(|selection| snapshot.anchor_after(selection.end)) + .collect::>() + }; + + editor.insert("\n", cx); // Needed to end the fold + + let placeholder = FoldPlaceholder { + render: render_fold_icon_button(IconName::File, file_name.into()), + ..Default::default() + }; + + let render_trailer = move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any(); + + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut rows_to_fold = BTreeSet::new(); + let crease_iter = start_anchors + .into_iter() + .zip(end_anchors) + .map(|(start, end)| { + rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row)); + + Crease::inline( + start..end, + placeholder.clone(), + fold_toggle("tool-use"), + render_trailer, + ) + }); + + editor.insert_creases(crease_iter, cx); + + for buffer_row in rows_to_fold { + editor.fold_at(&FoldAt { buffer_row }, cx); + } + }); + }); + let Some(task) = self .context_store .update(cx, |context_store, cx| { @@ -334,3 +438,33 @@ pub fn render_file_context_entry( } }) } + +fn render_fold_icon_button( + icon: IconName, + label: SharedString, +) -> Arc, &mut WindowContext) -> AnyElement> { + Arc::new(move |fold_id, _fold_range, _cx| { + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ElevatedSurface) + .child(Icon::new(icon)) + .child(Label::new(label.clone()).single_line()) + .into_any_element() + }) +} + +fn fold_toggle( + name: &'static str, +) -> impl Fn( + MultiBufferRow, + bool, + Arc, + &mut WindowContext, +) -> AnyElement { + move |row, is_folded, fold, _cx| { + Disclosure::new((name, row.0 as u64), !is_folded) + .toggle_state(is_folded) + .on_click(move |_e, cx| fold(!is_folded, cx)) + .into_any_element() + } +} diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index e10be7d5eb..2f4e1c3846 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -39,6 +39,7 @@ impl ContextStrip { pub fn new( context_store: Model, workspace: WeakView, + editor: WeakView, thread_store: Option>, context_picker_menu_handle: PopoverMenuHandle, suggest_context_kind: SuggestContextKind, @@ -49,6 +50,7 @@ impl ContextStrip { workspace.clone(), thread_store.clone(), context_store.downgrade(), + editor.clone(), ConfirmBehavior::KeepOpen, cx, ) diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index 86f980d3db..52181bc8d8 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -834,6 +834,7 @@ impl PromptEditor { ContextStrip::new( context_store.clone(), workspace.clone(), + prompt_editor.downgrade(), thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, @@ -985,6 +986,7 @@ impl PromptEditor { ContextStrip::new( context_store.clone(), workspace.clone(), + prompt_editor.downgrade(), thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index bb4b0263a6..a75ab6416b 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -64,6 +64,7 @@ impl MessageEditor { workspace.clone(), Some(thread_store.clone()), context_store.downgrade(), + editor.downgrade(), ConfirmBehavior::Close, cx, ) @@ -73,6 +74,7 @@ impl MessageEditor { ContextStrip::new( context_store.clone(), workspace.clone(), + editor.downgrade(), Some(thread_store.clone()), context_picker_menu_handle.clone(), SuggestContextKind::File, diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 75e9b45467..8b3064bd30 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -8,7 +8,7 @@ pub use crate::slash_command_working_set::*; use anyhow::Result; use futures::stream::{self, BoxStream}; use futures::StreamExt; -use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; +use gpui::{AppContext, SharedString, Task, WeakView, WindowContext}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; use serde::{Deserialize, Serialize}; @@ -103,12 +103,6 @@ pub trait SlashCommand: 'static + Send + Sync { ) -> Task; } -pub type RenderFoldPlaceholder = Arc< - dyn Send - + Sync - + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, ->; - #[derive(Debug, PartialEq)] pub enum SlashCommandContent { Text {