assistant: Allow drag&dropping files/tabs into assistant panel (#17415)
This adds ability to the assistant panel's context editor to accept files being dropped on it. Multiple things can be dropped on the assistant panel: - project panel entries (one or many) - tabs (one) - external files (one or many) Release Notes: - N/A Demo: https://github.com/user-attachments/assets/fddee751-cbdf-4e2c-ac80-35dfb857cc8a Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
7907ab32d7
commit
f2d539f762
4 changed files with 185 additions and 22 deletions
|
@ -27,8 +27,8 @@ use context_servers::ContextServerRegistry;
|
||||||
pub use context_store::*;
|
pub use context_store::*;
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::Context as _;
|
|
||||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||||
|
use gpui::{impl_actions, Context as _};
|
||||||
use indexed_docs::IndexedDocsRegistry;
|
use indexed_docs::IndexedDocsRegistry;
|
||||||
pub(crate) use inline_assistant::*;
|
pub(crate) use inline_assistant::*;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -45,6 +45,7 @@ use slash_command::{
|
||||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||||
tab_command, terminal_command, workflow_command,
|
tab_command, terminal_command, workflow_command,
|
||||||
};
|
};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
pub(crate) use streaming_diff::*;
|
pub(crate) use streaming_diff::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -70,6 +71,14 @@ actions!(
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Deserialize)]
|
||||||
|
pub enum InsertDraggedFiles {
|
||||||
|
ProjectPaths(Vec<PathBuf>),
|
||||||
|
ExternalFiles(Vec<PathBuf>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_actions!(assistant, [InsertDraggedFiles]);
|
||||||
|
|
||||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
|
|
@ -6,17 +6,17 @@ use crate::{
|
||||||
slash_command::{
|
slash_command::{
|
||||||
default_command::DefaultSlashCommand,
|
default_command::DefaultSlashCommand,
|
||||||
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
|
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
|
||||||
file_command::codeblock_fence_for_path,
|
file_command::{self, codeblock_fence_for_path},
|
||||||
SlashCommandCompletionProvider, SlashCommandRegistry,
|
SlashCommandCompletionProvider, SlashCommandRegistry,
|
||||||
},
|
},
|
||||||
slash_command_picker,
|
slash_command_picker,
|
||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
Assist, CacheStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
|
Assist, CacheStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
|
||||||
ContextStoreEvent, CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssistId,
|
ContextStoreEvent, CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssistId,
|
||||||
InlineAssistant, InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus,
|
InlineAssistant, InsertDraggedFiles, InsertIntoEditor, Message, MessageId, MessageMetadata,
|
||||||
ModelPickerDelegate, ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus,
|
MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, PendingSlashCommand,
|
||||||
QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus,
|
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split,
|
||||||
ToggleModelSelector, WorkflowStepResolution,
|
ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||||
|
@ -37,10 +37,10 @@ use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
|
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
|
||||||
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
|
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
|
||||||
Context as _, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight,
|
Context as _, Empty, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
|
||||||
InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, RenderImage,
|
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
|
||||||
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||||
UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use indexed_docs::IndexedDocsStore;
|
use indexed_docs::IndexedDocsStore;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -52,12 +52,18 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectLspAdapterDelegate};
|
use project::{Project, ProjectLspAdapterDelegate, Worktree};
|
||||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||||
use settings::{update_settings_file, Settings};
|
use settings::{update_settings_file, Settings};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow, cmp, collections::hash_map, fmt::Write, ops::Range, path::PathBuf, sync::Arc,
|
borrow::Cow,
|
||||||
|
cmp,
|
||||||
|
collections::hash_map,
|
||||||
|
fmt::Write,
|
||||||
|
ops::{ControlFlow, Range},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||||
|
@ -68,16 +74,16 @@ use ui::{
|
||||||
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::{maybe, ResultExt};
|
||||||
use workspace::searchable::SearchableItemHandle;
|
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
item::{self, FollowableItem, Item, ItemHandle},
|
item::{self, FollowableItem, Item, ItemHandle},
|
||||||
pane::{self, SaveIntent},
|
pane::{self, SaveIntent},
|
||||||
searchable::{SearchEvent, SearchableItem},
|
searchable::{SearchEvent, SearchableItem},
|
||||||
Pane, Save, ShowConfiguration, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation,
|
DraggedSelection, Pane, Save, ShowConfiguration, ToggleZoom, ToolbarItemEvent,
|
||||||
ToolbarItemView, Workspace,
|
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||||
};
|
};
|
||||||
|
use workspace::{searchable::SearchableItemHandle, DraggedTab};
|
||||||
use zed_actions::InlineAssist;
|
use zed_actions::InlineAssist;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
@ -96,6 +102,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
.register_action(AssistantPanel::inline_assist)
|
.register_action(AssistantPanel::inline_assist)
|
||||||
.register_action(ContextEditor::quote_selection)
|
.register_action(ContextEditor::quote_selection)
|
||||||
.register_action(ContextEditor::insert_selection)
|
.register_action(ContextEditor::insert_selection)
|
||||||
|
.register_action(ContextEditor::insert_dragged_files)
|
||||||
.register_action(AssistantPanel::show_configuration)
|
.register_action(AssistantPanel::show_configuration)
|
||||||
.register_action(AssistantPanel::create_new_context);
|
.register_action(AssistantPanel::create_new_context);
|
||||||
},
|
},
|
||||||
|
@ -340,6 +347,62 @@ impl AssistantPanel {
|
||||||
NewContext.boxed_clone(),
|
NewContext.boxed_clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
|
||||||
|
let action = maybe!({
|
||||||
|
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||||
|
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
|
||||||
|
{
|
||||||
|
if &tab.pane == cx.view() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let item = tab.pane.read(cx).item_for_index(tab.ix);
|
||||||
|
Some(
|
||||||
|
item.and_then(|item| item.project_path(cx))
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
} else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
|
||||||
|
{
|
||||||
|
Some(
|
||||||
|
selection
|
||||||
|
.items()
|
||||||
|
.filter_map(|item| {
|
||||||
|
project.read(cx).path_for_entry(item.entry_id, cx)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let paths = project_paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|project_path| {
|
||||||
|
let worktree = project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
|
|
||||||
|
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||||
|
full_path.push(&project_path.path);
|
||||||
|
Some(full_path)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Some(InsertDraggedFiles::ProjectPaths(paths))
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
cx.dispatch_action(action.boxed_clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFlow::Break(())
|
||||||
|
});
|
||||||
|
|
||||||
pane.set_can_split(false, cx);
|
pane.set_can_split(false, cx);
|
||||||
pane.set_can_navigate(true, cx);
|
pane.set_can_navigate(true, cx);
|
||||||
pane.display_nav_history_buttons(None);
|
pane.display_nav_history_buttons(None);
|
||||||
|
@ -1441,6 +1504,12 @@ pub struct ContextEditor {
|
||||||
show_accept_terms: bool,
|
show_accept_terms: bool,
|
||||||
pub(crate) slash_menu_handle:
|
pub(crate) slash_menu_handle:
|
||||||
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
|
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
|
||||||
|
// dragged_file_worktrees is used to keep references to worktrees that were added
|
||||||
|
// when the user drag/dropped an external file onto the context editor. Since
|
||||||
|
// the worktree is not part of the project panel, it would be dropped as soon as
|
||||||
|
// the file is opened. In order to keep the worktree alive for the duration of the
|
||||||
|
// context editor, we keep a reference here.
|
||||||
|
dragged_file_worktrees: Vec<Model<Worktree>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TAB_TITLE: &str = "New Context";
|
const DEFAULT_TAB_TITLE: &str = "New Context";
|
||||||
|
@ -1505,6 +1574,7 @@ impl ContextEditor {
|
||||||
error_message: None,
|
error_message: None,
|
||||||
show_accept_terms: false,
|
show_accept_terms: false,
|
||||||
slash_menu_handle: Default::default(),
|
slash_menu_handle: Default::default(),
|
||||||
|
dragged_file_worktrees: Vec::new(),
|
||||||
};
|
};
|
||||||
this.update_message_headers(cx);
|
this.update_message_headers(cx);
|
||||||
this.update_image_blocks(cx);
|
this.update_image_blocks(cx);
|
||||||
|
@ -2980,6 +3050,80 @@ impl ContextEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_dragged_files(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
action: &InsertDraggedFiles,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
|
||||||
|
let paths = match action {
|
||||||
|
InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
|
||||||
|
InsertDraggedFiles::ExternalFiles(paths) => {
|
||||||
|
let tasks = paths
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(move |_, cx| 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
(paths, worktrees)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let (paths, dragged_file_worktrees) = paths.await;
|
||||||
|
let cmd_name = file_command::FileSlashCommand.name();
|
||||||
|
|
||||||
|
context_editor_view
|
||||||
|
.update(&mut cx, |context_editor, 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", cx);
|
||||||
|
editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
context_editor.confirm_command(&ConfirmCommand, cx);
|
||||||
|
|
||||||
|
context_editor
|
||||||
|
.dragged_file_worktrees
|
||||||
|
.extend(dragged_file_worktrees);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn quote_selection(
|
fn quote_selection(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &QuoteSelection,
|
_: &QuoteSelection,
|
||||||
|
|
|
@ -1825,7 +1825,7 @@ impl Pane {
|
||||||
}))
|
}))
|
||||||
.on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
|
.on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
|
||||||
this.drag_split_direction = None;
|
this.drag_split_direction = None;
|
||||||
this.handle_project_entry_drop(&selection.active_selection.entry_id, cx)
|
this.handle_dragged_selection_drop(selection, cx)
|
||||||
}))
|
}))
|
||||||
.on_drop(cx.listener(move |this, paths, cx| {
|
.on_drop(cx.listener(move |this, paths, cx| {
|
||||||
this.drag_split_direction = None;
|
this.drag_split_direction = None;
|
||||||
|
@ -2170,6 +2170,19 @@ impl Pane {
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_dragged_selection_drop(
|
||||||
|
&mut self,
|
||||||
|
dragged_selection: &DraggedSelection,
|
||||||
|
cx: &mut ViewContext<'_, Self>,
|
||||||
|
) {
|
||||||
|
if let Some(custom_drop_handle) = self.custom_drop_handle.clone() {
|
||||||
|
if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.handle_project_entry_drop(&dragged_selection.active_selection.entry_id, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_project_entry_drop(
|
fn handle_project_entry_drop(
|
||||||
&mut self,
|
&mut self,
|
||||||
project_entry_id: &ProjectEntryId,
|
project_entry_id: &ProjectEntryId,
|
||||||
|
@ -2478,10 +2491,7 @@ impl Render for Pane {
|
||||||
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
|
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
|
||||||
}))
|
}))
|
||||||
.on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
|
.on_drop(cx.listener(move |this, selection: &DraggedSelection, cx| {
|
||||||
this.handle_project_entry_drop(
|
this.handle_dragged_selection_drop(selection, cx)
|
||||||
&selection.active_selection.entry_id,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
.on_drop(cx.listener(move |this, paths, cx| {
|
.on_drop(cx.listener(move |this, paths, cx| {
|
||||||
this.handle_external_paths_drop(paths, cx)
|
this.handle_external_paths_drop(paths, cx)
|
||||||
|
|
|
@ -2063,7 +2063,7 @@ impl Workspace {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_path_for_path(
|
pub fn project_path_for_path(
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
abs_path: &Path,
|
abs_path: &Path,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue