@@ -22,18 +22,18 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
- Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient,
- prelude::*, pulsating_between,
+ Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
+ KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
+ linear_gradient, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage};
use language_model_selector::ToggleModelSelector;
-use project::Project;
+use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library};
-use search::{BufferSearchBar, buffer_search::DivRegistrar};
+use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
@@ -43,7 +43,7 @@ use ui::{
};
use util::{ResultExt as _, maybe};
use workspace::dock::{DockPosition, Panel, PanelEvent};
-use workspace::{CollaboratorId, ToolbarItemView, Workspace};
+use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
@@ -2570,6 +2570,108 @@ impl AssistantPanel {
.into_any()
}
+ fn render_drag_target(&self, cx: &Context<Self>) -> Div {
+ let is_local = self.project.read(cx).is_local();
+ div()
+ .invisible()
+ .absolute()
+ .top_0()
+ .right_0()
+ .bottom_0()
+ .left_0()
+ .bg(cx.theme().colors().drop_target_background)
+ .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
+ .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
+ .when(is_local, |this| {
+ this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
+ })
+ .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
+ let item = tab.pane.read(cx).item_for_index(tab.ix);
+ let project_paths = item
+ .and_then(|item| item.project_path(cx))
+ .into_iter()
+ .collect::<Vec<_>>();
+ this.handle_drop(project_paths, vec![], window, cx);
+ }))
+ .on_drop(
+ cx.listener(move |this, selection: &DraggedSelection, window, cx| {
+ let project_paths = selection
+ .items()
+ .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
+ .collect::<Vec<_>>();
+ this.handle_drop(project_paths, vec![], window, cx);
+ }),
+ )
+ .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
+ let tasks = paths
+ .paths()
+ .into_iter()
+ .map(|path| {
+ Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
+ })
+ .collect::<Vec<_>>();
+ cx.spawn_in(window, async move |this, cx| {
+ let mut paths = vec![];
+ let mut added_worktrees = vec![];
+ let opened_paths = futures::future::join_all(tasks).await;
+ for entry in opened_paths {
+ if let Some((worktree, project_path)) = entry.log_err() {
+ added_worktrees.push(worktree);
+ paths.push(project_path);
+ }
+ }
+ this.update_in(cx, |this, window, cx| {
+ this.handle_drop(paths, added_worktrees, window, cx);
+ })
+ .ok();
+ })
+ .detach();
+ }))
+ }
+
+ fn handle_drop(
+ &mut self,
+ paths: Vec<ProjectPath>,
+ added_worktrees: Vec<Entity<Worktree>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ match &self.active_view {
+ ActiveView::Thread { .. } => {
+ let context_store = self.thread.read(cx).context_store().clone();
+ context_store.update(cx, move |context_store, cx| {
+ let mut tasks = Vec::new();
+ for project_path in &paths {
+ tasks.push(context_store.add_file_from_path(
+ project_path.clone(),
+ false,
+ cx,
+ ));
+ }
+ cx.background_spawn(async move {
+ futures::future::join_all(tasks).await;
+ // Need to hold onto the worktrees until they have already been used when
+ // opening the buffers.
+ drop(added_worktrees);
+ })
+ .detach();
+ });
+ }
+ ActiveView::PromptEditor { context_editor, .. } => {
+ context_editor.update(cx, |context_editor, cx| {
+ ContextEditor::insert_dragged_files(
+ context_editor,
+ paths,
+ added_worktrees,
+ window,
+ cx,
+ );
+ });
+ }
+ ActiveView::History | ActiveView::Configuration => {}
+ }
+ }
+
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
let message = message.into();
IconButton::new("copy", IconName::Copy)
@@ -2617,18 +2719,24 @@ impl Render for AssistantPanel {
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.map(|parent| match &self.active_view {
- ActiveView::Thread { .. } => parent
- .child(self.render_active_thread_or_empty_state(window, cx))
- .children(self.render_tool_use_limit_reached(cx))
- .child(h_flex().child(self.message_editor.clone()))
- .children(self.render_last_error(cx)),
+ ActiveView::Thread { .. } => parent.child(
+ v_flex()
+ .relative()
+ .justify_between()
+ .size_full()
+ .child(self.render_active_thread_or_empty_state(window, cx))
+ .children(self.render_tool_use_limit_reached(cx))
+ .child(h_flex().child(self.message_editor.clone()))
+ .children(self.render_last_error(cx))
+ .child(self.render_drag_target(cx)),
+ ),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor {
context_editor,
buffer_search_bar,
..
} => {
- let mut registrar = DivRegistrar::new(
+ let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view {
ActiveView::PromptEditor {
buffer_search_bar, ..
@@ -2642,6 +2750,7 @@ impl Render for AssistantPanel {
registrar
.into_div()
.size_full()
+ .relative()
.map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() {
@@ -2657,7 +2766,8 @@ impl Render for AssistantPanel {
)
})
})
- .child(context_editor.clone()),
+ .child(context_editor.clone())
+ .child(self.render_drag_target(cx)),
)
}
ActiveView::Configuration => parent.children(self.configuration.clone()),
@@ -43,8 +43,8 @@ use language_model_selector::{
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
-use project::lsp_store::LocalLspAdapterDelegate;
use project::{Project, Worktree};
+use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
use rope::Point;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file};
@@ -100,7 +100,7 @@ actions!(
#[derive(PartialEq, Clone)]
pub enum InsertDraggedFiles {
- ProjectPaths(Vec<PathBuf>),
+ ProjectPaths(Vec<ProjectPath>),
ExternalFiles(Vec<PathBuf>),
}
@@ -1725,7 +1725,7 @@ impl ContextEditor {
);
}
- pub fn insert_dragged_files(
+ pub fn handle_insert_dragged_files(
workspace: &mut Workspace,
action: &InsertDraggedFiles,
window: &mut Window,
@@ -1740,7 +1740,7 @@ impl ContextEditor {
return;
};
- let project = workspace.project().clone();
+ let project = context_editor_view.read(cx).project.clone();
let paths = match action {
InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
@@ -1751,22 +1751,17 @@ impl ContextEditor {
.map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
.collect::<Vec<_>>();
- cx.spawn(async move |_, cx| {
+ cx.background_spawn(async move {
let mut paths = vec![];
let mut worktrees = vec![];
let opened_paths = futures::future::join_all(tasks).await;
- for (worktree, project_path) in opened_paths.into_iter().flatten() {
- let Ok(worktree_root_name) =
- worktree.read_with(cx, |worktree, _| worktree.root_name().to_string())
- else {
- continue;
- };
- let mut full_path = PathBuf::from(worktree_root_name.clone());
- full_path.push(&project_path.path);
- paths.push(full_path);
- worktrees.push(worktree);
+ for entry in opened_paths {
+ if let Some((worktree, project_path)) = entry.log_err() {
+ worktrees.push(worktree);
+ paths.push(project_path);
+ }
}
(paths, worktrees)
@@ -1774,33 +1769,50 @@ impl ContextEditor {
}
};
- window
- .spawn(cx, async move |cx| {
+ context_editor_view.update(cx, |_, cx| {
+ cx.spawn_in(window, async move |this, cx| {
let (paths, dragged_file_worktrees) = paths.await;
- let cmd_name = FileSlashCommand.name();
-
- context_editor_view
- .update_in(cx, |context_editor, window, cx| {
- let file_argument = paths
- .into_iter()
- .map(|path| path.to_string_lossy().to_string())
- .collect::<Vec<_>>()
- .join(" ");
-
- context_editor.editor.update(cx, |editor, cx| {
- editor.insert("\n", window, cx);
- editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
- });
-
- context_editor.confirm_command(&ConfirmCommand, window, cx);
-
- context_editor
- .dragged_file_worktrees
- .extend(dragged_file_worktrees);
- })
- .log_err();
+ this.update_in(cx, |this, window, cx| {
+ this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
+ })
+ .ok();
})
.detach();
+ })
+ }
+
+ pub fn insert_dragged_files(
+ &mut self,
+ opened_paths: Vec<ProjectPath>,
+ added_worktrees: Vec<Entity<Worktree>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mut file_slash_command_args = vec![];
+ for project_path in opened_paths.into_iter() {
+ let Some(worktree) = self
+ .project
+ .read(cx)
+ .worktree_for_id(project_path.worktree_id, cx)
+ else {
+ continue;
+ };
+ let worktree_root_name = worktree.read(cx).root_name().to_string();
+ let mut full_path = PathBuf::from(worktree_root_name.clone());
+ full_path.push(&project_path.path);
+ file_slash_command_args.push(full_path.to_string_lossy().to_string());
+ }
+
+ let cmd_name = FileSlashCommand.name();
+
+ let file_argument = file_slash_command_args.join(" ");
+
+ self.editor.update(cx, |editor, cx| {
+ editor.insert("\n", window, cx);
+ editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
+ });
+ self.confirm_command(&ConfirmCommand, window, cx);
+ self.dragged_file_worktrees.extend(added_worktrees);
}
pub fn quote_selection(