agent: Fuzzy match on paths and symbols when typing @
(#28357)
Release Notes: - agent: Improve fuzzy matching when using @-mentions
This commit is contained in:
parent
088d7c1342
commit
780143298a
5 changed files with 415 additions and 328 deletions
|
@ -18,16 +18,133 @@ use text::{Anchor, ToPoint};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::AssistantContext;
|
use crate::context_picker::file_context_picker::search_files;
|
||||||
|
use crate::context_picker::symbol_context_picker::search_symbols;
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
|
|
||||||
use super::fetch_context_picker::fetch_url_content;
|
use super::fetch_context_picker::fetch_url_content;
|
||||||
use super::thread_context_picker::ThreadContextEntry;
|
use super::file_context_picker::FileMatch;
|
||||||
|
use super::symbol_context_picker::SymbolMatch;
|
||||||
|
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||||
use super::{
|
use super::{
|
||||||
ContextPickerMode, MentionLink, recent_context_picker_entries, supported_context_picker_modes,
|
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
|
||||||
|
supported_context_picker_modes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) enum Match {
|
||||||
|
Symbol(SymbolMatch),
|
||||||
|
File(FileMatch),
|
||||||
|
Thread(ThreadMatch),
|
||||||
|
Fetch(SharedString),
|
||||||
|
Mode(ContextPickerMode),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search(
|
||||||
|
mode: Option<ContextPickerMode>,
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
recent_entries: Vec<RecentEntry>,
|
||||||
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Vec<Match>> {
|
||||||
|
match mode {
|
||||||
|
Some(ContextPickerMode::File) => {
|
||||||
|
let search_files_task =
|
||||||
|
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
search_files_task
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Match::File)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(ContextPickerMode::Symbol) => {
|
||||||
|
let search_symbols_task =
|
||||||
|
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
search_symbols_task
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Match::Symbol)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(ContextPickerMode::Thread) => {
|
||||||
|
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
|
||||||
|
let search_threads_task =
|
||||||
|
search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
search_threads_task
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Match::Thread)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::ready(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ContextPickerMode::Fetch) => {
|
||||||
|
if !query.is_empty() {
|
||||||
|
Task::ready(vec![Match::Fetch(query.into())])
|
||||||
|
} else {
|
||||||
|
Task::ready(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if query.is_empty() {
|
||||||
|
let mut matches = recent_entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| match entry {
|
||||||
|
super::RecentEntry::File {
|
||||||
|
project_path,
|
||||||
|
path_prefix,
|
||||||
|
} => Match::File(FileMatch {
|
||||||
|
mat: fuzzy::PathMatch {
|
||||||
|
score: 1.,
|
||||||
|
positions: Vec::new(),
|
||||||
|
worktree_id: project_path.worktree_id.to_usize(),
|
||||||
|
path: project_path.path,
|
||||||
|
path_prefix,
|
||||||
|
is_dir: false,
|
||||||
|
distance_to_relative_ancestor: 0,
|
||||||
|
},
|
||||||
|
is_recent: true,
|
||||||
|
}),
|
||||||
|
super::RecentEntry::Thread(thread_context_entry) => {
|
||||||
|
Match::Thread(ThreadMatch {
|
||||||
|
thread: thread_context_entry,
|
||||||
|
is_recent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
matches.extend(
|
||||||
|
supported_context_picker_modes(&thread_store)
|
||||||
|
.into_iter()
|
||||||
|
.map(Match::Mode),
|
||||||
|
);
|
||||||
|
|
||||||
|
Task::ready(matches)
|
||||||
|
} else {
|
||||||
|
let search_files_task =
|
||||||
|
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
search_files_task
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Match::File)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ContextPickerCompletionProvider {
|
pub struct ContextPickerCompletionProvider {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_completions(
|
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
|
||||||
excerpt_id: ExcerptId,
|
Completion {
|
||||||
source_range: Range<Anchor>,
|
replace_range: source_range.clone(),
|
||||||
context_store: Entity<ContextStore>,
|
new_text: format!("@{} ", mode.mention_prefix()),
|
||||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||||
editor: Entity<Editor>,
|
icon_path: Some(mode.icon().path().into()),
|
||||||
workspace: Entity<Workspace>,
|
documentation: None,
|
||||||
cx: &App,
|
source: project::CompletionSource::Custom,
|
||||||
) -> Vec<Completion> {
|
insert_text_mode: None,
|
||||||
let mut completions = Vec::new();
|
// This ensures that when a user accepts this completion, the
|
||||||
|
// completion menu will still be shown after "@category " is
|
||||||
completions.extend(
|
// inserted
|
||||||
recent_context_picker_entries(
|
confirm: Some(Arc::new(|_, _, _| true)),
|
||||||
context_store.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.iter()
|
|
||||||
.filter_map(|entry| match entry {
|
|
||||||
super::RecentEntry::File {
|
|
||||||
project_path,
|
|
||||||
path_prefix,
|
|
||||||
} => Some(Self::completion_for_path(
|
|
||||||
project_path.clone(),
|
|
||||||
path_prefix,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
cx,
|
|
||||||
)),
|
|
||||||
super::RecentEntry::Thread(thread_context_entry) => {
|
|
||||||
let thread_store = thread_store
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|thread_store| thread_store.upgrade())?;
|
|
||||||
Some(Self::completion_for_thread(
|
|
||||||
thread_context_entry.clone(),
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
true,
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
thread_store,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
completions.extend(
|
|
||||||
supported_context_picker_modes(&thread_store)
|
|
||||||
.iter()
|
|
||||||
.map(|mode| {
|
|
||||||
Completion {
|
|
||||||
replace_range: source_range.clone(),
|
|
||||||
new_text: format!("@{} ", mode.mention_prefix()),
|
|
||||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
|
||||||
icon_path: Some(mode.icon().path().into()),
|
|
||||||
documentation: None,
|
|
||||||
source: project::CompletionSource::Custom,
|
|
||||||
insert_text_mode: None,
|
|
||||||
// This ensures that when a user accepts this completion, the
|
|
||||||
// completion menu will still be shown after "@category " is
|
|
||||||
// inserted
|
|
||||||
confirm: Some(Arc::new(|_, _, _| true)),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
completions
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_code_label_for_full_path(
|
|
||||||
file_name: &str,
|
|
||||||
directory: Option<&str>,
|
|
||||||
cx: &App,
|
|
||||||
) -> CodeLabel {
|
|
||||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
|
||||||
let mut label = CodeLabel::default();
|
|
||||||
|
|
||||||
label.push_str(&file_name, None);
|
|
||||||
label.push_str(" ", None);
|
|
||||||
|
|
||||||
if let Some(directory) = directory {
|
|
||||||
label.push_str(&directory, comment_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label.filter_range = 0..label.text().len();
|
|
||||||
|
|
||||||
label
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completion_for_thread(
|
fn completion_for_thread(
|
||||||
|
@ -261,11 +301,8 @@ impl ContextPickerCompletionProvider {
|
||||||
path_prefix,
|
path_prefix,
|
||||||
);
|
);
|
||||||
|
|
||||||
let label = Self::build_code_label_for_full_path(
|
let label =
|
||||||
&file_name,
|
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
|
||||||
directory.as_ref().map(|s| s.as_ref()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let full_path = if let Some(directory) = directory {
|
let full_path = if let Some(directory) = directory {
|
||||||
format!("{}{}", directory, file_name)
|
format!("{}{}", directory, file_name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -382,6 +419,22 @@ impl ContextPickerCompletionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||||
|
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||||
|
let mut label = CodeLabel::default();
|
||||||
|
|
||||||
|
label.push_str(&file_name, None);
|
||||||
|
label.push_str(" ", None);
|
||||||
|
|
||||||
|
if let Some(directory) = directory {
|
||||||
|
label.push_str(&directory, comment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.filter_range = 0..label.text().len();
|
||||||
|
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
fn completions(
|
fn completions(
|
||||||
&self,
|
&self,
|
||||||
|
@ -404,10 +457,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
return Task::ready(Ok(None));
|
return Task::ready(Ok(None));
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
let Some((workspace, context_store)) =
|
||||||
return Task::ready(Ok(None));
|
self.workspace.upgrade().zip(self.context_store.upgrade())
|
||||||
};
|
else {
|
||||||
let Some(context_store) = self.context_store.upgrade() else {
|
|
||||||
return Task::ready(Ok(None));
|
return Task::ready(Ok(None));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -419,154 +471,89 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
let http_client = workspace.read(cx).client().http_client().clone();
|
let http_client = workspace.read(cx).client().http_client().clone();
|
||||||
|
|
||||||
|
let MentionCompletion { mode, argument, .. } = state;
|
||||||
|
let query = argument.unwrap_or_else(|| "".to_string());
|
||||||
|
|
||||||
|
let recent_entries = recent_context_picker_entries(
|
||||||
|
context_store.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let search_task = search(
|
||||||
|
mode,
|
||||||
|
query,
|
||||||
|
Arc::<AtomicBool>::default(),
|
||||||
|
recent_entries,
|
||||||
|
thread_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
let mut completions = Vec::new();
|
let matches = search_task.await;
|
||||||
|
let Some(editor) = editor.upgrade() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
let MentionCompletion { mode, argument, .. } = state;
|
Ok(Some(cx.update(|cx| {
|
||||||
|
matches
|
||||||
let query = argument.unwrap_or_else(|| "".to_string());
|
.into_iter()
|
||||||
match mode {
|
.filter_map(|mat| match mat {
|
||||||
Some(ContextPickerMode::File) => {
|
Match::File(FileMatch { mat, is_recent }) => {
|
||||||
let path_matches = cx
|
Some(Self::completion_for_path(
|
||||||
.update(|cx| {
|
ProjectPath {
|
||||||
super::file_context_picker::search_paths(
|
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||||
query,
|
path: mat.path.clone(),
|
||||||
Arc::<AtomicBool>::default(),
|
|
||||||
&workspace,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(editor) = editor.upgrade() {
|
|
||||||
completions.reserve(path_matches.len());
|
|
||||||
cx.update(|cx| {
|
|
||||||
completions.extend(path_matches.iter().map(|mat| {
|
|
||||||
Self::completion_for_path(
|
|
||||||
ProjectPath {
|
|
||||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
|
||||||
path: mat.path.clone(),
|
|
||||||
},
|
|
||||||
&mat.path_prefix,
|
|
||||||
false,
|
|
||||||
mat.is_dir,
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(ContextPickerMode::Symbol) => {
|
|
||||||
if let Some(editor) = editor.upgrade() {
|
|
||||||
let symbol_matches = cx
|
|
||||||
.update(|cx| {
|
|
||||||
super::symbol_context_picker::search_symbols(
|
|
||||||
query,
|
|
||||||
Arc::new(AtomicBool::default()),
|
|
||||||
&workspace,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
cx.update(|cx| {
|
|
||||||
completions.extend(symbol_matches.into_iter().filter_map(
|
|
||||||
|(_, symbol)| {
|
|
||||||
Self::completion_for_symbol(
|
|
||||||
symbol,
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
));
|
&mat.path_prefix,
|
||||||
})?;
|
is_recent,
|
||||||
}
|
mat.is_dir,
|
||||||
}
|
|
||||||
Some(ContextPickerMode::Fetch) => {
|
|
||||||
if let Some(editor) = editor.upgrade() {
|
|
||||||
if !query.is_empty() {
|
|
||||||
completions.push(Self::completion_for_fetch(
|
|
||||||
source_range.clone(),
|
|
||||||
query.into(),
|
|
||||||
excerpt_id,
|
excerpt_id,
|
||||||
|
source_range.clone(),
|
||||||
editor.clone(),
|
editor.clone(),
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
http_client.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
context_store.update(cx, |store, _| {
|
|
||||||
let urls = store.context().iter().filter_map(|context| {
|
|
||||||
if let AssistantContext::FetchedUrl(context) = context {
|
|
||||||
Some(context.url.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for url in urls {
|
|
||||||
completions.push(Self::completion_for_fetch(
|
|
||||||
source_range.clone(),
|
|
||||||
url,
|
|
||||||
excerpt_id,
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
http_client.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(ContextPickerMode::Thread) => {
|
|
||||||
if let Some((thread_store, editor)) = thread_store
|
|
||||||
.and_then(|thread_store| thread_store.upgrade())
|
|
||||||
.zip(editor.upgrade())
|
|
||||||
{
|
|
||||||
let threads = cx
|
|
||||||
.update(|cx| {
|
|
||||||
super::thread_context_picker::search_threads(
|
|
||||||
query,
|
|
||||||
thread_store.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await;
|
|
||||||
for thread in threads {
|
|
||||||
completions.push(Self::completion_for_thread(
|
|
||||||
thread.clone(),
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
false,
|
|
||||||
editor.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
cx.update(|cx| {
|
|
||||||
if let Some(editor) = editor.upgrade() {
|
|
||||||
completions.extend(Self::default_completions(
|
|
||||||
excerpt_id,
|
|
||||||
source_range.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
editor,
|
|
||||||
workspace.clone(),
|
|
||||||
cx,
|
cx,
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
})?;
|
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
|
||||||
}
|
symbol,
|
||||||
}
|
excerpt_id,
|
||||||
Ok(Some(completions))
|
source_range.clone(),
|
||||||
|
editor.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
Match::Thread(ThreadMatch {
|
||||||
|
thread, is_recent, ..
|
||||||
|
}) => {
|
||||||
|
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
|
||||||
|
Some(Self::completion_for_thread(
|
||||||
|
thread,
|
||||||
|
excerpt_id,
|
||||||
|
source_range.clone(),
|
||||||
|
is_recent,
|
||||||
|
editor.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
thread_store,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Match::Fetch(url) => Some(Self::completion_for_fetch(
|
||||||
|
source_range.clone(),
|
||||||
|
url,
|
||||||
|
excerpt_id,
|
||||||
|
editor.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
http_client.clone(),
|
||||||
|
)),
|
||||||
|
Match::Mode(mode) => {
|
||||||
|
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})?))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -676,7 +663,12 @@ impl MentionCompletion {
|
||||||
let mut end = last_mention_start + 1;
|
let mut end = last_mention_start + 1;
|
||||||
if let Some(mode_text) = parts.next() {
|
if let Some(mode_text) = parts.next() {
|
||||||
end += mode_text.len();
|
end += mode_text.len();
|
||||||
mode = ContextPickerMode::try_from(mode_text).ok();
|
|
||||||
|
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
|
||||||
|
mode = Some(parsed_mode);
|
||||||
|
} else {
|
||||||
|
argument = Some(mode_text.to_string());
|
||||||
|
}
|
||||||
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
|
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
|
||||||
Some(whitespace_count) => {
|
Some(whitespace_count) => {
|
||||||
if let Some(argument_text) = parts.next() {
|
if let Some(argument_text) = parts.next() {
|
||||||
|
@ -702,13 +694,13 @@ impl MentionCompletion {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{Focusable, TestAppContext, VisualTestContext};
|
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
|
||||||
use project::{Project, ProjectPath};
|
use project::{Project, ProjectPath};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{ops::Deref, path::PathBuf};
|
use std::ops::Deref;
|
||||||
use util::{path, separator};
|
use util::{path, separator};
|
||||||
use workspace::AppState;
|
use workspace::{AppState, Item};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mention_completion_parse() {
|
fn test_mention_completion_parse() {
|
||||||
|
@ -768,9 +760,42 @@ mod tests {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
MentionCompletion::try_parse("Lorem @main", 0),
|
||||||
|
Some(MentionCompletion {
|
||||||
|
source_range: 6..11,
|
||||||
|
mode: None,
|
||||||
|
argument: Some("main".to_string()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
|
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AtMentionEditor(Entity<Editor>);
|
||||||
|
|
||||||
|
impl Item for AtMentionEditor {
|
||||||
|
type Event = ();
|
||||||
|
|
||||||
|
fn include_in_nav_history() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<()> for AtMentionEditor {}
|
||||||
|
|
||||||
|
impl Focusable for AtMentionEditor {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.0.read(cx).focus_handle(cx).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AtMentionEditor {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.0.clone().into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -846,25 +871,27 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = workspace
|
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
.update_in(&mut cx, |workspace, window, cx| {
|
let editor = cx.new(|cx| {
|
||||||
workspace.open_path(
|
Editor::new(
|
||||||
ProjectPath {
|
editor::EditorMode::Full,
|
||||||
worktree_id,
|
multi_buffer::MultiBuffer::build_simple("", cx),
|
||||||
path: PathBuf::from("editor").into(),
|
|
||||||
},
|
|
||||||
None,
|
None,
|
||||||
true,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
});
|
||||||
.await
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
.expect("Could not open test file");
|
pane.add_item(
|
||||||
|
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
|
||||||
let editor = cx.update(|_, cx| {
|
true,
|
||||||
item.act_as::<Editor>(cx)
|
true,
|
||||||
.expect("Opened test file wasn't an editor")
|
None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
|
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
|
||||||
|
@ -895,10 +922,10 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
current_completion_labels(editor),
|
current_completion_labels(editor),
|
||||||
&[
|
&[
|
||||||
"editor dir/",
|
|
||||||
"seven.txt dir/b/",
|
"seven.txt dir/b/",
|
||||||
"six.txt dir/b/",
|
"six.txt dir/b/",
|
||||||
"five.txt dir/b/",
|
"five.txt dir/b/",
|
||||||
|
"four.txt dir/a/",
|
||||||
"Files & Directories",
|
"Files & Directories",
|
||||||
"Symbols",
|
"Symbols",
|
||||||
"Fetch"
|
"Fetch"
|
||||||
|
@ -993,14 +1020,14 @@ mod tests {
|
||||||
editor.update(&mut cx, |editor, cx| {
|
editor.update(&mut cx, |editor, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.text(cx),
|
editor.text(cx),
|
||||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
|
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
|
||||||
);
|
);
|
||||||
assert!(!editor.has_visible_completions_menu());
|
assert!(!editor.has_visible_completions_menu());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crease_ranges(editor, cx),
|
crease_ranges(editor, cx),
|
||||||
vec![
|
vec![
|
||||||
Point::new(0, 6)..Point::new(0, 37),
|
Point::new(0, 6)..Point::new(0, 37),
|
||||||
Point::new(0, 44)..Point::new(0, 71)
|
Point::new(0, 44)..Point::new(0, 79)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1010,14 +1037,14 @@ mod tests {
|
||||||
editor.update(&mut cx, |editor, cx| {
|
editor.update(&mut cx, |editor, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.text(cx),
|
editor.text(cx),
|
||||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
|
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
|
||||||
);
|
);
|
||||||
assert!(editor.has_visible_completions_menu());
|
assert!(editor.has_visible_completions_menu());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crease_ranges(editor, cx),
|
crease_ranges(editor, cx),
|
||||||
vec![
|
vec![
|
||||||
Point::new(0, 6)..Point::new(0, 37),
|
Point::new(0, 6)..Point::new(0, 37),
|
||||||
Point::new(0, 44)..Point::new(0, 71)
|
Point::new(0, 44)..Point::new(0, 79)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1031,15 +1058,15 @@ mod tests {
|
||||||
editor.update(&mut cx, |editor, cx| {
|
editor.update(&mut cx, |editor, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.text(cx),
|
editor.text(cx),
|
||||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
|
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
|
||||||
);
|
);
|
||||||
assert!(!editor.has_visible_completions_menu());
|
assert!(!editor.has_visible_completions_menu());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crease_ranges(editor, cx),
|
crease_ranges(editor, cx),
|
||||||
vec![
|
vec![
|
||||||
Point::new(0, 6)..Point::new(0, 37),
|
Point::new(0, 6)..Point::new(0, 37),
|
||||||
Point::new(0, 44)..Point::new(0, 71),
|
Point::new(0, 44)..Point::new(0, 79),
|
||||||
Point::new(1, 0)..Point::new(1, 35)
|
Point::new(1, 0)..Point::new(1, 31)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: WeakEntity<ContextStore>,
|
context_store: WeakEntity<ContextStore>,
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
matches: Vec<PathMatch>,
|
matches: Vec<FileMatch>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
// TODO: This should be probably be run in the background.
|
// TODO: This should be probably be run in the background.
|
||||||
|
@ -128,7 +128,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let path_match = &self.matches[ix];
|
let FileMatch { mat, .. } = &self.matches[ix];
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
|
@ -189,9 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(render_file_context_entry(
|
.child(render_file_context_entry(
|
||||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||||
&path_match.path,
|
&mat.path,
|
||||||
&path_match.path_prefix,
|
&mat.path_prefix,
|
||||||
path_match.is_dir,
|
mat.is_dir,
|
||||||
self.context_store.clone(),
|
self.context_store.clone(),
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
|
@ -199,12 +199,17 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn search_paths(
|
pub struct FileMatch {
|
||||||
|
pub mat: PathMatch,
|
||||||
|
pub is_recent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_files(
|
||||||
query: String,
|
query: String,
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Vec<PathMatch>> {
|
) -> Task<Vec<FileMatch>> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
|
@ -213,28 +218,34 @@ pub(crate) fn search_paths(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(project_path, _)| {
|
.filter_map(|(project_path, _)| {
|
||||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
Some(PathMatch {
|
Some(FileMatch {
|
||||||
score: 0.,
|
mat: PathMatch {
|
||||||
positions: Vec::new(),
|
score: 0.,
|
||||||
worktree_id: project_path.worktree_id.to_usize(),
|
positions: Vec::new(),
|
||||||
path: project_path.path,
|
worktree_id: project_path.worktree_id.to_usize(),
|
||||||
path_prefix: worktree.read(cx).root_name().into(),
|
path: project_path.path,
|
||||||
distance_to_relative_ancestor: 0,
|
path_prefix: worktree.read(cx).root_name().into(),
|
||||||
is_dir: false,
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
|
},
|
||||||
|
is_recent: true,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||||
let worktree = worktree.read(cx);
|
let worktree = worktree.read(cx);
|
||||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||||
worktree.entries(false, 0).map(move |entry| PathMatch {
|
worktree.entries(false, 0).map(move |entry| FileMatch {
|
||||||
score: 0.,
|
mat: PathMatch {
|
||||||
positions: Vec::new(),
|
score: 0.,
|
||||||
worktree_id: worktree.id().to_usize(),
|
positions: Vec::new(),
|
||||||
path: entry.path.clone(),
|
worktree_id: worktree.id().to_usize(),
|
||||||
path_prefix: path_prefix.clone(),
|
path: entry.path.clone(),
|
||||||
distance_to_relative_ancestor: 0,
|
path_prefix: path_prefix.clone(),
|
||||||
is_dir: entry.is_dir(),
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: entry.is_dir(),
|
||||||
|
},
|
||||||
|
is_recent: false,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -269,6 +280,12 @@ pub(crate) fn search_paths(
|
||||||
executor,
|
executor,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| FileMatch {
|
||||||
|
mat,
|
||||||
|
is_recent: false,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::cmp::Reverse;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::Result;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||||
|
@ -119,11 +119,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||||
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
let context_store = self.context_store.clone();
|
let context_store = self.context_store.clone();
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let symbols = search_task
|
let symbols = search_task.await;
|
||||||
.await
|
|
||||||
.context("Failed to load symbols")
|
|
||||||
.log_err()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let symbol_entries = context_store
|
let symbol_entries = context_store
|
||||||
.read_with(cx, |context_store, cx| {
|
.read_with(cx, |context_store, cx| {
|
||||||
|
@ -285,12 +281,16 @@ fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Optio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SymbolMatch {
|
||||||
|
pub symbol: Symbol,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn search_symbols(
|
pub(crate) fn search_symbols(
|
||||||
query: String,
|
query: String,
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
|
) -> Task<Vec<SymbolMatch>> {
|
||||||
let symbols_task = workspace.update(cx, |workspace, cx| {
|
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
.project()
|
.project()
|
||||||
|
@ -298,19 +298,28 @@ pub(crate) fn search_symbols(
|
||||||
});
|
});
|
||||||
let project = workspace.read(cx).project().clone();
|
let project = workspace.read(cx).project().clone();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let symbols = symbols_task.await?;
|
let Some(symbols) = symbols_task.await.log_err() else {
|
||||||
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
|
return Vec::new();
|
||||||
.update(cx, |project, cx| {
|
};
|
||||||
symbols
|
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
|
||||||
.iter()
|
project
|
||||||
.enumerate()
|
.update(cx, |project, cx| {
|
||||||
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
|
symbols
|
||||||
.partition(|candidate| {
|
.iter()
|
||||||
project
|
.enumerate()
|
||||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
.map(|(id, symbol)| {
|
||||||
.map_or(false, |e| !e.is_ignored)
|
StringMatchCandidate::new(id, &symbol.label.filter_text())
|
||||||
})
|
})
|
||||||
})?;
|
.partition(|candidate| {
|
||||||
|
project
|
||||||
|
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||||
|
.map_or(false, |e| !e.is_ignored)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_MATCHES: usize = 100;
|
const MAX_MATCHES: usize = 100;
|
||||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
@ -339,7 +348,7 @@ pub(crate) fn search_symbols(
|
||||||
let mut matches = visible_matches;
|
let mut matches = visible_matches;
|
||||||
matches.append(&mut external_matches);
|
matches.append(&mut external_matches);
|
||||||
|
|
||||||
Ok(matches
|
matches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut mat| {
|
.map(|mut mat| {
|
||||||
let symbol = symbols[mat.candidate_id].clone();
|
let symbol = symbols[mat.candidate_id].clone();
|
||||||
|
@ -347,19 +356,19 @@ pub(crate) fn search_symbols(
|
||||||
for position in &mut mat.positions {
|
for position in &mut mat.positions {
|
||||||
*position += filter_start;
|
*position += filter_start;
|
||||||
}
|
}
|
||||||
(mat, symbol)
|
SymbolMatch { symbol }
|
||||||
})
|
})
|
||||||
.collect())
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_symbol_entries(
|
fn compute_symbol_entries(
|
||||||
symbols: Vec<(StringMatch, Symbol)>,
|
symbols: Vec<SymbolMatch>,
|
||||||
context_store: &ContextStore,
|
context_store: &ContextStore,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Vec<SymbolEntry> {
|
) -> Vec<SymbolEntry> {
|
||||||
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||||
for (_, symbol) in symbols {
|
for SymbolMatch { symbol, .. } in symbols {
|
||||||
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||||
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||||
let mut is_included = false;
|
let mut is_included = false;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||||
|
@ -114,11 +115,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_task = search_threads(query, threads, cx);
|
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
|
||||||
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| {
|
||||||
this.delegate.matches = matches;
|
this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
|
||||||
this.delegate.selected_index = 0;
|
this.delegate.selected_index = 0;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
|
@ -217,11 +218,18 @@ pub fn render_thread_context_entry(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ThreadMatch {
|
||||||
|
pub thread: ThreadContextEntry,
|
||||||
|
pub is_recent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn search_threads(
|
pub(crate) fn search_threads(
|
||||||
query: String,
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Vec<ThreadContextEntry>> {
|
) -> Task<Vec<ThreadMatch>> {
|
||||||
let threads = thread_store.update(cx, |this, _cx| {
|
let threads = thread_store.update(cx, |this, _cx| {
|
||||||
this.threads()
|
this.threads()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -236,6 +244,12 @@ pub(crate) fn search_threads(
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
threads
|
threads
|
||||||
|
.into_iter()
|
||||||
|
.map(|thread| ThreadMatch {
|
||||||
|
thread,
|
||||||
|
is_recent: false,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let candidates = threads
|
let candidates = threads
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -247,14 +261,17 @@ pub(crate) fn search_threads(
|
||||||
&query,
|
&query,
|
||||||
false,
|
false,
|
||||||
100,
|
100,
|
||||||
&Default::default(),
|
&cancellation_flag,
|
||||||
executor,
|
executor,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
matches
|
matches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mat| threads[mat.candidate_id].clone())
|
.map(|mat| ThreadMatch {
|
||||||
|
thread: threads[mat.candidate_id].clone(),
|
||||||
|
is_recent: false,
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,14 +3,16 @@ use std::sync::Arc;
|
||||||
use crate::assistant_model_selector::ModelType;
|
use crate::assistant_model_selector::ModelType;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::actions::MoveUp;
|
use editor::actions::MoveUp;
|
||||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
use editor::{
|
||||||
|
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer,
|
||||||
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::{Buffer, Language};
|
||||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||||
use language_model_selector::ToggleModelSelector;
|
use language_model_selector::ToggleModelSelector;
|
||||||
use multi_buffer;
|
use multi_buffer;
|
||||||
|
@ -66,8 +68,24 @@ impl MessageEditor {
|
||||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
let language = Language::new(
|
||||||
|
language::LanguageConfig {
|
||||||
|
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
let mut editor = Editor::auto_height(10, window, cx);
|
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||||
|
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
let mut editor = Editor::new(
|
||||||
|
editor::EditorMode::AutoHeight { max_lines: 10 },
|
||||||
|
buffer,
|
||||||
|
None,
|
||||||
|
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 {
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
|
@ -75,7 +93,6 @@ impl MessageEditor {
|
||||||
max_entries_visible: 12,
|
max_entries_visible: 12,
|
||||||
placement: Some(ContextMenuPlacement::Above),
|
placement: Some(ContextMenuPlacement::Above),
|
||||||
});
|
});
|
||||||
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue