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 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::thread_store::ThreadStore;
|
||||
|
||||
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::{
|
||||
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 {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
|
@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider {
|
|||
}
|
||||
}
|
||||
|
||||
fn default_completions(
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
editor: Entity<Editor>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Vec<Completion> {
|
||||
let mut completions = Vec::new();
|
||||
|
||||
completions.extend(
|
||||
recent_context_picker_entries(
|
||||
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);
|
||||
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
|
||||
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)),
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
fn completion_for_thread(
|
||||
|
@ -261,11 +301,8 @@ impl ContextPickerCompletionProvider {
|
|||
path_prefix,
|
||||
);
|
||||
|
||||
let label = Self::build_code_label_for_full_path(
|
||||
&file_name,
|
||||
directory.as_ref().map(|s| s.as_ref()),
|
||||
cx,
|
||||
);
|
||||
let label =
|
||||
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
} 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 {
|
||||
fn completions(
|
||||
&self,
|
||||
|
@ -404,10 +457,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
return Task::ready(Ok(None));
|
||||
};
|
||||
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
let Some(context_store) = self.context_store.upgrade() else {
|
||||
let Some((workspace, context_store)) =
|
||||
self.workspace.upgrade().zip(self.context_store.upgrade())
|
||||
else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
|
||||
|
@ -419,154 +471,89 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
let editor = self.editor.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| {
|
||||
let mut completions = Vec::new();
|
||||
let matches = search_task.await;
|
||||
let Some(editor) = editor.upgrade() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
match mode {
|
||||
Some(ContextPickerMode::File) => {
|
||||
let path_matches = cx
|
||||
.update(|cx| {
|
||||
super::file_context_picker::search_paths(
|
||||
query,
|
||||
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,
|
||||
)
|
||||
Ok(Some(cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| match mat {
|
||||
Match::File(FileMatch { mat, is_recent }) => {
|
||||
Some(Self::completion_for_path(
|
||||
ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
},
|
||||
));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Fetch) => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
if !query.is_empty() {
|
||||
completions.push(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
query.into(),
|
||||
&mat.path_prefix,
|
||||
is_recent,
|
||||
mat.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.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,
|
||||
));
|
||||
))
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(Some(completions))
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
|
||||
symbol,
|
||||
excerpt_id,
|
||||
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;
|
||||
if let Some(mode_text) = parts.next() {
|
||||
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()) {
|
||||
Some(whitespace_count) => {
|
||||
if let Some(argument_text) = parts.next() {
|
||||
|
@ -702,13 +694,13 @@ impl MentionCompletion {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{Focusable, TestAppContext, VisualTestContext};
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
|
||||
use project::{Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{ops::Deref, path::PathBuf};
|
||||
use std::ops::Deref;
|
||||
use util::{path, separator};
|
||||
use workspace::AppState;
|
||||
use workspace::{AppState, Item};
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
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]
|
||||
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -846,25 +871,27 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
let item = workspace
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: PathBuf::from("editor").into(),
|
||||
},
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::new(
|
||||
editor::EditorMode::Full,
|
||||
multi_buffer::MultiBuffer::build_simple("", cx),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|_, cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
|
||||
|
@ -895,10 +922,10 @@ mod tests {
|
|||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
"editor dir/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Fetch"
|
||||
|
@ -993,14 +1020,14 @@ mod tests {
|
|||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
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_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
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| {
|
||||
assert_eq!(
|
||||
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_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
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| {
|
||||
assert_eq!(
|
||||
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_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71),
|
||||
Point::new(1, 0)..Point::new(1, 35)
|
||||
Point::new(0, 44)..Point::new(0, 79),
|
||||
Point::new(1, 0)..Point::new(1, 31)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue