agent2: Support directories in @file mentions (#36416)
Release Notes: - N/A
This commit is contained in:
parent
821e97a392
commit
7bcea7dc2c
5 changed files with 325 additions and 168 deletions
|
@ -15,7 +15,9 @@ use url::Url;
|
||||||
pub enum MentionUri {
|
pub enum MentionUri {
|
||||||
File {
|
File {
|
||||||
abs_path: PathBuf,
|
abs_path: PathBuf,
|
||||||
is_directory: bool,
|
},
|
||||||
|
Directory {
|
||||||
|
abs_path: PathBuf,
|
||||||
},
|
},
|
||||||
Symbol {
|
Symbol {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
@ -79,14 +81,14 @@ impl MentionUri {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let file_path =
|
let abs_path =
|
||||||
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
|
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
|
||||||
let is_directory = input.ends_with("/");
|
|
||||||
|
|
||||||
Ok(Self::File {
|
if input.ends_with("/") {
|
||||||
abs_path: file_path,
|
Ok(Self::Directory { abs_path })
|
||||||
is_directory,
|
} else {
|
||||||
})
|
Ok(Self::File { abs_path })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"zed" => {
|
"zed" => {
|
||||||
|
@ -120,7 +122,7 @@ impl MentionUri {
|
||||||
|
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
MentionUri::File { abs_path, .. } => abs_path
|
MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
|
@ -138,18 +140,11 @@ impl MentionUri {
|
||||||
|
|
||||||
pub fn icon_path(&self, cx: &mut App) -> SharedString {
|
pub fn icon_path(&self, cx: &mut App) -> SharedString {
|
||||||
match self {
|
match self {
|
||||||
MentionUri::File {
|
MentionUri::File { abs_path } => {
|
||||||
abs_path,
|
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||||
is_directory,
|
|
||||||
} => {
|
|
||||||
if *is_directory {
|
|
||||||
FileIcons::get_folder_icon(false, cx)
|
|
||||||
.unwrap_or_else(|| IconName::Folder.path().into())
|
|
||||||
} else {
|
|
||||||
FileIcons::get_icon(abs_path, cx)
|
|
||||||
.unwrap_or_else(|| IconName::File.path().into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
|
||||||
|
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||||
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
|
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
|
||||||
|
@ -165,13 +160,16 @@ impl MentionUri {
|
||||||
|
|
||||||
pub fn to_uri(&self) -> Url {
|
pub fn to_uri(&self) -> Url {
|
||||||
match self {
|
match self {
|
||||||
MentionUri::File {
|
MentionUri::File { abs_path } => {
|
||||||
abs_path,
|
let mut url = Url::parse("file:///").unwrap();
|
||||||
is_directory,
|
let path = abs_path.to_string_lossy();
|
||||||
} => {
|
url.set_path(&path);
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Directory { abs_path } => {
|
||||||
let mut url = Url::parse("file:///").unwrap();
|
let mut url = Url::parse("file:///").unwrap();
|
||||||
let mut path = abs_path.to_string_lossy().to_string();
|
let mut path = abs_path.to_string_lossy().to_string();
|
||||||
if *is_directory && !path.ends_with("/") {
|
if !path.ends_with("/") {
|
||||||
path.push_str("/");
|
path.push_str("/");
|
||||||
}
|
}
|
||||||
url.set_path(&path);
|
url.set_path(&path);
|
||||||
|
@ -274,12 +272,8 @@ mod tests {
|
||||||
let file_uri = "file:///path/to/file.rs";
|
let file_uri = "file:///path/to/file.rs";
|
||||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||||
match &parsed {
|
match &parsed {
|
||||||
MentionUri::File {
|
MentionUri::File { abs_path } => {
|
||||||
abs_path,
|
|
||||||
is_directory,
|
|
||||||
} => {
|
|
||||||
assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
|
assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
|
||||||
assert!(!is_directory);
|
|
||||||
}
|
}
|
||||||
_ => panic!("Expected File variant"),
|
_ => panic!("Expected File variant"),
|
||||||
}
|
}
|
||||||
|
@ -291,32 +285,26 @@ mod tests {
|
||||||
let file_uri = "file:///path/to/dir/";
|
let file_uri = "file:///path/to/dir/";
|
||||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||||
match &parsed {
|
match &parsed {
|
||||||
MentionUri::File {
|
MentionUri::Directory { abs_path } => {
|
||||||
abs_path,
|
|
||||||
is_directory,
|
|
||||||
} => {
|
|
||||||
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
|
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
|
||||||
assert!(is_directory);
|
|
||||||
}
|
}
|
||||||
_ => panic!("Expected File variant"),
|
_ => panic!("Expected Directory variant"),
|
||||||
}
|
}
|
||||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_directory_uri_with_slash() {
|
fn test_to_directory_uri_with_slash() {
|
||||||
let uri = MentionUri::File {
|
let uri = MentionUri::Directory {
|
||||||
abs_path: PathBuf::from("/path/to/dir/"),
|
abs_path: PathBuf::from("/path/to/dir/"),
|
||||||
is_directory: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
|
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_directory_uri_without_slash() {
|
fn test_to_directory_uri_without_slash() {
|
||||||
let uri = MentionUri::File {
|
let uri = MentionUri::Directory {
|
||||||
abs_path: PathBuf::from("/path/to/dir"),
|
abs_path: PathBuf::from("/path/to/dir"),
|
||||||
is_directory: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
|
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,7 @@ impl UserMessage {
|
||||||
They are up-to-date and don't need to be re-read.\n\n";
|
They are up-to-date and don't need to be re-read.\n\n";
|
||||||
|
|
||||||
const OPEN_FILES_TAG: &str = "<files>";
|
const OPEN_FILES_TAG: &str = "<files>";
|
||||||
|
const OPEN_DIRECTORIES_TAG: &str = "<directories>";
|
||||||
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
|
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
|
||||||
const OPEN_THREADS_TAG: &str = "<threads>";
|
const OPEN_THREADS_TAG: &str = "<threads>";
|
||||||
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
|
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
|
||||||
|
@ -153,6 +154,7 @@ impl UserMessage {
|
||||||
"<rules>\nThe user has specified the following rules that should be applied:\n";
|
"<rules>\nThe user has specified the following rules that should be applied:\n";
|
||||||
|
|
||||||
let mut file_context = OPEN_FILES_TAG.to_string();
|
let mut file_context = OPEN_FILES_TAG.to_string();
|
||||||
|
let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
|
||||||
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
|
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
|
||||||
let mut thread_context = OPEN_THREADS_TAG.to_string();
|
let mut thread_context = OPEN_THREADS_TAG.to_string();
|
||||||
let mut fetch_context = OPEN_FETCH_TAG.to_string();
|
let mut fetch_context = OPEN_FETCH_TAG.to_string();
|
||||||
|
@ -168,7 +170,7 @@ impl UserMessage {
|
||||||
}
|
}
|
||||||
UserMessageContent::Mention { uri, content } => {
|
UserMessageContent::Mention { uri, content } => {
|
||||||
match uri {
|
match uri {
|
||||||
MentionUri::File { abs_path, .. } => {
|
MentionUri::File { abs_path } => {
|
||||||
write!(
|
write!(
|
||||||
&mut symbol_context,
|
&mut symbol_context,
|
||||||
"\n{}",
|
"\n{}",
|
||||||
|
@ -179,6 +181,9 @@ impl UserMessage {
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
MentionUri::Directory { .. } => {
|
||||||
|
write!(&mut directory_context, "\n{}\n", content).ok();
|
||||||
|
}
|
||||||
MentionUri::Symbol {
|
MentionUri::Symbol {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
}
|
}
|
||||||
|
@ -233,6 +238,13 @@ impl UserMessage {
|
||||||
.push(language_model::MessageContent::Text(file_context));
|
.push(language_model::MessageContent::Text(file_context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
|
||||||
|
directory_context.push_str("</directories>\n");
|
||||||
|
message
|
||||||
|
.content
|
||||||
|
.push(language_model::MessageContent::Text(directory_context));
|
||||||
|
}
|
||||||
|
|
||||||
if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
|
if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
|
||||||
symbol_context.push_str("</symbols>\n");
|
symbol_context.push_str("</symbols>\n");
|
||||||
message
|
message
|
||||||
|
|
|
@ -445,19 +445,20 @@ impl ContextPickerCompletionProvider {
|
||||||
|
|
||||||
let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
|
let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
|
||||||
|
|
||||||
let file_uri = MentionUri::File {
|
let uri = if is_directory {
|
||||||
abs_path,
|
MentionUri::Directory { abs_path }
|
||||||
is_directory,
|
} else {
|
||||||
|
MentionUri::File { abs_path }
|
||||||
};
|
};
|
||||||
|
|
||||||
let crease_icon_path = file_uri.icon_path(cx);
|
let crease_icon_path = uri.icon_path(cx);
|
||||||
let completion_icon_path = if is_recent {
|
let completion_icon_path = if is_recent {
|
||||||
IconName::HistoryRerun.path().into()
|
IconName::HistoryRerun.path().into()
|
||||||
} else {
|
} else {
|
||||||
crease_icon_path.clone()
|
crease_icon_path.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_text = format!("{} ", file_uri.as_link());
|
let new_text = format!("{} ", uri.as_link());
|
||||||
let new_text_len = new_text.len();
|
let new_text_len = new_text.len();
|
||||||
Some(Completion {
|
Some(Completion {
|
||||||
replace_range: source_range.clone(),
|
replace_range: source_range.clone(),
|
||||||
|
@ -472,7 +473,7 @@ impl ContextPickerCompletionProvider {
|
||||||
source_range.start,
|
source_range.start,
|
||||||
new_text_len - 1,
|
new_text_len - 1,
|
||||||
message_editor,
|
message_editor,
|
||||||
file_uri,
|
uri,
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use acp_thread::{MentionUri, selection_name};
|
||||||
use agent::{TextThreadStore, ThreadId, ThreadStore};
|
use agent::{TextThreadStore, ThreadId, ThreadStore};
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use assistant_slash_commands::codeblock_fence_for_path;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||||
|
@ -15,7 +16,7 @@ use editor::{
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::{
|
||||||
FutureExt as _, TryFutureExt as _,
|
FutureExt as _, TryFutureExt as _,
|
||||||
future::{Shared, try_join_all},
|
future::{Shared, join_all, try_join_all},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
|
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
|
||||||
|
@ -23,12 +24,12 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::{Buffer, Language};
|
use language::{Buffer, Language};
|
||||||
use language_model::LanguageModelImage;
|
use language_model::LanguageModelImage;
|
||||||
use project::{CompletionIntent, Project};
|
use project::{CompletionIntent, Project, ProjectPath, Worktree};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::Write,
|
fmt::{Display, Write},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
@ -245,6 +246,9 @@ impl MessageEditor {
|
||||||
MentionUri::Fetch { url } => {
|
MentionUri::Fetch { url } => {
|
||||||
self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
|
self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
|
||||||
}
|
}
|
||||||
|
MentionUri::Directory { abs_path } => {
|
||||||
|
self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx);
|
||||||
|
}
|
||||||
MentionUri::Thread { id, name } => {
|
MentionUri::Thread { id, name } => {
|
||||||
self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx);
|
self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx);
|
||||||
}
|
}
|
||||||
|
@ -260,6 +264,124 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn confirm_mention_for_directory(
|
||||||
|
&mut self,
|
||||||
|
crease_id: CreaseId,
|
||||||
|
anchor: Anchor,
|
||||||
|
abs_path: PathBuf,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in worktree.child_entries(path) {
|
||||||
|
if entry.is_dir() {
|
||||||
|
files.extend(collect_files_in_path(worktree, &entry.path));
|
||||||
|
} else if entry.is_file() {
|
||||||
|
files.push((entry.path.clone(), worktree.full_path(&entry.path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = MentionUri::Directory {
|
||||||
|
abs_path: abs_path.clone(),
|
||||||
|
};
|
||||||
|
let Some(project_path) = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(&abs_path, cx)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let project = self.project.clone();
|
||||||
|
let task = cx.spawn(async move |_, cx| {
|
||||||
|
let directory_path = entry.path.clone();
|
||||||
|
|
||||||
|
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||||
|
let file_paths = worktree.read_with(cx, |worktree, _cx| {
|
||||||
|
collect_files_in_path(worktree, &directory_path)
|
||||||
|
})?;
|
||||||
|
let descendants_future = cx.update(|cx| {
|
||||||
|
join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
|
||||||
|
let rel_path = worktree_path
|
||||||
|
.strip_prefix(&directory_path)
|
||||||
|
.log_err()
|
||||||
|
.map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
|
||||||
|
|
||||||
|
let open_task = project.update(cx, |project, cx| {
|
||||||
|
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: worktree_path,
|
||||||
|
};
|
||||||
|
buffer_store.open_buffer(project_path, cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: report load errors instead of just logging
|
||||||
|
let rope_task = cx.spawn(async move |cx| {
|
||||||
|
let buffer = open_task.await.log_err()?;
|
||||||
|
let rope = buffer
|
||||||
|
.read_with(cx, |buffer, _cx| buffer.as_rope().clone())
|
||||||
|
.log_err()?;
|
||||||
|
Some(rope)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let rope = rope_task.await?;
|
||||||
|
Some((rel_path, full_path, rope.to_string()))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let contents = cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
let contents = descendants_future.await.into_iter().flatten();
|
||||||
|
contents.collect()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
anyhow::Ok(contents)
|
||||||
|
});
|
||||||
|
let task = cx
|
||||||
|
.spawn(async move |_, _| {
|
||||||
|
task.await
|
||||||
|
.map(|contents| DirectoryContents(contents).to_string())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
self.mention_set.directories.insert(abs_path, task.clone());
|
||||||
|
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if task.await.notify_async_err(cx).is_some() {
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
this.mention_set.insert_uri(crease_id, uri);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
editor
|
||||||
|
.update(cx, |editor, cx| {
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
|
||||||
|
});
|
||||||
|
editor.remove_creases([crease_id], cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn confirm_mention_for_fetch(
|
fn confirm_mention_for_fetch(
|
||||||
&mut self,
|
&mut self,
|
||||||
crease_id: CreaseId,
|
crease_id: CreaseId,
|
||||||
|
@ -361,6 +483,104 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn confirm_mention_for_thread(
|
||||||
|
&mut self,
|
||||||
|
crease_id: CreaseId,
|
||||||
|
anchor: Anchor,
|
||||||
|
id: ThreadId,
|
||||||
|
name: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let uri = MentionUri::Thread {
|
||||||
|
id: id.clone(),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
let open_task = self.thread_store.update(cx, |thread_store, cx| {
|
||||||
|
thread_store.open_thread(&id, window, cx)
|
||||||
|
});
|
||||||
|
let task = cx
|
||||||
|
.spawn(async move |_, cx| {
|
||||||
|
let thread = open_task.await.map_err(|e| e.to_string())?;
|
||||||
|
let content = thread
|
||||||
|
.read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(content)
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
self.mention_set.insert_thread(id, task.clone());
|
||||||
|
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if task.await.notify_async_err(cx).is_some() {
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
this.mention_set.insert_uri(crease_id, uri);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
editor
|
||||||
|
.update(cx, |editor, cx| {
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
|
||||||
|
});
|
||||||
|
editor.remove_creases([crease_id], cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm_mention_for_text_thread(
|
||||||
|
&mut self,
|
||||||
|
crease_id: CreaseId,
|
||||||
|
anchor: Anchor,
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let uri = MentionUri::TextThread {
|
||||||
|
path: path.clone(),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
|
||||||
|
text_thread_store.open_local_context(path.as_path().into(), cx)
|
||||||
|
});
|
||||||
|
let task = cx
|
||||||
|
.spawn(async move |_, cx| {
|
||||||
|
let context = context.await.map_err(|e| e.to_string())?;
|
||||||
|
let xml = context
|
||||||
|
.update(cx, |context, cx| context.to_xml(cx))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(xml)
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
self.mention_set.insert_text_thread(path, task.clone());
|
||||||
|
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if task.await.notify_async_err(cx).is_some() {
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
this.mention_set.insert_uri(crease_id, uri);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
editor
|
||||||
|
.update(cx, |editor, cx| {
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
|
||||||
|
});
|
||||||
|
editor.remove_creases([crease_id], cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contents(
|
pub fn contents(
|
||||||
&self,
|
&self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -613,13 +833,8 @@ impl MessageEditor {
|
||||||
if task.await.notify_async_err(cx).is_some() {
|
if task.await.notify_async_err(cx).is_some() {
|
||||||
if let Some(abs_path) = abs_path.clone() {
|
if let Some(abs_path) = abs_path.clone() {
|
||||||
this.update(cx, |this, _cx| {
|
this.update(cx, |this, _cx| {
|
||||||
this.mention_set.insert_uri(
|
this.mention_set
|
||||||
crease_id,
|
.insert_uri(crease_id, MentionUri::File { abs_path });
|
||||||
MentionUri::File {
|
|
||||||
abs_path,
|
|
||||||
is_directory: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -637,104 +852,6 @@ impl MessageEditor {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_mention_for_thread(
|
|
||||||
&mut self,
|
|
||||||
crease_id: CreaseId,
|
|
||||||
anchor: Anchor,
|
|
||||||
id: ThreadId,
|
|
||||||
name: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let uri = MentionUri::Thread {
|
|
||||||
id: id.clone(),
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
let open_task = self.thread_store.update(cx, |thread_store, cx| {
|
|
||||||
thread_store.open_thread(&id, window, cx)
|
|
||||||
});
|
|
||||||
let task = cx
|
|
||||||
.spawn(async move |_, cx| {
|
|
||||||
let thread = open_task.await.map_err(|e| e.to_string())?;
|
|
||||||
let content = thread
|
|
||||||
.read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(content)
|
|
||||||
})
|
|
||||||
.shared();
|
|
||||||
|
|
||||||
self.mention_set.insert_thread(id, task.clone());
|
|
||||||
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if task.await.notify_async_err(cx).is_some() {
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
this.mention_set.insert_uri(crease_id, uri);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
editor
|
|
||||||
.update(cx, |editor, cx| {
|
|
||||||
editor.display_map.update(cx, |display_map, cx| {
|
|
||||||
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
|
|
||||||
});
|
|
||||||
editor.remove_creases([crease_id], cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_mention_for_text_thread(
|
|
||||||
&mut self,
|
|
||||||
crease_id: CreaseId,
|
|
||||||
anchor: Anchor,
|
|
||||||
path: PathBuf,
|
|
||||||
name: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let uri = MentionUri::TextThread {
|
|
||||||
path: path.clone(),
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
|
|
||||||
text_thread_store.open_local_context(path.as_path().into(), cx)
|
|
||||||
});
|
|
||||||
let task = cx
|
|
||||||
.spawn(async move |_, cx| {
|
|
||||||
let context = context.await.map_err(|e| e.to_string())?;
|
|
||||||
let xml = context
|
|
||||||
.update(cx, |context, cx| context.to_xml(cx))
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(xml)
|
|
||||||
})
|
|
||||||
.shared();
|
|
||||||
|
|
||||||
self.mention_set.insert_text_thread(path, task.clone());
|
|
||||||
|
|
||||||
let editor = self.editor.clone();
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if task.await.notify_async_err(cx).is_some() {
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
this.mention_set.insert_uri(crease_id, uri);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
editor
|
|
||||||
.update(cx, |editor, cx| {
|
|
||||||
editor.display_map.update(cx, |display_map, cx| {
|
|
||||||
display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
|
|
||||||
});
|
|
||||||
editor.remove_creases([crease_id], cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
|
pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.set_mode(mode);
|
editor.set_mode(mode);
|
||||||
|
@ -817,6 +934,10 @@ impl MessageEditor {
|
||||||
self.mention_set
|
self.mention_set
|
||||||
.add_fetch_result(url, Task::ready(Ok(text)).shared());
|
.add_fetch_result(url, Task::ready(Ok(text)).shared());
|
||||||
}
|
}
|
||||||
|
MentionUri::Directory { abs_path } => {
|
||||||
|
let task = Task::ready(Ok(text)).shared();
|
||||||
|
self.mention_set.directories.insert(abs_path, task);
|
||||||
|
}
|
||||||
MentionUri::File { .. }
|
MentionUri::File { .. }
|
||||||
| MentionUri::Symbol { .. }
|
| MentionUri::Symbol { .. }
|
||||||
| MentionUri::Rule { .. }
|
| MentionUri::Rule { .. }
|
||||||
|
@ -882,6 +1003,18 @@ impl MessageEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>);
|
||||||
|
|
||||||
|
impl Display for DirectoryContents {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for (_relative_path, full_path, content) in self.0.iter() {
|
||||||
|
let fence = codeblock_fence_for_path(Some(full_path), None);
|
||||||
|
write!(f, "\n{fence}\n{content}\n```")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Focusable for MessageEditor {
|
impl Focusable for MessageEditor {
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
self.editor.focus_handle(cx)
|
self.editor.focus_handle(cx)
|
||||||
|
@ -1064,6 +1197,7 @@ pub struct MentionSet {
|
||||||
images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
|
images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
|
||||||
thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
|
thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
|
||||||
text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
|
text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
|
||||||
|
directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MentionSet {
|
impl MentionSet {
|
||||||
|
@ -1116,7 +1250,6 @@ impl MentionSet {
|
||||||
.map(|(&crease_id, uri)| {
|
.map(|(&crease_id, uri)| {
|
||||||
match uri {
|
match uri {
|
||||||
MentionUri::File { abs_path, .. } => {
|
MentionUri::File { abs_path, .. } => {
|
||||||
// TODO directories
|
|
||||||
let uri = uri.clone();
|
let uri = uri.clone();
|
||||||
let abs_path = abs_path.to_path_buf();
|
let abs_path = abs_path.to_path_buf();
|
||||||
|
|
||||||
|
@ -1141,6 +1274,24 @@ impl MentionSet {
|
||||||
anyhow::Ok((crease_id, Mention::Text { uri, content }))
|
anyhow::Ok((crease_id, Mention::Text { uri, content }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
MentionUri::Directory { abs_path } => {
|
||||||
|
let Some(content) = self.directories.get(abs_path).cloned() else {
|
||||||
|
return Task::ready(Err(anyhow!("missing directory load task")));
|
||||||
|
};
|
||||||
|
let uri = uri.clone();
|
||||||
|
cx.spawn(async move |_| {
|
||||||
|
Ok((
|
||||||
|
crease_id,
|
||||||
|
Mention::Text {
|
||||||
|
uri,
|
||||||
|
content: content
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
MentionUri::Symbol {
|
MentionUri::Symbol {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
}
|
}
|
||||||
|
|
|
@ -2790,25 +2790,30 @@ impl AcpThreadView {
|
||||||
|
|
||||||
if let Some(mention) = MentionUri::parse(&url).log_err() {
|
if let Some(mention) = MentionUri::parse(&url).log_err() {
|
||||||
workspace.update(cx, |workspace, cx| match mention {
|
workspace.update(cx, |workspace, cx| match mention {
|
||||||
MentionUri::File { abs_path, .. } => {
|
MentionUri::File { abs_path } => {
|
||||||
let project = workspace.project();
|
let project = workspace.project();
|
||||||
let Some((path, entry)) = project.update(cx, |project, cx| {
|
let Some(path) =
|
||||||
|
project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.open_path(path, None, true, window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
MentionUri::Directory { abs_path } => {
|
||||||
|
let project = workspace.project();
|
||||||
|
let Some(entry) = project.update(cx, |project, cx| {
|
||||||
let path = project.find_project_path(abs_path, cx)?;
|
let path = project.find_project_path(abs_path, cx)?;
|
||||||
let entry = project.entry_for_path(&path, cx)?;
|
project.entry_for_path(&path, cx)
|
||||||
Some((path, entry))
|
|
||||||
}) else {
|
}) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if entry.is_dir() {
|
project.update(cx, |_, cx| {
|
||||||
project.update(cx, |_, cx| {
|
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
workspace
|
|
||||||
.open_path(path, None, true, window, cx)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MentionUri::Symbol {
|
MentionUri::Symbol {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue