assistant2: Rework @mentions
(#26983)
https://github.com/user-attachments/assets/167f753f-2775-4d31-bfef-55565e61e4bc Release Notes: - N/A
This commit is contained in:
parent
4a5f89aded
commit
699369995b
18 changed files with 1637 additions and 485 deletions
|
@ -1,19 +1,28 @@
|
||||||
|
mod completion_provider;
|
||||||
mod fetch_context_picker;
|
mod fetch_context_picker;
|
||||||
mod file_context_picker;
|
mod file_context_picker;
|
||||||
mod thread_context_picker;
|
mod thread_context_picker;
|
||||||
|
|
||||||
|
use std::ops::Range;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use editor::Editor;
|
use editor::display_map::{Crease, FoldId};
|
||||||
|
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||||
use file_context_picker::render_file_context_entry;
|
use file_context_picker::render_file_context_entry;
|
||||||
use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
|
use gpui::{
|
||||||
|
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
|
||||||
|
};
|
||||||
|
use multi_buffer::MultiBufferRow;
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
|
use ui::{
|
||||||
|
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
||||||
|
};
|
||||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||||
|
|
||||||
|
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||||
|
@ -34,7 +43,28 @@ enum ContextPickerMode {
|
||||||
Thread,
|
Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ContextPickerMode {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"file" => Ok(Self::File),
|
||||||
|
"fetch" => Ok(Self::Fetch),
|
||||||
|
"thread" => Ok(Self::Thread),
|
||||||
|
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ContextPickerMode {
|
impl ContextPickerMode {
|
||||||
|
pub fn mention_prefix(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::File => "file",
|
||||||
|
Self::Fetch => "fetch",
|
||||||
|
Self::Thread => "thread",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::File => "File/Directory",
|
Self::File => "File/Directory",
|
||||||
|
@ -63,7 +93,6 @@ enum ContextPickerState {
|
||||||
pub(super) struct ContextPicker {
|
pub(super) struct ContextPicker {
|
||||||
mode: ContextPickerState,
|
mode: ContextPickerState,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
|
@ -74,7 +103,6 @@ impl ContextPicker {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -88,7 +116,6 @@ impl ContextPicker {
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
context_store,
|
||||||
thread_store,
|
thread_store,
|
||||||
editor,
|
|
||||||
confirm_behavior,
|
confirm_behavior,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,10 +136,7 @@ impl ContextPicker {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||||
|
|
||||||
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
let modes = supported_context_picker_modes(&self.thread_store);
|
||||||
if self.allow_threads() {
|
|
||||||
modes.push(ContextPickerMode::Thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
let menu = menu
|
let menu = menu
|
||||||
.when(has_recent, |menu| {
|
.when(has_recent, |menu| {
|
||||||
|
@ -174,7 +198,6 @@ impl ContextPicker {
|
||||||
FileContextPicker::new(
|
FileContextPicker::new(
|
||||||
context_picker.clone(),
|
context_picker.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.editor.clone(),
|
|
||||||
self.context_store.clone(),
|
self.context_store.clone(),
|
||||||
self.confirm_behavior,
|
self.confirm_behavior,
|
||||||
window,
|
window,
|
||||||
|
@ -278,7 +301,7 @@ impl ContextPicker {
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = context_store.update(cx, |context_store, cx| {
|
let task = context_store.update(cx, |context_store, cx| {
|
||||||
context_store.add_file_from_path(project_path.clone(), cx)
|
context_store.add_file_from_path(project_path.clone(), true, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
|
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
|
||||||
|
@ -308,7 +331,7 @@ impl ContextPicker {
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let thread = open_thread_task.await?;
|
let thread = open_thread_task.await?;
|
||||||
context_store.update(cx, |context_store, cx| {
|
context_store.update(cx, |context_store, cx| {
|
||||||
context_store.add_thread(thread, cx);
|
context_store.add_thread(thread, true, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
this.update(cx, |_this, cx| cx.notify())
|
this.update(cx, |_this, cx| cx.notify())
|
||||||
|
@ -328,7 +351,7 @@ impl ContextPicker {
|
||||||
|
|
||||||
let mut current_files = context_store.file_paths(cx);
|
let mut current_files = context_store.file_paths(cx);
|
||||||
|
|
||||||
if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
|
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
|
||||||
current_files.insert(active_path);
|
current_files.insert(active_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,16 +407,6 @@ impl ContextPicker {
|
||||||
|
|
||||||
recent
|
recent
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
|
||||||
let active_item = workspace.active_item(cx)?;
|
|
||||||
|
|
||||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
|
||||||
let buffer = editor.buffer().read(cx).as_singleton()?;
|
|
||||||
|
|
||||||
let path = buffer.read(cx).file()?.path().to_path_buf();
|
|
||||||
Some(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||||
|
@ -429,3 +442,212 @@ enum RecentEntry {
|
||||||
},
|
},
|
||||||
Thread(ThreadContextEntry),
|
Thread(ThreadContextEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supported_context_picker_modes(
|
||||||
|
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||||
|
) -> Vec<ContextPickerMode> {
|
||||||
|
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
||||||
|
if thread_store.is_some() {
|
||||||
|
modes.push(ContextPickerMode::Thread);
|
||||||
|
}
|
||||||
|
modes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||||
|
let active_item = workspace.active_item(cx)?;
|
||||||
|
|
||||||
|
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||||
|
let buffer = editor.buffer().read(cx).as_singleton()?;
|
||||||
|
|
||||||
|
let path = buffer.read(cx).file()?.path().to_path_buf();
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_context_picker_entries(
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<RecentEntry> {
|
||||||
|
let mut recent = Vec::with_capacity(6);
|
||||||
|
|
||||||
|
let mut current_files = context_store.read(cx).file_paths(cx);
|
||||||
|
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
|
||||||
|
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
|
||||||
|
current_files.insert(active_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
|
||||||
|
recent.extend(
|
||||||
|
workspace
|
||||||
|
.recent_navigation_history_iter(cx)
|
||||||
|
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||||
|
.take(4)
|
||||||
|
.filter_map(|(project_path, _)| {
|
||||||
|
project
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
|
.map(|worktree| RecentEntry::File {
|
||||||
|
project_path,
|
||||||
|
path_prefix: worktree.read(cx).root_name().into(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut current_threads = context_store.read(cx).thread_ids();
|
||||||
|
|
||||||
|
if let Some(active_thread) = workspace
|
||||||
|
.panel::<AssistantPanel>(cx)
|
||||||
|
.map(|panel| panel.read(cx).active_thread(cx))
|
||||||
|
{
|
||||||
|
current_threads.insert(active_thread.read(cx).id().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
|
||||||
|
recent.extend(
|
||||||
|
thread_store
|
||||||
|
.read(cx)
|
||||||
|
.threads()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|thread| !current_threads.contains(&thread.id))
|
||||||
|
.take(2)
|
||||||
|
.map(|thread| {
|
||||||
|
RecentEntry::Thread(ThreadContextEntry {
|
||||||
|
id: thread.id,
|
||||||
|
summary: thread.summary,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
recent
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert_crease_for_mention(
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
crease_start: text::Anchor,
|
||||||
|
content_len: usize,
|
||||||
|
crease_label: SharedString,
|
||||||
|
crease_icon_path: SharedString,
|
||||||
|
editor_entity: Entity<Editor>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
editor_entity.update(cx, |editor, cx| {
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||||
|
|
||||||
|
let placeholder = FoldPlaceholder {
|
||||||
|
render: render_fold_icon_button(
|
||||||
|
crease_icon_path,
|
||||||
|
crease_label,
|
||||||
|
editor_entity.downgrade(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_trailer =
|
||||||
|
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||||
|
|
||||||
|
let crease = Crease::inline(
|
||||||
|
start..end,
|
||||||
|
placeholder.clone(),
|
||||||
|
fold_toggle("mention"),
|
||||||
|
render_trailer,
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.insert_creases(vec![crease.clone()], cx);
|
||||||
|
editor.fold_creases(vec![crease], false, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_fold_icon_button(
|
||||||
|
icon_path: SharedString,
|
||||||
|
label: SharedString,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||||
|
Arc::new({
|
||||||
|
move |fold_id, fold_range, cx| {
|
||||||
|
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let snapshot = editor
|
||||||
|
.buffer()
|
||||||
|
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
|
||||||
|
|
||||||
|
let is_in_pending_selection = || {
|
||||||
|
editor
|
||||||
|
.selections
|
||||||
|
.pending
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|pending_selection| {
|
||||||
|
pending_selection
|
||||||
|
.selection
|
||||||
|
.range()
|
||||||
|
.includes(&fold_range, &snapshot)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_in_complete_selection = || {
|
||||||
|
editor
|
||||||
|
.selections
|
||||||
|
.disjoint_in_range::<usize>(fold_range.clone(), cx)
|
||||||
|
.into_iter()
|
||||||
|
.any(|selection| {
|
||||||
|
// This is needed to cover a corner case, if we just check for an existing
|
||||||
|
// selection in the fold range, having a cursor at the start of the fold
|
||||||
|
// marks it as selected. Non-empty selections don't cause this.
|
||||||
|
let length = selection.end - selection.start;
|
||||||
|
length > 0
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
is_in_pending_selection() || is_in_complete_selection()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
ButtonLike::new(fold_id)
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
|
.toggle_state(is_in_text_selection)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::from_path(icon_path.clone())
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new(label.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.single_line(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold_toggle(
|
||||||
|
name: &'static str,
|
||||||
|
) -> impl Fn(
|
||||||
|
MultiBufferRow,
|
||||||
|
bool,
|
||||||
|
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
||||||
|
&mut Window,
|
||||||
|
&mut App,
|
||||||
|
) -> AnyElement {
|
||||||
|
move |row, is_folded, fold, _window, _cx| {
|
||||||
|
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||||
|
.toggle_state(is_folded)
|
||||||
|
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1024
crates/assistant2/src/context_picker/completion_provider.rs
Normal file
1024
crates/assistant2/src/context_picker/completion_provider.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -81,8 +81,12 @@ impl FetchContextPickerDelegate {
|
||||||
url: String::new(),
|
url: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
|
pub(crate) async fn fetch_url_content(
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<String> {
|
||||||
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||||
format!("https://{url}")
|
format!("https://{url}")
|
||||||
} else {
|
} else {
|
||||||
|
@ -154,7 +158,6 @@ impl FetchContextPickerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for FetchContextPickerDelegate {
|
impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
type ListItem = ListItem;
|
type ListItem = ListItem;
|
||||||
|
@ -208,7 +211,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
let confirm_behavior = self.confirm_behavior;
|
let confirm_behavior = self.confirm_behavior;
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let text = cx
|
let text = cx
|
||||||
.background_spawn(Self::build_message(http_client, url.clone()))
|
.background_spawn(fetch_url_content(http_client, url.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
|
|
@ -1,25 +1,15 @@
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::ops::Range;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use editor::actions::FoldAt;
|
|
||||||
use editor::display_map::{Crease, FoldId};
|
|
||||||
use editor::scroll::Autoscroll;
|
|
||||||
use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
|
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
|
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||||
Task, WeakEntity,
|
|
||||||
};
|
};
|
||||||
use multi_buffer::{MultiBufferPoint, MultiBufferRow};
|
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||||
use rope::Point;
|
use ui::{prelude::*, ListItem, Tooltip};
|
||||||
use text::SelectionGoal;
|
|
||||||
use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
|
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||||
|
|
||||||
|
@ -34,7 +24,6 @@ impl FileContextPicker {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -43,7 +32,6 @@ impl FileContextPicker {
|
||||||
let delegate = FileContextPickerDelegate::new(
|
let delegate = FileContextPickerDelegate::new(
|
||||||
context_picker,
|
context_picker,
|
||||||
workspace,
|
workspace,
|
||||||
editor,
|
|
||||||
context_store,
|
context_store,
|
||||||
confirm_behavior,
|
confirm_behavior,
|
||||||
);
|
);
|
||||||
|
@ -68,7 +56,6 @@ impl Render for FileContextPicker {
|
||||||
pub struct FileContextPickerDelegate {
|
pub struct FileContextPickerDelegate {
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
matches: Vec<PathMatch>,
|
matches: Vec<PathMatch>,
|
||||||
|
@ -79,27 +66,144 @@ impl FileContextPickerDelegate {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
context_picker,
|
context_picker,
|
||||||
workspace,
|
workspace,
|
||||||
editor,
|
|
||||||
context_store,
|
context_store,
|
||||||
confirm_behavior,
|
confirm_behavior,
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn search(
|
impl PickerDelegate for FileContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search files & directories…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
// TODO: This should be probably be run in the background.
|
||||||
|
let paths = search_task.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.delegate.matches = paths;
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||||
|
path: mat.path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_directory = mat.is_dir;
|
||||||
|
|
||||||
|
let Some(task) = self
|
||||||
|
.context_store
|
||||||
|
.update(cx, |context_store, cx| {
|
||||||
|
if is_directory {
|
||||||
|
context_store.add_directory(project_path, true, cx)
|
||||||
|
} else {
|
||||||
|
context_store.add_file_from_path(project_path, true, cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let confirm_behavior = self.confirm_behavior;
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await.notify_async_err(cx) {
|
||||||
|
None => anyhow::Ok(()),
|
||||||
|
Some(()) => this.update_in(cx, |this, window, cx| match confirm_behavior {
|
||||||
|
ConfirmBehavior::KeepOpen => {}
|
||||||
|
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let path_match = &self.matches[ix];
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.toggle_state(selected)
|
||||||
|
.child(render_file_context_entry(
|
||||||
|
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||||
|
&path_match.path,
|
||||||
|
&path_match.path_prefix,
|
||||||
|
path_match.is_dir,
|
||||||
|
self.context_store.clone(),
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_paths(
|
||||||
query: String,
|
query: String,
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &App,
|
||||||
) -> Task<Vec<PathMatch>> {
|
) -> Task<Vec<PathMatch>> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
|
@ -168,227 +272,6 @@ impl FileContextPickerDelegate {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for FileContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
self.matches.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Search files & directories…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return Task::ready(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
// TODO: This should be probably be run in the background.
|
|
||||||
let paths = search_task.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.delegate.matches = paths;
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let file_name = mat
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.map(|os_str| os_str.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or(mat.path_prefix.to_string());
|
|
||||||
|
|
||||||
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 is_directory = mat.is_dir;
|
|
||||||
|
|
||||||
let Some(editor_entity) = self.editor.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
editor_entity.update(cx, |editor, cx| {
|
|
||||||
editor.transact(window, cx, |editor, window, 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::<MultiBufferPoint>(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()), window, cx, |s| {
|
|
||||||
s.select(selections)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_anchors = {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
editor
|
|
||||||
.selections
|
|
||||||
.all::<Point>(cx)
|
|
||||||
.into_iter()
|
|
||||||
.map(|selection| snapshot.anchor_before(selection.start))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insert(&full_path, window, cx);
|
|
||||||
|
|
||||||
let end_anchors = {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
editor
|
|
||||||
.selections
|
|
||||||
.all::<Point>(cx)
|
|
||||||
.into_iter()
|
|
||||||
.map(|selection| snapshot.anchor_after(selection.end))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insert("\n", window, cx); // Needed to end the fold
|
|
||||||
|
|
||||||
let file_icon = if is_directory {
|
|
||||||
FileIcons::get_folder_icon(false, cx)
|
|
||||||
} else {
|
|
||||||
FileIcons::get_icon(&Path::new(&full_path), cx)
|
|
||||||
}
|
|
||||||
.unwrap_or_else(|| SharedString::new(""));
|
|
||||||
|
|
||||||
let placeholder = FoldPlaceholder {
|
|
||||||
render: render_fold_icon_button(
|
|
||||||
file_icon,
|
|
||||||
file_name.into(),
|
|
||||||
editor_entity.downgrade(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let render_trailer =
|
|
||||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| 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 }, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(task) = self
|
|
||||||
.context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
if is_directory {
|
|
||||||
context_store.add_directory(project_path, cx)
|
|
||||||
} else {
|
|
||||||
context_store.add_file_from_path(project_path, cx)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let confirm_behavior = self.confirm_behavior;
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await.notify_async_err(cx) {
|
|
||||||
None => anyhow::Ok(()),
|
|
||||||
Some(()) => this.update_in(cx, |this, window, cx| match confirm_behavior {
|
|
||||||
ConfirmBehavior::KeepOpen => {}
|
|
||||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let path_match = &self.matches[ix];
|
|
||||||
|
|
||||||
Some(
|
|
||||||
ListItem::new(ix)
|
|
||||||
.inset(true)
|
|
||||||
.toggle_state(selected)
|
|
||||||
.child(render_file_context_entry(
|
|
||||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
|
||||||
&path_match.path,
|
|
||||||
&path_match.path_prefix,
|
|
||||||
path_match.is_dir,
|
|
||||||
self.context_store.clone(),
|
|
||||||
cx,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_file_context_entry(
|
pub fn render_file_context_entry(
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
@ -484,85 +367,3 @@ pub fn render_file_context_entry(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_fold_icon_button(
|
|
||||||
icon: SharedString,
|
|
||||||
label: SharedString,
|
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
|
||||||
Arc::new(move |fold_id, fold_range, cx| {
|
|
||||||
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor
|
|
||||||
.buffer()
|
|
||||||
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
|
|
||||||
|
|
||||||
let is_in_pending_selection = || {
|
|
||||||
editor
|
|
||||||
.selections
|
|
||||||
.pending
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|pending_selection| {
|
|
||||||
pending_selection
|
|
||||||
.selection
|
|
||||||
.range()
|
|
||||||
.includes(&fold_range, &snapshot)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut is_in_complete_selection = || {
|
|
||||||
editor
|
|
||||||
.selections
|
|
||||||
.disjoint_in_range::<usize>(fold_range.clone(), cx)
|
|
||||||
.into_iter()
|
|
||||||
.any(|selection| {
|
|
||||||
// This is needed to cover a corner case, if we just check for an existing
|
|
||||||
// selection in the fold range, having a cursor at the start of the fold
|
|
||||||
// marks it as selected. Non-empty selections don't cause this.
|
|
||||||
let length = selection.end - selection.start;
|
|
||||||
length > 0
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
is_in_pending_selection() || is_in_complete_selection()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
ButtonLike::new(fold_id)
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
|
||||||
.toggle_state(is_in_text_selection)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::from_path(icon.clone())
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new(label.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.single_line(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fold_toggle(
|
|
||||||
name: &'static str,
|
|
||||||
) -> impl Fn(
|
|
||||||
MultiBufferRow,
|
|
||||||
bool,
|
|
||||||
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
|
||||||
&mut Window,
|
|
||||||
&mut App,
|
|
||||||
) -> AnyElement {
|
|
||||||
move |row, is_folded, fold, _window, _cx| {
|
|
||||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
|
||||||
.toggle_state(is_folded)
|
|
||||||
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -110,45 +110,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
|
let Some(threads) = self.thread_store.upgrade() else {
|
||||||
this.threads()
|
|
||||||
.into_iter()
|
|
||||||
.map(|thread| ThreadContextEntry {
|
|
||||||
id: thread.id,
|
|
||||||
summary: thread.summary,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}) else {
|
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let executor = cx.background_executor().clone();
|
let search_task = search_threads(query, threads, cx);
|
||||||
let search_task = cx.background_spawn(async move {
|
|
||||||
if query.is_empty() {
|
|
||||||
threads
|
|
||||||
} else {
|
|
||||||
let candidates = threads
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let matches = fuzzy::match_strings(
|
|
||||||
&candidates,
|
|
||||||
&query,
|
|
||||||
false,
|
|
||||||
100,
|
|
||||||
&Default::default(),
|
|
||||||
executor,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
matches
|
|
||||||
.into_iter()
|
|
||||||
.map(|mat| threads[mat.candidate_id].clone())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let matches = search_task.await;
|
let matches = search_task.await;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -176,7 +142,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.delegate
|
this.delegate
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
|
.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_thread(thread, true, cx)
|
||||||
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
match this.delegate.confirm_behavior {
|
match this.delegate.confirm_behavior {
|
||||||
|
@ -248,3 +216,46 @@ pub fn render_thread_context_entry(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_threads(
|
||||||
|
query: String,
|
||||||
|
thread_store: Entity<ThreadStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Vec<ThreadContextEntry>> {
|
||||||
|
let threads = thread_store.update(cx, |this, _cx| {
|
||||||
|
this.threads()
|
||||||
|
.into_iter()
|
||||||
|
.map(|thread| ThreadContextEntry {
|
||||||
|
id: thread.id,
|
||||||
|
summary: thread.summary,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
if query.is_empty() {
|
||||||
|
threads
|
||||||
|
} else {
|
||||||
|
let candidates = threads
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| threads[mat.candidate_id].clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ impl ContextStore {
|
||||||
pub fn add_file_from_path(
|
pub fn add_file_from_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
project_path: ProjectPath,
|
project_path: ProjectPath,
|
||||||
|
remove_if_exists: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
@ -86,7 +87,9 @@ impl ContextStore {
|
||||||
let already_included = this.update(cx, |this, _cx| {
|
let already_included = this.update(cx, |this, _cx| {
|
||||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||||
Some(FileInclusion::Direct(context_id)) => {
|
Some(FileInclusion::Direct(context_id)) => {
|
||||||
|
if remove_if_exists {
|
||||||
this.remove_context(context_id);
|
this.remove_context(context_id);
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Some(FileInclusion::InDirectory(_)) => true,
|
Some(FileInclusion::InDirectory(_)) => true,
|
||||||
|
@ -157,6 +160,7 @@ impl ContextStore {
|
||||||
pub fn add_directory(
|
pub fn add_directory(
|
||||||
&mut self,
|
&mut self,
|
||||||
project_path: ProjectPath,
|
project_path: ProjectPath,
|
||||||
|
remove_if_exists: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
@ -169,7 +173,9 @@ impl ContextStore {
|
||||||
|
|
||||||
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
||||||
{
|
{
|
||||||
|
if remove_if_exists {
|
||||||
self.remove_context(context_id);
|
self.remove_context(context_id);
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -256,9 +262,16 @@ impl ContextStore {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
|
pub fn add_thread(
|
||||||
|
&mut self,
|
||||||
|
thread: Entity<Thread>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
|
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
|
||||||
|
if remove_if_exists {
|
||||||
self.remove_context(context_id);
|
self.remove_context(context_id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.insert_thread(thread, cx);
|
self.insert_thread(thread, cx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ impl ContextStrip {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
suggest_context_kind: SuggestContextKind,
|
suggest_context_kind: SuggestContextKind,
|
||||||
|
@ -51,7 +50,6 @@ impl ContextStrip {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
editor.clone(),
|
|
||||||
ConfirmBehavior::KeepOpen,
|
ConfirmBehavior::KeepOpen,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -861,7 +861,6 @@ impl PromptEditor<BufferCodegen> {
|
||||||
ContextStrip::new(
|
ContextStrip::new(
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
prompt_editor.downgrade(),
|
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::Thread,
|
SuggestContextKind::Thread,
|
||||||
|
@ -1014,7 +1013,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
ContextStrip::new(
|
ContextStrip::new(
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
prompt_editor.downgrade(),
|
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::Thread,
|
SuggestContextKind::Thread,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::actions::MoveUp;
|
use editor::actions::MoveUp;
|
||||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use git::ExpandCommitEditor;
|
use git::ExpandCommitEditor;
|
||||||
use git_ui::git_panel;
|
use git_ui::git_panel;
|
||||||
|
@ -13,10 +13,8 @@ use gpui::{
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use language_model_selector::ToggleModelSelector;
|
use language_model_selector::ToggleModelSelector;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rope::Point;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use text::Bias;
|
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||||
|
@ -25,7 +23,7 @@ use vim_mode_setting::VimModeSetting;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::assistant_model_selector::AssistantModelSelector;
|
use crate::assistant_model_selector::AssistantModelSelector;
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||||
use crate::thread::{RequestKind, Thread};
|
use crate::thread::{RequestKind, Thread};
|
||||||
|
@ -68,16 +66,30 @@ impl MessageEditor {
|
||||||
let mut editor = Editor::auto_height(10, window, cx);
|
let mut editor = Editor::auto_height(10, window, cx);
|
||||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||||
editor.set_show_indent_guides(false, cx);
|
editor.set_show_indent_guides(false, cx);
|
||||||
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
|
min_entries_visible: 12,
|
||||||
|
max_entries_visible: 12,
|
||||||
|
placement: Some(ContextMenuPlacement::Above),
|
||||||
|
});
|
||||||
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let editor_entity = editor.downgrade();
|
||||||
|
editor.update(cx, |editor, _| {
|
||||||
|
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||||
|
workspace.clone(),
|
||||||
|
context_store.downgrade(),
|
||||||
|
Some(thread_store.clone()),
|
||||||
|
editor_entity,
|
||||||
|
))));
|
||||||
|
});
|
||||||
|
|
||||||
let inline_context_picker = cx.new(|cx| {
|
let inline_context_picker = cx.new(|cx| {
|
||||||
ContextPicker::new(
|
ContextPicker::new(
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
editor.downgrade(),
|
|
||||||
ConfirmBehavior::Close,
|
ConfirmBehavior::Close,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -88,7 +100,6 @@ impl MessageEditor {
|
||||||
ContextStrip::new(
|
ContextStrip::new(
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
editor.downgrade(),
|
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
context_picker_menu_handle.clone(),
|
context_picker_menu_handle.clone(),
|
||||||
SuggestContextKind::File,
|
SuggestContextKind::File,
|
||||||
|
@ -98,7 +109,6 @@ impl MessageEditor {
|
||||||
});
|
});
|
||||||
|
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
cx.subscribe_in(&editor, window, Self::handle_editor_event),
|
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&inline_context_picker,
|
&inline_context_picker,
|
||||||
window,
|
window,
|
||||||
|
@ -232,34 +242,6 @@ impl MessageEditor {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_editor_event(
|
|
||||||
&mut self,
|
|
||||||
editor: &Entity<Editor>,
|
|
||||||
event: &EditorEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
EditorEvent::SelectionsChanged { .. } => {
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
|
||||||
if newest_cursor.column > 0 {
|
|
||||||
let behind_cursor = snapshot.clip_point(
|
|
||||||
Point::new(newest_cursor.row, newest_cursor.column - 1),
|
|
||||||
Bias::Left,
|
|
||||||
);
|
|
||||||
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
|
|
||||||
if char_behind_cursor == Some('@') {
|
|
||||||
self.inline_context_picker_menu_handle.show(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_inline_context_picker_event(
|
fn handle_inline_context_picker_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_inline_context_picker: &Entity<ContextPicker>,
|
_inline_context_picker: &Entity<ContextPicker>,
|
||||||
|
@ -616,6 +598,7 @@ impl Render for MessageEditor {
|
||||||
background: editor_bg_color,
|
background: editor_bg_color,
|
||||||
local_player: cx.theme().players().local(),
|
local_player: cx.theme().players().local(),
|
||||||
text: text_style,
|
text: text_style,
|
||||||
|
syntax: cx.theme().syntax().clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::context_editor::ContextEditor;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
pub use assistant_slash_command::SlashCommand;
|
pub use assistant_slash_command::SlashCommand;
|
||||||
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
||||||
use editor::{CompletionProvider, Editor};
|
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||||
use language::{Anchor, Buffer, ToPoint};
|
use language::{Anchor, Buffer, ToPoint};
|
||||||
|
@ -126,6 +126,7 @@ impl SlashCommandCompletionProvider {
|
||||||
)),
|
)),
|
||||||
new_text,
|
new_text,
|
||||||
label: command.label(cx),
|
label: command.label(cx),
|
||||||
|
icon_path: None,
|
||||||
confirm,
|
confirm,
|
||||||
source: CompletionSource::Custom,
|
source: CompletionSource::Custom,
|
||||||
})
|
})
|
||||||
|
@ -223,6 +224,7 @@ impl SlashCommandCompletionProvider {
|
||||||
last_argument_range.clone()
|
last_argument_range.clone()
|
||||||
},
|
},
|
||||||
label: new_argument.label,
|
label: new_argument.label,
|
||||||
|
icon_path: None,
|
||||||
new_text,
|
new_text,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
confirm,
|
confirm,
|
||||||
|
@ -241,6 +243,7 @@ impl SlashCommandCompletionProvider {
|
||||||
impl CompletionProvider for SlashCommandCompletionProvider {
|
impl CompletionProvider for SlashCommandCompletionProvider {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
_excerpt_id: ExcerptId,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
buffer_position: Anchor,
|
buffer_position: Anchor,
|
||||||
_: editor::CompletionContext,
|
_: editor::CompletionContext,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{Context as _, Result};
|
||||||
use channel::{ChannelChat, ChannelStore, MessageParams};
|
use channel::{ChannelChat, ChannelStore, MessageParams};
|
||||||
use client::{UserId, UserStore};
|
use client::{UserId, UserStore};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight,
|
AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight,
|
||||||
|
@ -56,6 +56,7 @@ struct MessageEditorCompletionProvider(WeakEntity<MessageEditor>);
|
||||||
impl CompletionProvider for MessageEditorCompletionProvider {
|
impl CompletionProvider for MessageEditorCompletionProvider {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
_excerpt_id: ExcerptId,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
buffer_position: language::Anchor,
|
buffer_position: language::Anchor,
|
||||||
_: editor::CompletionContext,
|
_: editor::CompletionContext,
|
||||||
|
@ -311,6 +312,7 @@ impl MessageEditor {
|
||||||
old_range: range.clone(),
|
old_range: range.clone(),
|
||||||
new_text,
|
new_text,
|
||||||
label,
|
label,
|
||||||
|
icon_path: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
source: CompletionSource::Custom,
|
source: CompletionSource::Custom,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use super::{
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use dap::OutputEvent;
|
use dap::OutputEvent;
|
||||||
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
|
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
|
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
|
||||||
use language::{Buffer, CodeLabel};
|
use language::{Buffer, CodeLabel};
|
||||||
|
@ -246,6 +246,7 @@ struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
|
||||||
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
_excerpt_id: ExcerptId,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
buffer_position: language::Anchor,
|
buffer_position: language::Anchor,
|
||||||
_trigger: editor::CompletionContext,
|
_trigger: editor::CompletionContext,
|
||||||
|
@ -367,6 +368,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||||
text: format!("{} {}", string_match.string.clone(), variable_value),
|
text: format!("{} {}", string_match.string.clone(), variable_value),
|
||||||
runs: Vec::new(),
|
runs: Vec::new(),
|
||||||
},
|
},
|
||||||
|
icon_path: None,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
source: project::CompletionSource::Custom,
|
source: project::CompletionSource::Custom,
|
||||||
|
@ -408,6 +410,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||||
text: completion.label.clone(),
|
text: completion.label.clone(),
|
||||||
runs: Vec::new(),
|
runs: Vec::new(),
|
||||||
},
|
},
|
||||||
|
icon_path: None,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
source: project::CompletionSource::Custom,
|
source: project::CompletionSource::Custom,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, Focusable, FontWeight,
|
div, px, uniform_list, AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight,
|
||||||
ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||||
UniformListScrollHandle,
|
UniformListScrollHandle,
|
||||||
};
|
};
|
||||||
|
@ -236,6 +236,7 @@ impl CompletionsMenu {
|
||||||
runs: Default::default(),
|
runs: Default::default(),
|
||||||
filter_range: Default::default(),
|
filter_range: Default::default(),
|
||||||
},
|
},
|
||||||
|
icon_path: None,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
source: CompletionSource::Custom,
|
source: CompletionSource::Custom,
|
||||||
|
@ -539,9 +540,17 @@ impl CompletionsMenu {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let color_swatch = completion
|
|
||||||
|
let start_slot = completion
|
||||||
.color()
|
.color()
|
||||||
.map(|color| div().size_4().bg(color).rounded_xs());
|
.map(|color| div().size_4().bg(color).rounded_xs().into_any_element())
|
||||||
|
.or_else(|| {
|
||||||
|
completion.icon_path.as_ref().map(|path| {
|
||||||
|
Icon::from_path(path)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
div().min_w(px(280.)).max_w(px(540.)).child(
|
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||||
ListItem::new(mat.candidate_id)
|
ListItem::new(mat.candidate_id)
|
||||||
|
@ -559,7 +568,7 @@ impl CompletionsMenu {
|
||||||
task.detach_and_log_err(cx)
|
task.detach_and_log_err(cx)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.start_slot::<Div>(color_swatch)
|
.start_slot::<AnyElement>(start_slot)
|
||||||
.child(h_flex().overflow_hidden().child(completion_label))
|
.child(h_flex().overflow_hidden().child(completion_label))
|
||||||
.end_slot::<Label>(documentation_label),
|
.end_slot::<Label>(documentation_label),
|
||||||
)
|
)
|
||||||
|
|
|
@ -531,6 +531,18 @@ impl EditPredictionPreview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ContextMenuOptions {
|
||||||
|
pub min_entries_visible: usize,
|
||||||
|
pub max_entries_visible: usize,
|
||||||
|
pub placement: Option<ContextMenuPlacement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ContextMenuPlacement {
|
||||||
|
Above,
|
||||||
|
Below,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||||
struct EditorActionId(usize);
|
struct EditorActionId(usize);
|
||||||
|
|
||||||
|
@ -677,6 +689,7 @@ pub struct Editor {
|
||||||
active_indent_guides_state: ActiveIndentGuidesState,
|
active_indent_guides_state: ActiveIndentGuidesState,
|
||||||
nav_history: Option<ItemNavHistory>,
|
nav_history: Option<ItemNavHistory>,
|
||||||
context_menu: RefCell<Option<CodeContextMenu>>,
|
context_menu: RefCell<Option<CodeContextMenu>>,
|
||||||
|
context_menu_options: Option<ContextMenuOptions>,
|
||||||
mouse_context_menu: Option<MouseContextMenu>,
|
mouse_context_menu: Option<MouseContextMenu>,
|
||||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||||
signature_help_state: SignatureHelpState,
|
signature_help_state: SignatureHelpState,
|
||||||
|
@ -1441,6 +1454,7 @@ impl Editor {
|
||||||
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
||||||
nav_history: None,
|
nav_history: None,
|
||||||
context_menu: RefCell::new(None),
|
context_menu: RefCell::new(None),
|
||||||
|
context_menu_options: None,
|
||||||
mouse_context_menu: None,
|
mouse_context_menu: None,
|
||||||
completion_tasks: Default::default(),
|
completion_tasks: Default::default(),
|
||||||
signature_help_state: SignatureHelpState::default(),
|
signature_help_state: SignatureHelpState::default(),
|
||||||
|
@ -4251,8 +4265,14 @@ impl Editor {
|
||||||
|
|
||||||
let (mut words, provided_completions) = match provider {
|
let (mut words, provided_completions) = match provider {
|
||||||
Some(provider) => {
|
Some(provider) => {
|
||||||
let completions =
|
let completions = provider.completions(
|
||||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
position.excerpt_id,
|
||||||
|
&buffer,
|
||||||
|
buffer_position,
|
||||||
|
completion_context,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let words = match completion_settings.words {
|
let words = match completion_settings.words {
|
||||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||||
|
@ -4310,6 +4330,7 @@ impl Editor {
|
||||||
old_range: old_range.clone(),
|
old_range: old_range.clone(),
|
||||||
new_text: word.clone(),
|
new_text: word.clone(),
|
||||||
label: CodeLabel::plain(word, None),
|
label: CodeLabel::plain(word, None),
|
||||||
|
icon_path: None,
|
||||||
documentation: None,
|
documentation: None,
|
||||||
source: CompletionSource::BufferWord {
|
source: CompletionSource::BufferWord {
|
||||||
word_range,
|
word_range,
|
||||||
|
@ -4384,6 +4405,17 @@ impl Editor {
|
||||||
self.completion_tasks.push((id, task));
|
self.completion_tasks.push((id, task));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
pub fn current_completions(&self) -> Option<Vec<project::Completion>> {
|
||||||
|
let menu = self.context_menu.borrow();
|
||||||
|
if let CodeContextMenu::Completions(menu) = menu.as_ref()? {
|
||||||
|
let completions = menu.completions.borrow();
|
||||||
|
Some(completions.to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn confirm_completion(
|
pub fn confirm_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &ConfirmCompletion,
|
action: &ConfirmCompletion,
|
||||||
|
@ -6435,6 +6467,10 @@ impl Editor {
|
||||||
.map(|menu| menu.origin())
|
.map(|menu| menu.origin())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) {
|
||||||
|
self.context_menu_options = Some(options);
|
||||||
|
}
|
||||||
|
|
||||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||||
|
|
||||||
|
@ -17857,6 +17893,7 @@ pub trait SemanticsProvider {
|
||||||
pub trait CompletionProvider {
|
pub trait CompletionProvider {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
buffer_position: text::Anchor,
|
buffer_position: text::Anchor,
|
||||||
trigger: CompletionContext,
|
trigger: CompletionContext,
|
||||||
|
@ -18090,6 +18127,7 @@ fn snippet_completions(
|
||||||
runs: Vec::new(),
|
runs: Vec::new(),
|
||||||
filter_range: 0..matching_prefix.len(),
|
filter_range: 0..matching_prefix.len(),
|
||||||
},
|
},
|
||||||
|
icon_path: None,
|
||||||
documentation: snippet
|
documentation: snippet
|
||||||
.description
|
.description
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -18106,6 +18144,7 @@ fn snippet_completions(
|
||||||
impl CompletionProvider for Entity<Project> {
|
impl CompletionProvider for Entity<Project> {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
_excerpt_id: ExcerptId,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
buffer_position: text::Anchor,
|
buffer_position: text::Anchor,
|
||||||
options: CompletionContext,
|
options: CompletionContext,
|
||||||
|
|
|
@ -16,15 +16,15 @@ use crate::{
|
||||||
items::BufferSearchHighlights,
|
items::BufferSearchHighlights,
|
||||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||||
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
|
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
|
||||||
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
|
BlockId, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
|
||||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
|
||||||
EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk, GoToPreviousHunk,
|
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk,
|
||||||
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
|
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||||
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp,
|
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
||||||
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
|
||||||
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
|
||||||
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS,
|
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||||
};
|
};
|
||||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||||
use client::ParticipantIndex;
|
use client::ParticipantIndex;
|
||||||
|
@ -3338,6 +3338,7 @@ impl EditorElement {
|
||||||
let height_below_menu = Pixels::ZERO;
|
let height_below_menu = Pixels::ZERO;
|
||||||
let mut edit_prediction_popover_visible = false;
|
let mut edit_prediction_popover_visible = false;
|
||||||
let mut context_menu_visible = false;
|
let mut context_menu_visible = false;
|
||||||
|
let context_menu_placement;
|
||||||
|
|
||||||
{
|
{
|
||||||
let editor = self.editor.read(cx);
|
let editor = self.editor.read(cx);
|
||||||
|
@ -3351,11 +3352,22 @@ impl EditorElement {
|
||||||
|
|
||||||
if editor.context_menu_visible() {
|
if editor.context_menu_visible() {
|
||||||
if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() {
|
if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() {
|
||||||
min_menu_height += line_height * 3. + POPOVER_Y_PADDING;
|
let (min_height_in_lines, max_height_in_lines) = editor
|
||||||
max_menu_height += line_height * 12. + POPOVER_Y_PADDING;
|
.context_menu_options
|
||||||
|
.as_ref()
|
||||||
|
.map_or((3, 12), |options| {
|
||||||
|
(options.min_entries_visible, options.max_entries_visible)
|
||||||
|
});
|
||||||
|
|
||||||
|
min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||||||
|
max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||||||
context_menu_visible = true;
|
context_menu_visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context_menu_placement = editor
|
||||||
|
.context_menu_options
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|options| options.placement.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible = edit_prediction_popover_visible || context_menu_visible;
|
let visible = edit_prediction_popover_visible || context_menu_visible;
|
||||||
|
@ -3390,6 +3402,7 @@ impl EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
min_height,
|
min_height,
|
||||||
max_height,
|
max_height,
|
||||||
|
context_menu_placement,
|
||||||
text_hitbox,
|
text_hitbox,
|
||||||
viewport_bounds,
|
viewport_bounds,
|
||||||
window,
|
window,
|
||||||
|
@ -3532,8 +3545,16 @@ impl EditorElement {
|
||||||
x: -gutter_overshoot,
|
x: -gutter_overshoot,
|
||||||
y: gutter_row.next_row().as_f32() * line_height - scroll_pixel_position.y,
|
y: gutter_row.next_row().as_f32() * line_height - scroll_pixel_position.y,
|
||||||
};
|
};
|
||||||
let min_height = line_height * 3. + POPOVER_Y_PADDING;
|
|
||||||
let max_height = line_height * 12. + POPOVER_Y_PADDING;
|
let (min_height_in_lines, max_height_in_lines) = editor
|
||||||
|
.context_menu_options
|
||||||
|
.as_ref()
|
||||||
|
.map_or((3, 12), |options| {
|
||||||
|
(options.min_entries_visible, options.max_entries_visible)
|
||||||
|
});
|
||||||
|
|
||||||
|
let min_height = line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||||||
|
let max_height = line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING;
|
||||||
let viewport_bounds =
|
let viewport_bounds =
|
||||||
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
|
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
|
||||||
right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
|
right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
|
||||||
|
@ -3544,6 +3565,10 @@ impl EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
min_height,
|
min_height,
|
||||||
max_height,
|
max_height,
|
||||||
|
editor
|
||||||
|
.context_menu_options
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|options| options.placement.clone()),
|
||||||
text_hitbox,
|
text_hitbox,
|
||||||
viewport_bounds,
|
viewport_bounds,
|
||||||
window,
|
window,
|
||||||
|
@ -3564,6 +3589,7 @@ impl EditorElement {
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
min_height: Pixels,
|
min_height: Pixels,
|
||||||
max_height: Pixels,
|
max_height: Pixels,
|
||||||
|
placement: Option<ContextMenuPlacement>,
|
||||||
text_hitbox: &Hitbox,
|
text_hitbox: &Hitbox,
|
||||||
viewport_bounds: Bounds<Pixels>,
|
viewport_bounds: Bounds<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -3588,7 +3614,11 @@ impl EditorElement {
|
||||||
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
let available_above = bottom_y_when_flipped - text_hitbox.top();
|
||||||
let available_below = text_hitbox.bottom() - target_position.y;
|
let available_below = text_hitbox.bottom() - target_position.y;
|
||||||
let y_overflows_below = max_height > available_below;
|
let y_overflows_below = max_height > available_below;
|
||||||
let mut y_flipped = y_overflows_below && available_above > available_below;
|
let mut y_flipped = match placement {
|
||||||
|
Some(ContextMenuPlacement::Above) => true,
|
||||||
|
Some(ContextMenuPlacement::Below) => false,
|
||||||
|
None => y_overflows_below && available_above > available_below,
|
||||||
|
};
|
||||||
let mut height = cmp::min(
|
let mut height = cmp::min(
|
||||||
max_height,
|
max_height,
|
||||||
if y_flipped {
|
if y_flipped {
|
||||||
|
@ -3602,19 +3632,27 @@ impl EditorElement {
|
||||||
if height < min_height {
|
if height < min_height {
|
||||||
let available_above = bottom_y_when_flipped;
|
let available_above = bottom_y_when_flipped;
|
||||||
let available_below = viewport_bounds.bottom() - target_position.y;
|
let available_below = viewport_bounds.bottom() - target_position.y;
|
||||||
if available_below > min_height {
|
let (y_flipped_override, height_override) = match placement {
|
||||||
y_flipped = false;
|
Some(ContextMenuPlacement::Above) => {
|
||||||
height = min_height;
|
(true, cmp::min(available_above, min_height))
|
||||||
} else if available_above > min_height {
|
|
||||||
y_flipped = true;
|
|
||||||
height = min_height;
|
|
||||||
} else if available_above > available_below {
|
|
||||||
y_flipped = true;
|
|
||||||
height = available_above;
|
|
||||||
} else {
|
|
||||||
y_flipped = false;
|
|
||||||
height = available_below;
|
|
||||||
}
|
}
|
||||||
|
Some(ContextMenuPlacement::Below) => {
|
||||||
|
(false, cmp::min(available_below, min_height))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if available_below > min_height {
|
||||||
|
(false, min_height)
|
||||||
|
} else if available_above > min_height {
|
||||||
|
(true, min_height)
|
||||||
|
} else if available_above > available_below {
|
||||||
|
(true, available_above)
|
||||||
|
} else {
|
||||||
|
(false, available_below)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
y_flipped = y_flipped_override;
|
||||||
|
height = height_override;
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
|
let max_width_for_stable_x = viewport_bounds.right() - target_position.x;
|
||||||
|
|
|
@ -7872,6 +7872,7 @@ impl LspStore {
|
||||||
runs: Default::default(),
|
runs: Default::default(),
|
||||||
filter_range: Default::default(),
|
filter_range: Default::default(),
|
||||||
},
|
},
|
||||||
|
icon_path: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
}]))),
|
}]))),
|
||||||
0,
|
0,
|
||||||
|
@ -9098,6 +9099,7 @@ async fn populate_labels_for_completions(
|
||||||
old_range: completion.old_range,
|
old_range: completion.old_range,
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
source: completion.source,
|
source: completion.source,
|
||||||
|
icon_path: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9110,6 +9112,7 @@ async fn populate_labels_for_completions(
|
||||||
old_range: completion.old_range,
|
old_range: completion.old_range,
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
source: completion.source,
|
source: completion.source,
|
||||||
|
icon_path: None,
|
||||||
confirm: None,
|
confirm: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -390,6 +390,8 @@ pub struct Completion {
|
||||||
pub documentation: Option<CompletionDocumentation>,
|
pub documentation: Option<CompletionDocumentation>,
|
||||||
/// Completion data source which it was constructed from.
|
/// Completion data source which it was constructed from.
|
||||||
pub source: CompletionSource,
|
pub source: CompletionSource,
|
||||||
|
/// A path to an icon for this completion that is shown in the menu.
|
||||||
|
pub icon_path: Option<SharedString>,
|
||||||
/// An optional callback to invoke when this completion is confirmed.
|
/// An optional callback to invoke when this completion is confirmed.
|
||||||
/// Returns, whether new completions should be retriggered after the current one.
|
/// Returns, whether new completions should be retriggered after the current one.
|
||||||
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
|
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
|
||||||
|
|
|
@ -374,7 +374,7 @@ enum IconSource {
|
||||||
impl IconSource {
|
impl IconSource {
|
||||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
if path.starts_with("icons/file_icons") {
|
if path.starts_with("icons/") {
|
||||||
Self::Svg(path)
|
Self::Svg(path)
|
||||||
} else {
|
} else {
|
||||||
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
|
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue