Detailed changes
@@ -7,6 +7,7 @@ name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
+ "agent",
"agent-client-protocol",
"anyhow",
"buffer_diff",
@@ -21,6 +22,7 @@ dependencies = [
"markdown",
"parking_lot",
"project",
+ "prompt_store",
"rand 0.8.5",
"serde",
"serde_json",
@@ -392,6 +394,7 @@ dependencies = [
"ui",
"ui_input",
"unindent",
+ "url",
"urlencoding",
"util",
"uuid",
@@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
+agent.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
@@ -28,6 +29,7 @@ itertools.workspace = true
language.workspace = true
markdown.workspace = true
project.workspace = true
+prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -399,7 +399,7 @@ impl ContentBlock {
}
}
- let new_content = self.extract_content_from_block(block);
+ let new_content = self.block_string_contents(block);
match self {
ContentBlock::Empty => {
@@ -409,7 +409,7 @@ impl ContentBlock {
markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
}
ContentBlock::ResourceLink { resource_link } => {
- let existing_content = Self::resource_link_to_content(&resource_link.uri);
+ let existing_content = Self::resource_link_md(&resource_link.uri);
let combined = format!("{}\n{}", existing_content, new_content);
*self = Self::create_markdown_block(combined, language_registry, cx);
@@ -417,14 +417,6 @@ impl ContentBlock {
}
}
- fn resource_link_to_content(uri: &str) -> String {
- if let Some(uri) = MentionUri::parse(&uri).log_err() {
- uri.to_link()
- } else {
- uri.to_string().clone()
- }
- }
-
fn create_markdown_block(
content: String,
language_registry: &Arc<LanguageRegistry>,
@@ -436,11 +428,11 @@ impl ContentBlock {
}
}
- fn extract_content_from_block(&self, block: acp::ContentBlock) -> String {
+ fn block_string_contents(&self, block: acp::ContentBlock) -> String {
match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
acp::ContentBlock::ResourceLink(resource_link) => {
- Self::resource_link_to_content(&resource_link.uri)
+ Self::resource_link_md(&resource_link.uri)
}
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource:
@@ -449,13 +441,21 @@ impl ContentBlock {
..
}),
..
- }) => Self::resource_link_to_content(&uri),
+ }) => Self::resource_link_md(&uri),
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(),
}
}
+ fn resource_link_md(uri: &str) -> String {
+ if let Some(uri) = MentionUri::parse(&uri).log_err() {
+ uri.as_link().to_string()
+ } else {
+ uri.to_string()
+ }
+ }
+
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self {
ContentBlock::Empty => "",
@@ -1,13 +1,40 @@
-use agent_client_protocol as acp;
-use anyhow::{Result, bail};
-use std::path::PathBuf;
+use agent::ThreadId;
+use anyhow::{Context as _, Result, bail};
+use prompt_store::{PromptId, UserPromptId};
+use std::{
+ fmt,
+ ops::Range,
+ path::{Path, PathBuf},
+};
+use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
File(PathBuf),
- Symbol(PathBuf, String),
- Thread(acp::SessionId),
- Rule(String),
+ Symbol {
+ path: PathBuf,
+ name: String,
+ line_range: Range<u32>,
+ },
+ Thread {
+ id: ThreadId,
+ name: String,
+ },
+ TextThread {
+ path: PathBuf,
+ name: String,
+ },
+ Rule {
+ id: PromptId,
+ name: String,
+ },
+ Selection {
+ path: PathBuf,
+ line_range: Range<u32>,
+ },
+ Fetch {
+ url: Url,
+ },
}
impl MentionUri {
@@ -17,7 +44,34 @@ impl MentionUri {
match url.scheme() {
"file" => {
if let Some(fragment) = url.fragment() {
- Ok(Self::Symbol(path.into(), fragment.into()))
+ let range = fragment
+ .strip_prefix("L")
+ .context("Line range must start with \"L\"")?;
+ let (start, end) = range
+ .split_once(":")
+ .context("Line range must use colon as separator")?;
+ let line_range = start
+ .parse::<u32>()
+ .context("Parsing line range start")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?
+ ..end
+ .parse::<u32>()
+ .context("Parsing line range end")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?;
+ if let Some(name) = single_query_param(&url, "symbol")? {
+ Ok(Self::Symbol {
+ name,
+ path: path.into(),
+ line_range,
+ })
+ } else {
+ Ok(Self::Selection {
+ path: path.into(),
+ line_range,
+ })
+ }
} else {
let file_path =
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
@@ -26,100 +80,292 @@ impl MentionUri {
}
}
"zed" => {
- if let Some(thread) = path.strip_prefix("/agent/thread/") {
- Ok(Self::Thread(acp::SessionId(thread.into())))
- } else if let Some(rule) = path.strip_prefix("/agent/rule/") {
- Ok(Self::Rule(rule.into()))
+ if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
+ let name = single_query_param(&url, "name")?.context("Missing thread name")?;
+ Ok(Self::Thread {
+ id: thread_id.into(),
+ name,
+ })
+ } else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
+ let name = single_query_param(&url, "name")?.context("Missing thread name")?;
+ Ok(Self::TextThread {
+ path: path.into(),
+ name,
+ })
+ } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
+ let name = single_query_param(&url, "name")?.context("Missing rule name")?;
+ let rule_id = UserPromptId(rule_id.parse()?);
+ Ok(Self::Rule {
+ id: rule_id.into(),
+ name,
+ })
} else {
bail!("invalid zed url: {:?}", input);
}
}
+ "http" | "https" => Ok(MentionUri::Fetch { url }),
other => bail!("unrecognized scheme {:?}", other),
}
}
- pub fn name(&self) -> String {
+ fn name(&self) -> String {
match self {
- MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
- MentionUri::Symbol(_path, name) => name.clone(),
- MentionUri::Thread(thread) => thread.to_string(),
- MentionUri::Rule(rule) => rule.clone(),
+ MentionUri::File(path) => path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned(),
+ MentionUri::Symbol { name, .. } => name.clone(),
+ MentionUri::Thread { name, .. } => name.clone(),
+ MentionUri::TextThread { name, .. } => name.clone(),
+ MentionUri::Rule { name, .. } => name.clone(),
+ MentionUri::Selection {
+ path, line_range, ..
+ } => selection_name(path, line_range),
+ MentionUri::Fetch { url } => url.to_string(),
}
}
- pub fn to_link(&self) -> String {
- let name = self.name();
- let uri = self.to_uri();
- format!("[{name}]({uri})")
+ pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
+ MentionLink(self)
}
- pub fn to_uri(&self) -> String {
+ pub fn to_uri(&self) -> Url {
match self {
MentionUri::File(path) => {
- format!("file://{}", path.display())
+ let mut url = Url::parse("file:///").unwrap();
+ url.set_path(&path.to_string_lossy());
+ url
}
- MentionUri::Symbol(path, name) => {
- format!("file://{}#{}", path.display(), name)
+ MentionUri::Symbol {
+ path,
+ name,
+ line_range,
+ } => {
+ let mut url = Url::parse("file:///").unwrap();
+ url.set_path(&path.to_string_lossy());
+ url.query_pairs_mut().append_pair("symbol", name);
+ url.set_fragment(Some(&format!(
+ "L{}:{}",
+ line_range.start + 1,
+ line_range.end + 1
+ )));
+ url
}
- MentionUri::Thread(thread) => {
- format!("zed:///agent/thread/{}", thread.0)
+ MentionUri::Selection { path, line_range } => {
+ let mut url = Url::parse("file:///").unwrap();
+ url.set_path(&path.to_string_lossy());
+ url.set_fragment(Some(&format!(
+ "L{}:{}",
+ line_range.start + 1,
+ line_range.end + 1
+ )));
+ url
}
- MentionUri::Rule(rule) => {
- format!("zed:///agent/rule/{}", rule)
+ MentionUri::Thread { name, id } => {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path(&format!("/agent/thread/{id}"));
+ url.query_pairs_mut().append_pair("name", name);
+ url
}
+ MentionUri::TextThread { path, name } => {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
+ url.query_pairs_mut().append_pair("name", name);
+ url
+ }
+ MentionUri::Rule { name, id } => {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path(&format!("/agent/rule/{id}"));
+ url.query_pairs_mut().append_pair("name", name);
+ url
+ }
+ MentionUri::Fetch { url } => url.clone(),
}
}
}
+pub struct MentionLink<'a>(&'a MentionUri);
+
+impl fmt::Display for MentionLink<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "[@{}]({})", self.0.name(), self.0.to_uri())
+ }
+}
+
+fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
+ let pairs = url.query_pairs().collect::<Vec<_>>();
+ match pairs.as_slice() {
+ [] => Ok(None),
+ [(k, v)] => {
+ if k != name {
+ bail!("invalid query parameter")
+ }
+
+ Ok(Some(v.to_string()))
+ }
+ _ => bail!("too many query pairs"),
+ }
+}
+
+pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
+ format!(
+ "{} ({}:{})",
+ path.file_name().unwrap_or_default().display(),
+ line_range.start + 1,
+ line_range.end + 1
+ )
+}
+
#[cfg(test)]
mod tests {
use super::*;
#[test]
- fn test_mention_uri_parse_and_display() {
- // Test file URI
+ fn test_parse_file_uri() {
let file_uri = "file:///path/to/file.rs";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
_ => panic!("Expected File variant"),
}
- assert_eq!(parsed.to_uri(), file_uri);
+ assert_eq!(parsed.to_uri().to_string(), file_uri);
+ }
- // Test symbol URI
- let symbol_uri = "file:///path/to/file.rs#MySymbol";
+ #[test]
+ fn test_parse_symbol_uri() {
+ let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
- MentionUri::Symbol(path, symbol) => {
+ MentionUri::Symbol {
+ path,
+ name,
+ line_range,
+ } => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
- assert_eq!(symbol, "MySymbol");
+ assert_eq!(name, "MySymbol");
+ assert_eq!(line_range.start, 9);
+ assert_eq!(line_range.end, 19);
}
_ => panic!("Expected Symbol variant"),
}
- assert_eq!(parsed.to_uri(), symbol_uri);
+ assert_eq!(parsed.to_uri().to_string(), symbol_uri);
+ }
+
+ #[test]
+ fn test_parse_selection_uri() {
+ let selection_uri = "file:///path/to/file.rs#L5:15";
+ let parsed = MentionUri::parse(selection_uri).unwrap();
+ match &parsed {
+ MentionUri::Selection { path, line_range } => {
+ assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
+ assert_eq!(line_range.start, 4);
+ assert_eq!(line_range.end, 14);
+ }
+ _ => panic!("Expected Selection variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), selection_uri);
+ }
- // Test thread URI
- let thread_uri = "zed:///agent/thread/session123";
+ #[test]
+ fn test_parse_thread_uri() {
+ let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed {
- MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"),
+ MentionUri::Thread {
+ id: thread_id,
+ name,
+ } => {
+ assert_eq!(thread_id.to_string(), "session123");
+ assert_eq!(name, "Thread name");
+ }
_ => panic!("Expected Thread variant"),
}
- assert_eq!(parsed.to_uri(), thread_uri);
+ assert_eq!(parsed.to_uri().to_string(), thread_uri);
+ }
- // Test rule URI
- let rule_uri = "zed:///agent/rule/my_rule";
+ #[test]
+ fn test_parse_rule_uri() {
+ let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
let parsed = MentionUri::parse(rule_uri).unwrap();
match &parsed {
- MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
+ MentionUri::Rule { id, name } => {
+ assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
+ assert_eq!(name, "Some rule");
+ }
_ => panic!("Expected Rule variant"),
}
- assert_eq!(parsed.to_uri(), rule_uri);
+ assert_eq!(parsed.to_uri().to_string(), rule_uri);
+ }
+
+ #[test]
+ fn test_parse_fetch_http_uri() {
+ let http_uri = "http://example.com/path?query=value#fragment";
+ let parsed = MentionUri::parse(http_uri).unwrap();
+ match &parsed {
+ MentionUri::Fetch { url } => {
+ assert_eq!(url.to_string(), http_uri);
+ }
+ _ => panic!("Expected Fetch variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), http_uri);
+ }
- // Test invalid scheme
- assert!(MentionUri::parse("http://example.com").is_err());
+ #[test]
+ fn test_parse_fetch_https_uri() {
+ let https_uri = "https://example.com/api/endpoint";
+ let parsed = MentionUri::parse(https_uri).unwrap();
+ match &parsed {
+ MentionUri::Fetch { url } => {
+ assert_eq!(url.to_string(), https_uri);
+ }
+ _ => panic!("Expected Fetch variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), https_uri);
+ }
+
+ #[test]
+ fn test_invalid_scheme() {
+ assert!(MentionUri::parse("ftp://example.com").is_err());
+ assert!(MentionUri::parse("ssh://example.com").is_err());
+ assert!(MentionUri::parse("unknown://example.com").is_err());
+ }
- // Test invalid zed path
+ #[test]
+ fn test_invalid_zed_path() {
assert!(MentionUri::parse("zed:///invalid/path").is_err());
+ assert!(MentionUri::parse("zed:///agent/unknown/test").is_err());
+ }
+
+ #[test]
+ fn test_invalid_line_range_format() {
+ // Missing L prefix
+ assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err());
+
+ // Missing colon separator
+ assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err());
+
+ // Invalid numbers
+ assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err());
+ assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err());
+ }
+
+ #[test]
+ fn test_invalid_query_parameters() {
+ // Invalid query parameter name
+ assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err());
+
+ // Too many query parameters
+ assert!(
+ MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err()
+ );
+ }
+
+ #[test]
+ fn test_zero_based_line_numbers() {
+ // Test that 0-based line numbers are rejected (should be 1-based)
+ assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err());
+ assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err());
+ assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err());
}
}
@@ -205,6 +205,22 @@ impl ThreadStore {
(this, ready_rx)
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn fake(project: Entity<Project>, cx: &mut App) -> Self {
+ Self {
+ project,
+ tools: cx.new(|_| ToolWorkingSet::default()),
+ prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
+ prompt_store: None,
+ context_server_tool_ids: HashMap::default(),
+ threads: Vec::new(),
+ project_context: SharedProjectContext::default(),
+ reload_system_prompt_tx: mpsc::channel(0).0,
+ _reload_system_prompt_task: Task::ready(()),
+ _subscriptions: vec![],
+ }
+ }
+
fn handle_project_event(
&mut self,
_project: Entity<Project>,
@@ -25,8 +25,8 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
-use std::fmt::Write;
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
+use std::{fmt::Write, ops::Range};
use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -79,9 +79,9 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
if !content.is_empty() {
- markdown.push_str(&format!("{}\n\n{}\n", uri.to_link(), content));
+ let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content);
} else {
- markdown.push_str(&format!("{}\n", uri.to_link()));
+ let _ = write!(&mut markdown, "{}\n", uri.as_link());
}
}
}
@@ -104,12 +104,14 @@ impl UserMessage {
const OPEN_FILES_TAG: &str = "<files>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
const OPEN_THREADS_TAG: &str = "<threads>";
+ const OPEN_FETCH_TAG: &str = "<fetched_urls>";
const OPEN_RULES_TAG: &str =
"<rules>\nThe user has specified the following rules that should be applied:\n";
let mut file_context = OPEN_FILES_TAG.to_string();
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
let mut thread_context = OPEN_THREADS_TAG.to_string();
+ let mut fetch_context = OPEN_FETCH_TAG.to_string();
let mut rules_context = OPEN_RULES_TAG.to_string();
for chunk in &self.content {
@@ -122,21 +124,40 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
match uri {
- MentionUri::File(path) | MentionUri::Symbol(path, _) => {
+ MentionUri::File(path) => {
write!(
&mut symbol_context,
"\n{}",
MarkdownCodeBlock {
- tag: &codeblock_tag(&path),
+ tag: &codeblock_tag(&path, None),
text: &content.to_string(),
}
)
.ok();
}
- MentionUri::Thread(_session_id) => {
+ MentionUri::Symbol {
+ path, line_range, ..
+ }
+ | MentionUri::Selection {
+ path, line_range, ..
+ } => {
+ write!(
+ &mut rules_context,
+ "\n{}",
+ MarkdownCodeBlock {
+ tag: &codeblock_tag(&path, Some(line_range)),
+ text: &content
+ }
+ )
+ .ok();
+ }
+ MentionUri::Thread { .. } => {
+ write!(&mut thread_context, "\n{}\n", content).ok();
+ }
+ MentionUri::TextThread { .. } => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
- MentionUri::Rule(_user_prompt_id) => {
+ MentionUri::Rule { .. } => {
write!(
&mut rules_context,
"\n{}",
@@ -147,9 +168,12 @@ impl UserMessage {
)
.ok();
}
+ MentionUri::Fetch { url } => {
+ write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
+ }
}
- language_model::MessageContent::Text(uri.to_link())
+ language_model::MessageContent::Text(uri.as_link().to_string())
}
};
@@ -179,6 +203,13 @@ impl UserMessage {
.push(language_model::MessageContent::Text(thread_context));
}
+ if fetch_context.len() > OPEN_FETCH_TAG.len() {
+ fetch_context.push_str("</fetched_urls>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(fetch_context));
+ }
+
if rules_context.len() > OPEN_RULES_TAG.len() {
rules_context.push_str("</user_rules>\n");
message
@@ -200,6 +231,26 @@ impl UserMessage {
}
}
+fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
+ let mut result = String::new();
+
+ if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
+ let _ = write!(result, "{} ", extension);
+ }
+
+ let _ = write!(result, "{}", full_path.display());
+
+ if let Some(range) = line_range {
+ if range.start == range.end {
+ let _ = write!(result, ":{}", range.start + 1);
+ } else {
+ let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1);
+ }
+ }
+
+ result
+}
+
impl AgentMessage {
pub fn to_markdown(&self) -> String {
let mut markdown = String::from("## Assistant\n\n");
@@ -1367,18 +1418,6 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
}
}
-fn codeblock_tag(full_path: &Path) -> String {
- let mut result = String::new();
-
- if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
- let _ = write!(result, "{} ", extension);
- }
-
- let _ = write!(result, "{}", full_path.display());
-
- result
-}
-
impl From<&str> for UserMessageContent {
fn from(text: &str) -> Self {
Self::Text(text.into())
@@ -93,6 +93,7 @@ time.workspace = true
time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
+url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
@@ -102,6 +103,8 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
+agent = { workspace = true, features = ["test-support"] }
+assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
@@ -3,71 +3,184 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
-use acp_thread::MentionUri;
-use anyhow::{Context as _, Result};
-use collections::HashMap;
+use acp_thread::{MentionUri, selection_name};
+use anyhow::{Context as _, Result, anyhow};
+use collections::{HashMap, HashSet};
use editor::display_map::CreaseId;
-use editor::{CompletionProvider, Editor, ExcerptId};
+use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
use file_icons::FileIcons;
use futures::future::try_join_all;
+use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
+use http_client::HttpClientWithUrl;
+use itertools::Itertools as _;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use parking_lot::Mutex;
-use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId};
+use project::{
+ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
+};
+use prompt_store::PromptStore;
use rope::Point;
-use text::{Anchor, ToPoint};
+use text::{Anchor, OffsetRangeExt as _, ToPoint as _};
use ui::prelude::*;
+use url::Url;
use workspace::Workspace;
-
-use crate::context_picker::MentionLink;
-use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
+use workspace::notifications::NotifyResultExt;
+
+use agent::{
+ context::RULES_ICON,
+ thread_store::{TextThreadStore, ThreadStore},
+};
+
+use crate::context_picker::fetch_context_picker::fetch_url_content;
+use crate::context_picker::file_context_picker::{FileMatch, search_files};
+use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
+use crate::context_picker::symbol_context_picker::SymbolMatch;
+use crate::context_picker::symbol_context_picker::search_symbols;
+use crate::context_picker::thread_context_picker::{
+ ThreadContextEntry, ThreadMatch, search_threads,
+};
+use crate::context_picker::{
+ ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry,
+ available_context_picker_entries, recent_context_picker_entries, selection_ranges,
+};
#[derive(Default)]
pub struct MentionSet {
- paths_by_crease_id: HashMap<CreaseId, MentionUri>,
+ uri_by_crease_id: HashMap<CreaseId, MentionUri>,
+ fetch_results: HashMap<Url, String>,
}
impl MentionSet {
- pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) {
- self.paths_by_crease_id
- .insert(crease_id, MentionUri::File(path));
+ pub fn insert(&mut self, crease_id: CreaseId, uri: MentionUri) {
+ self.uri_by_crease_id.insert(crease_id, uri);
+ }
+
+ pub fn add_fetch_result(&mut self, url: Url, content: String) {
+ self.fetch_results.insert(url, content);
}
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
- self.paths_by_crease_id.drain().map(|(id, _)| id)
+ self.fetch_results.clear();
+ self.uri_by_crease_id.drain().map(|(id, _)| id)
}
pub fn contents(
&self,
project: Entity<Project>,
+ thread_store: Entity<ThreadStore>,
+ text_thread_store: Entity<TextThreadStore>,
+ window: &mut Window,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, Mention>>> {
let contents = self
- .paths_by_crease_id
+ .uri_by_crease_id
.iter()
- .map(|(crease_id, uri)| match uri {
- MentionUri::File(path) => {
- let crease_id = *crease_id;
- let uri = uri.clone();
- let path = path.to_path_buf();
- let buffer_task = project.update(cx, |project, cx| {
- let path = project
- .find_project_path(path, cx)
- .context("Failed to find project path")?;
- anyhow::Ok(project.open_buffer(path, cx))
- });
-
- cx.spawn(async move |cx| {
- let buffer = buffer_task?.await?;
- let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-
- anyhow::Ok((crease_id, Mention { uri, content }))
- })
- }
- _ => {
- // TODO
- unimplemented!()
+ .map(|(&crease_id, uri)| {
+ match uri {
+ MentionUri::File(path) => {
+ let uri = uri.clone();
+ let path = path.to_path_buf();
+ let buffer_task = project.update(cx, |project, cx| {
+ let path = project
+ .find_project_path(path, cx)
+ .context("Failed to find project path")?;
+ anyhow::Ok(project.open_buffer(path, cx))
+ });
+
+ cx.spawn(async move |cx| {
+ let buffer = buffer_task?.await?;
+ let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
+
+ anyhow::Ok((crease_id, Mention { uri, content }))
+ })
+ }
+ MentionUri::Symbol {
+ path, line_range, ..
+ }
+ | MentionUri::Selection {
+ path, line_range, ..
+ } => {
+ let uri = uri.clone();
+ let path_buf = path.clone();
+ let line_range = line_range.clone();
+
+ let buffer_task = project.update(cx, |project, cx| {
+ let path = project
+ .find_project_path(&path_buf, cx)
+ .context("Failed to find project path")?;
+ anyhow::Ok(project.open_buffer(path, cx))
+ });
+
+ cx.spawn(async move |cx| {
+ let buffer = buffer_task?.await?;
+ let content = buffer.read_with(cx, |buffer, _cx| {
+ buffer
+ .text_for_range(
+ Point::new(line_range.start, 0)
+ ..Point::new(
+ line_range.end,
+ buffer.line_len(line_range.end),
+ ),
+ )
+ .collect()
+ })?;
+
+ anyhow::Ok((crease_id, Mention { uri, content }))
+ })
+ }
+ MentionUri::Thread { id: thread_id, .. } => {
+ let open_task = thread_store.update(cx, |thread_store, cx| {
+ thread_store.open_thread(&thread_id, window, cx)
+ });
+
+ let uri = uri.clone();
+ cx.spawn(async move |cx| {
+ let thread = open_task.await?;
+ let content = thread.read_with(cx, |thread, _cx| {
+ thread.latest_detailed_summary_or_text().to_string()
+ })?;
+
+ anyhow::Ok((crease_id, Mention { uri, content }))
+ })
+ }
+ MentionUri::TextThread { path, .. } => {
+ let context = text_thread_store.update(cx, |text_thread_store, cx| {
+ text_thread_store.open_local_context(path.as_path().into(), cx)
+ });
+ let uri = uri.clone();
+ cx.spawn(async move |cx| {
+ let context = context.await?;
+ let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
+ anyhow::Ok((crease_id, Mention { uri, content: xml }))
+ })
+ }
+ MentionUri::Rule { id: prompt_id, .. } => {
+ let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
+ else {
+ return Task::ready(Err(anyhow!("missing prompt store")));
+ };
+ let text_task = prompt_store.read(cx).load(*prompt_id, cx);
+ let uri = uri.clone();
+ cx.spawn(async move |_| {
+ // TODO: report load errors instead of just logging
+ let text = text_task.await?;
+ anyhow::Ok((crease_id, Mention { uri, content: text }))
+ })
+ }
+ MentionUri::Fetch { url } => {
+ let Some(content) = self.fetch_results.get(&url) else {
+ return Task::ready(Err(anyhow!("missing fetch result")));
+ };
+ Task::ready(Ok((
+ crease_id,
+ Mention {
+ uri: uri.clone(),
+ content: content.clone(),
+ },
+ )))
+ }
}
})
.collect::<Vec<_>>();
@@ -79,30 +192,458 @@ impl MentionSet {
}
}
+#[derive(Debug)]
pub struct Mention {
pub uri: MentionUri,
pub content: String,
}
+pub(crate) enum Match {
+ File(FileMatch),
+ Symbol(SymbolMatch),
+ Thread(ThreadMatch),
+ Fetch(SharedString),
+ Rules(RulesContextEntry),
+ Entry(EntryMatch),
+}
+
+pub struct EntryMatch {
+ mat: Option<StringMatch>,
+ entry: ContextPickerEntry,
+}
+
+impl Match {
+ pub fn score(&self) -> f64 {
+ match self {
+ Match::File(file) => file.mat.score,
+ Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
+ Match::Thread(_) => 1.,
+ Match::Symbol(_) => 1.,
+ Match::Rules(_) => 1.,
+ Match::Fetch(_) => 1.,
+ }
+ }
+}
+
+fn search(
+ mode: Option<ContextPickerMode>,
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ recent_entries: Vec<RecentEntry>,
+ prompt_store: Option<Entity<PromptStore>>,
+ thread_store: WeakEntity<ThreadStore>,
+ text_thread_context_store: WeakEntity<assistant_context::ContextStore>,
+ 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, context_store)) = thread_store
+ .upgrade()
+ .zip(text_thread_context_store.upgrade())
+ {
+ let search_threads_task = search_threads(
+ query.clone(),
+ cancellation_flag.clone(),
+ thread_store,
+ context_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())
+ }
+ }
+
+ Some(ContextPickerMode::Rules) => {
+ if let Some(prompt_store) = prompt_store.as_ref() {
+ let search_rules_task =
+ search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
+ cx.background_spawn(async move {
+ search_rules_task
+ .await
+ .into_iter()
+ .map(Match::Rules)
+ .collect::<Vec<_>>()
+ })
+ } else {
+ Task::ready(Vec::new())
+ }
+ }
+
+ None => {
+ if query.is_empty() {
+ let mut matches = recent_entries
+ .into_iter()
+ .map(|entry| match entry {
+ 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,
+ }),
+ RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch {
+ thread: thread_context_entry,
+ is_recent: true,
+ }),
+ })
+ .collect::<Vec<_>>();
+
+ matches.extend(
+ available_context_picker_entries(
+ &prompt_store,
+ &Some(thread_store.clone()),
+ &workspace,
+ cx,
+ )
+ .into_iter()
+ .map(|mode| {
+ Match::Entry(EntryMatch {
+ entry: mode,
+ mat: None,
+ })
+ }),
+ );
+
+ Task::ready(matches)
+ } else {
+ let executor = cx.background_executor().clone();
+
+ let search_files_task =
+ search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+
+ let entries = available_context_picker_entries(
+ &prompt_store,
+ &Some(thread_store.clone()),
+ &workspace,
+ cx,
+ );
+ let entry_candidates = entries
+ .iter()
+ .enumerate()
+ .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
+ .collect::<Vec<_>>();
+
+ cx.background_spawn(async move {
+ let mut matches = search_files_task
+ .await
+ .into_iter()
+ .map(Match::File)
+ .collect::<Vec<_>>();
+
+ let entry_matches = fuzzy::match_strings(
+ &entry_candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Arc::new(AtomicBool::default()),
+ executor,
+ )
+ .await;
+
+ matches.extend(entry_matches.into_iter().map(|mat| {
+ Match::Entry(EntryMatch {
+ entry: entries[mat.candidate_id],
+ mat: Some(mat),
+ })
+ }));
+
+ matches.sort_by(|a, b| {
+ b.score()
+ .partial_cmp(&a.score())
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ matches
+ })
+ }
+ }
+ }
+}
+
pub struct ContextPickerCompletionProvider {
+ mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>,
+ thread_store: WeakEntity<ThreadStore>,
+ text_thread_store: WeakEntity<TextThreadStore>,
editor: WeakEntity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
}
impl ContextPickerCompletionProvider {
pub fn new(
mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>,
+ thread_store: WeakEntity<ThreadStore>,
+ text_thread_store: WeakEntity<TextThreadStore>,
editor: WeakEntity<Editor>,
) -> Self {
Self {
mention_set,
workspace,
+ thread_store,
+ text_thread_store,
editor,
}
}
+ fn completion_for_entry(
+ entry: ContextPickerEntry,
+ excerpt_id: ExcerptId,
+ source_range: Range<Anchor>,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+ ) -> Option<Completion> {
+ match entry {
+ ContextPickerEntry::Mode(mode) => Some(Completion {
+ replace_range: source_range.clone(),
+ new_text: format!("@{} ", mode.keyword()),
+ 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)),
+ }),
+ ContextPickerEntry::Action(action) => {
+ let (new_text, on_action) = match action {
+ ContextPickerAction::AddSelections => {
+ let selections = selection_ranges(workspace, cx);
+
+ const PLACEHOLDER: &str = "selection ";
+
+ let new_text = std::iter::repeat(PLACEHOLDER)
+ .take(selections.len())
+ .chain(std::iter::once(""))
+ .join(" ");
+
+ let callback = Arc::new({
+ let mention_set = mention_set.clone();
+ let selections = selections.clone();
+ move |_, window: &mut Window, cx: &mut App| {
+ let editor = editor.clone();
+ let mention_set = mention_set.clone();
+ let selections = selections.clone();
+ window.defer(cx, move |window, cx| {
+ let mut current_offset = 0;
+
+ for (buffer, selection_range) in selections {
+ let snapshot =
+ editor.read(cx).buffer().read(cx).snapshot(cx);
+ let Some(start) = snapshot
+ .anchor_in_excerpt(excerpt_id, source_range.start)
+ else {
+ return;
+ };
+
+ let offset = start.to_offset(&snapshot) + current_offset;
+ let text_len = PLACEHOLDER.len() - 1;
+
+ let range = snapshot.anchor_after(offset)
+ ..snapshot.anchor_after(offset + text_len);
+
+ let path = buffer
+ .read(cx)
+ .file()
+ .map_or(PathBuf::from("untitled"), |file| {
+ file.path().to_path_buf()
+ });
+
+ let point_range = snapshot
+ .as_singleton()
+ .map(|(_, _, snapshot)| {
+ selection_range.to_point(&snapshot)
+ })
+ .unwrap_or_default();
+ let line_range = point_range.start.row..point_range.end.row;
+ let crease = crate::context_picker::crease_for_mention(
+ selection_name(&path, &line_range).into(),
+ IconName::Reader.path().into(),
+ range,
+ editor.downgrade(),
+ );
+
+ let [crease_id]: [_; 1] =
+ editor.update(cx, |editor, cx| {
+ let crease_ids =
+ editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(
+ vec![crease],
+ false,
+ window,
+ cx,
+ );
+ crease_ids.try_into().unwrap()
+ });
+
+ mention_set.lock().insert(
+ crease_id,
+ MentionUri::Selection { path, line_range },
+ );
+
+ current_offset += text_len + 1;
+ }
+ });
+
+ false
+ }
+ });
+
+ (new_text, callback)
+ }
+ };
+
+ Some(Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(action.label().to_string(), None),
+ icon_path: Some(action.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(on_action),
+ })
+ }
+ }
+ }
+
+ fn completion_for_thread(
+ thread_entry: ThreadContextEntry,
+ excerpt_id: ExcerptId,
+ source_range: Range<Anchor>,
+ recent: bool,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ ) -> Completion {
+ let icon_for_completion = if recent {
+ IconName::HistoryRerun
+ } else {
+ IconName::Thread
+ };
+
+ let uri = match &thread_entry {
+ ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
+ id: id.clone(),
+ name: title.to_string(),
+ },
+ ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
+ path: path.to_path_buf(),
+ name: title.to_string(),
+ },
+ };
+ let new_text = format!("{} ", uri.as_link());
+
+ let new_text_len = new_text.len();
+ Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(thread_entry.title().to_string(), None),
+ documentation: None,
+ insert_text_mode: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(icon_for_completion.path().into()),
+ confirm: Some(confirm_completion_callback(
+ IconName::Thread.path().into(),
+ thread_entry.title().clone(),
+ excerpt_id,
+ source_range.start,
+ new_text_len - 1,
+ editor.clone(),
+ mention_set,
+ uri,
+ )),
+ }
+ }
+
+ fn completion_for_rules(
+ rule: RulesContextEntry,
+ excerpt_id: ExcerptId,
+ source_range: Range<Anchor>,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ ) -> Completion {
+ let uri = MentionUri::Rule {
+ id: rule.prompt_id.into(),
+ name: rule.title.to_string(),
+ };
+ let new_text = format!("{} ", uri.as_link());
+ let new_text_len = new_text.len();
+ Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(rule.title.to_string(), None),
+ documentation: None,
+ insert_text_mode: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(RULES_ICON.path().into()),
+ confirm: Some(confirm_completion_callback(
+ RULES_ICON.path().into(),
+ rule.title.clone(),
+ excerpt_id,
+ source_range.start,
+ new_text_len - 1,
+ editor.clone(),
+ mention_set,
+ uri,
+ )),
+ }
+ }
+
pub(crate) fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
@@ -114,9 +655,12 @@ impl ContextPickerCompletionProvider {
mention_set: Arc<Mutex<MentionSet>>,
project: Entity<Project>,
cx: &App,
- ) -> Completion {
+ ) -> Option<Completion> {
let (file_name, directory) =
- extract_file_name_and_directory(&project_path.path, path_prefix);
+ crate::context_picker::file_context_picker::extract_file_name_and_directory(
+ &project_path.path,
+ path_prefix,
+ );
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
@@ -138,9 +682,12 @@ impl ContextPickerCompletionProvider {
crease_icon_path.clone()
};
- let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
+ let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
+
+ let file_uri = MentionUri::File(abs_path);
+ let new_text = format!("{} ", file_uri.as_link());
let new_text_len = new_text.len();
- Completion {
+ Some(Completion {
replace_range: source_range.clone(),
new_text,
label,
@@ -151,15 +698,153 @@ impl ContextPickerCompletionProvider {
confirm: Some(confirm_completion_callback(
crease_icon_path,
file_name,
- project_path,
excerpt_id,
source_range.start,
new_text_len - 1,
editor,
- mention_set,
- project,
+ mention_set.clone(),
+ file_uri,
)),
- }
+ })
+ }
+
+ fn completion_for_symbol(
+ symbol: Symbol,
+ excerpt_id: ExcerptId,
+ source_range: Range<Anchor>,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ workspace: Entity<Workspace>,
+ cx: &mut App,
+ ) -> Option<Completion> {
+ let project = workspace.read(cx).project().clone();
+
+ let label = CodeLabel::plain(symbol.name.clone(), None);
+
+ let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
+ let uri = MentionUri::Symbol {
+ path: abs_path,
+ name: symbol.name.clone(),
+ line_range: symbol.range.start.0.row..symbol.range.end.0.row,
+ };
+ let new_text = format!("{} ", uri.as_link());
+ let new_text_len = new_text.len();
+ Some(Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label,
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(IconName::Code.path().into()),
+ insert_text_mode: None,
+ confirm: Some(confirm_completion_callback(
+ IconName::Code.path().into(),
+ symbol.name.clone().into(),
+ excerpt_id,
+ source_range.start,
+ new_text_len - 1,
+ editor.clone(),
+ mention_set.clone(),
+ uri,
+ )),
+ })
+ }
+
+ fn completion_for_fetch(
+ source_range: Range<Anchor>,
+ url_to_fetch: SharedString,
+ excerpt_id: ExcerptId,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ http_client: Arc<HttpClientWithUrl>,
+ ) -> Option<Completion> {
+ let new_text = format!("@fetch {} ", url_to_fetch.clone());
+ let new_text_len = new_text.len();
+ Some(Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(url_to_fetch.to_string(), None),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(IconName::ToolWeb.path().into()),
+ insert_text_mode: None,
+ confirm: Some({
+ let start = source_range.start;
+ let content_len = new_text_len - 1;
+ let editor = editor.clone();
+ let url_to_fetch = url_to_fetch.clone();
+ let source_range = source_range.clone();
+ Arc::new(move |_, window, cx| {
+ let Some(url) = url::Url::parse(url_to_fetch.as_ref())
+ .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
+ .notify_app_err(cx)
+ else {
+ return false;
+ };
+ let mention_uri = MentionUri::Fetch { url: url.clone() };
+
+ let editor = editor.clone();
+ let mention_set = mention_set.clone();
+ let http_client = http_client.clone();
+ let source_range = source_range.clone();
+ window.defer(cx, move |window, cx| {
+ let url = url.clone();
+
+ let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
+ excerpt_id,
+ start,
+ content_len,
+ url.to_string().into(),
+ IconName::ToolWeb.path().into(),
+ editor.clone(),
+ window,
+ cx,
+ ) else {
+ return;
+ };
+
+ let editor = editor.clone();
+ let mention_set = mention_set.clone();
+ let http_client = http_client.clone();
+ let source_range = source_range.clone();
+ window
+ .spawn(cx, async move |cx| {
+ if let Some(content) =
+ fetch_url_content(http_client, url.to_string())
+ .await
+ .notify_async_err(cx)
+ {
+ mention_set.lock().add_fetch_result(url, content);
+ mention_set.lock().insert(crease_id, mention_uri.clone());
+ } else {
+ // Remove crease if we failed to fetch
+ editor
+ .update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let Some(anchor) = snapshot
+ .anchor_in_excerpt(excerpt_id, source_range.start)
+ else {
+ return;
+ };
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map.unfold_intersecting(
+ vec![anchor..anchor],
+ true,
+ cx,
+ );
+ });
+ editor.remove_creases([crease_id], cx);
+ })
+ .ok();
+ }
+ Some(())
+ })
+ .detach();
+ });
+ false
+ })
+ }),
+ })
}
}
@@ -206,16 +891,66 @@ impl CompletionProvider for ContextPickerCompletionProvider {
};
let project = workspace.read(cx).project().clone();
+ let http_client = workspace.read(cx).client().http_client();
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
+ let thread_store = self.thread_store.clone();
+ let text_thread_store = self.text_thread_store.clone();
let editor = self.editor.clone();
- let mention_set = self.mention_set.clone();
- let MentionCompletion { argument, .. } = state;
+
+ let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
- let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
+ let (exclude_paths, exclude_threads) = {
+ let mention_set = self.mention_set.lock();
+
+ let mut excluded_paths = HashSet::default();
+ let mut excluded_threads = HashSet::default();
+
+ for uri in mention_set.uri_by_crease_id.values() {
+ match uri {
+ MentionUri::File(path) => {
+ excluded_paths.insert(path.clone());
+ }
+ MentionUri::Thread { id, .. } => {
+ excluded_threads.insert(id.clone());
+ }
+ _ => {}
+ }
+ }
+
+ (excluded_paths, excluded_threads)
+ };
+
+ let recent_entries = recent_context_picker_entries(
+ Some(thread_store.clone()),
+ Some(text_thread_store.clone()),
+ workspace.clone(),
+ &exclude_paths,
+ &exclude_threads,
+ cx,
+ );
+
+ let prompt_store = thread_store
+ .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
+ .ok()
+ .flatten();
+
+ let search_task = search(
+ mode,
+ query,
+ Arc::<AtomicBool>::default(),
+ recent_entries,
+ prompt_store,
+ thread_store.clone(),
+ text_thread_store.clone(),
+ workspace.clone(),
+ cx,
+ );
+
+ let mention_set = self.mention_set.clone();
cx.spawn(async move |_, cx| {
let matches = search_task.await;
@@ -226,25 +961,74 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let completions = cx.update(|cx| {
matches
.into_iter()
- .map(|mat| {
- let path_match = &mat.mat;
- let project_path = ProjectPath {
- worktree_id: WorktreeId::from_usize(path_match.worktree_id),
- path: path_match.path.clone(),
- };
+ .filter_map(|mat| match mat {
+ Match::File(FileMatch { mat, is_recent }) => {
+ let project_path = ProjectPath {
+ worktree_id: WorktreeId::from_usize(mat.worktree_id),
+ path: mat.path.clone(),
+ };
+
+ Self::completion_for_path(
+ project_path,
+ &mat.path_prefix,
+ is_recent,
+ mat.is_dir,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ project.clone(),
+ cx,
+ )
+ }
+
+ Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
+ symbol,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ workspace.clone(),
+ cx,
+ ),
- Self::completion_for_path(
- project_path,
- &path_match.path_prefix,
- mat.is_recent,
- path_match.is_dir,
+ Match::Thread(ThreadMatch {
+ thread, is_recent, ..
+ }) => Some(Self::completion_for_thread(
+ thread,
+ excerpt_id,
+ source_range.clone(),
+ is_recent,
+ editor.clone(),
+ mention_set.clone(),
+ )),
+
+ Match::Rules(user_rules) => Some(Self::completion_for_rules(
+ user_rules,
excerpt_id,
source_range.clone(),
editor.clone(),
mention_set.clone(),
- project.clone(),
+ )),
+
+ Match::Fetch(url) => Self::completion_for_fetch(
+ source_range.clone(),
+ url,
+ excerpt_id,
+ editor.clone(),
+ mention_set.clone(),
+ http_client.clone(),
+ ),
+
+ Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
+ entry,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ &workspace,
cx,
- )
+ ),
})
.collect()
})?;
@@ -45,12 +45,8 @@ impl<T> MessageHistory<T> {
None
})
}
-
- #[cfg(test)]
- pub fn items(&self) -> &[T] {
- &self.items
- }
}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -4,15 +4,17 @@ use acp_thread::{
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
+use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
+use editor::scroll::Autoscroll;
use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
- EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
+ EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
};
use file_icons::FileIcons;
use gpui::{
@@ -27,8 +29,10 @@ use language::{Buffer, Language};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
+use prompt_store::PromptId;
use rope::Point;
use settings::{Settings as _, SettingsStore};
+use std::fmt::Write as _;
use std::path::PathBuf;
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
@@ -44,6 +48,7 @@ use ui::{
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
+use zed_actions::assistant::OpenRulesLibrary;
use crate::acp::AcpModelSelectorPopover;
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
@@ -61,6 +66,8 @@ pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ thread_store: Entity<ThreadStore>,
+ text_thread_store: Entity<TextThreadStore>,
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
@@ -108,6 +115,8 @@ impl AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ thread_store: Entity<ThreadStore>,
+ text_thread_store: Entity<TextThreadStore>,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
min_lines: usize,
max_lines: Option<usize>,
@@ -145,6 +154,8 @@ impl AcpThreadView {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(),
workspace.clone(),
+ thread_store.downgrade(),
+ text_thread_store.downgrade(),
cx.weak_entity(),
))));
editor.set_context_menu_options(ContextMenuOptions {
@@ -188,6 +199,8 @@ impl AcpThreadView {
agent: agent.clone(),
workspace: workspace.clone(),
project: project.clone(),
+ thread_store,
+ text_thread_store,
thread_state: Self::initial_state(agent, workspace, project, window, cx),
message_editor,
model_selector: None,
@@ -401,7 +414,13 @@ impl AcpThreadView {
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
- let contents = self.mention_set.lock().contents(project, cx);
+ let thread_store = self.thread_store.clone();
+ let text_thread_store = self.text_thread_store.clone();
+
+ let contents =
+ self.mention_set
+ .lock()
+ .contents(project, thread_store, text_thread_store, window, cx);
cx.spawn_in(window, async move |this, cx| {
let contents = match contents.await {
@@ -439,7 +458,7 @@ impl AcpThreadView {
acp::TextResourceContents {
mime_type: None,
text: mention.content.clone(),
- uri: mention.uri.to_uri(),
+ uri: mention.uri.to_uri().to_string(),
},
),
}));
@@ -614,8 +633,7 @@ impl AcpThreadView {
let path = PathBuf::from(&resource.uri);
let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
let start = text.len();
- let content = MentionUri::File(path).to_uri();
- text.push_str(&content);
+ let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri());
let end = text.len();
if let Some(project_path) = project_path {
let filename: SharedString = project_path
@@ -663,7 +681,9 @@ impl AcpThreadView {
);
if let Some(crease_id) = crease_id {
- mention_set.lock().insert(crease_id, project_path);
+ mention_set
+ .lock()
+ .insert(crease_id, MentionUri::File(project_path));
}
}
}
@@ -2698,9 +2718,72 @@ impl AcpThreadView {
.detach_and_log_err(cx);
}
}
- _ => {
- // TODO
- unimplemented!()
+ MentionUri::Symbol {
+ path, line_range, ..
+ }
+ | MentionUri::Selection { path, line_range } => {
+ let project = workspace.project();
+ let Some((path, _)) = project.update(cx, |project, cx| {
+ let path = project.find_project_path(path, cx)?;
+ let entry = project.entry_for_path(&path, cx)?;
+ Some((path, entry))
+ }) else {
+ return;
+ };
+
+ let item = workspace.open_path(path, None, true, window, cx);
+ window
+ .spawn(cx, async move |cx| {
+ let Some(editor) = item.await?.downcast::<Editor>() else {
+ return Ok(());
+ };
+ let range =
+ Point::new(line_range.start, 0)..Point::new(line_range.start, 0);
+ editor
+ .update_in(cx, |editor, window, cx| {
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges(vec![range]),
+ );
+ })
+ .ok();
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ MentionUri::Thread { id, .. } => {
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel
+ .open_thread_by_id(&id, window, cx)
+ .detach_and_log_err(cx)
+ });
+ }
+ }
+ MentionUri::TextThread { path, .. } => {
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel
+ .open_saved_prompt_editor(path.as_path().into(), window, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+ }
+ MentionUri::Rule { id, .. } => {
+ let PromptId::User { uuid } = id else {
+ return;
+ };
+ window.dispatch_action(
+ Box::new(OpenRulesLibrary {
+ prompt_to_select: Some(uuid.0),
+ }),
+ cx,
+ )
+ }
+ MentionUri::Fetch { url } => {
+ cx.open_url(url.as_str());
}
})
} else {
@@ -3090,7 +3173,7 @@ impl AcpThreadView {
.unwrap_or(path.path.as_os_str())
.display()
.to_string();
- let completion = ContextPickerCompletionProvider::completion_for_path(
+ let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
path,
&path_prefix,
false,
@@ -3101,7 +3184,9 @@ impl AcpThreadView {
self.mention_set.clone(),
self.project.clone(),
cx,
- );
+ ) else {
+ continue;
+ };
self.message_editor.update(cx, |message_editor, cx| {
message_editor.edit(
@@ -3431,17 +3516,14 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
#[cfg(test)]
mod tests {
+ use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol::SessionId;
use editor::EditorSettings;
use fs::FakeFs;
use futures::future::try_join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
- use lsp::{CompletionContext, CompletionTriggerKind};
- use project::CompletionIntent;
use rand::Rng;
- use serde_json::json;
use settings::SettingsStore;
- use util::path;
use super::*;
@@ -3554,109 +3636,6 @@ mod tests {
);
}
- #[gpui::test]
- async fn test_crease_removal(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree("/project", json!({"file": ""})).await;
- let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
- let agent = StubAgentServer::default();
- let (workspace, cx) =
- cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
- let thread_view = cx.update(|window, cx| {
- cx.new(|cx| {
- AcpThreadView::new(
- Rc::new(agent),
- workspace.downgrade(),
- project,
- Rc::new(RefCell::new(MessageHistory::default())),
- 1,
- None,
- window,
- cx,
- )
- })
- });
-
- cx.run_until_parked();
-
- let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
- let excerpt_id = message_editor.update(cx, |editor, cx| {
- editor
- .buffer()
- .read(cx)
- .excerpt_ids()
- .into_iter()
- .next()
- .unwrap()
- });
- let completions = message_editor.update_in(cx, |editor, window, cx| {
- editor.set_text("Hello @", window, cx);
- let buffer = editor.buffer().read(cx).as_singleton().unwrap();
- let completion_provider = editor.completion_provider().unwrap();
- completion_provider.completions(
- excerpt_id,
- &buffer,
- Anchor::MAX,
- CompletionContext {
- trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
- trigger_character: Some("@".into()),
- },
- window,
- cx,
- )
- });
- let [_, completion]: [_; 2] = completions
- .await
- .unwrap()
- .into_iter()
- .flat_map(|response| response.completions)
- .collect::<Vec<_>>()
- .try_into()
- .unwrap();
-
- message_editor.update_in(cx, |editor, window, cx| {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let start = snapshot
- .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
- .unwrap();
- let end = snapshot
- .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
- .unwrap();
- editor.edit([(start..end, completion.new_text)], cx);
- (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
- });
-
- cx.run_until_parked();
-
- // Backspace over the inserted crease (and the following space).
- message_editor.update_in(cx, |editor, window, cx| {
- editor.backspace(&Default::default(), window, cx);
- editor.backspace(&Default::default(), window, cx);
- });
-
- thread_view.update_in(cx, |thread_view, window, cx| {
- thread_view.chat(&Chat, window, cx);
- });
-
- cx.run_until_parked();
-
- let content = thread_view.update_in(cx, |thread_view, _window, _cx| {
- thread_view
- .message_history
- .borrow()
- .items()
- .iter()
- .flatten()
- .cloned()
- .collect::<Vec<_>>()
- });
-
- // We don't send a resource link for the deleted crease.
- pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
- }
-
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,
@@ -3666,12 +3645,19 @@ mod tests {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let thread_store =
+ cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
+ let text_thread_store =
+ cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
+
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpThreadView::new(
Rc::new(agent),
workspace.downgrade(),
project,
+ thread_store.clone(),
+ text_thread_store.clone(),
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,
@@ -973,6 +973,9 @@ impl AgentPanel {
agent: crate::ExternalAgent,
}
+ let thread_store = self.thread_store.clone();
+ let text_thread_store = self.context_store.clone();
+
cx.spawn_in(window, async move |this, cx| {
let server: Rc<dyn AgentServer> = match agent_choice {
Some(agent) => {
@@ -1011,6 +1014,8 @@ impl AgentPanel {
server,
workspace.clone(),
project,
+ thread_store.clone(),
+ text_thread_store.clone(),
message_history,
MIN_EDITOR_LINES,
Some(MAX_EDITOR_LINES),
@@ -1,15 +1,16 @@
mod completion_provider;
-mod fetch_context_picker;
+pub(crate) mod fetch_context_picker;
pub(crate) mod file_context_picker;
-mod rules_context_picker;
-mod symbol_context_picker;
-mod thread_context_picker;
+pub(crate) mod rules_context_picker;
+pub(crate) mod symbol_context_picker;
+pub(crate) mod thread_context_picker;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Result, anyhow};
+use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
@@ -45,7 +46,7 @@ use agent::{
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerEntry {
+pub(crate) enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
@@ -74,7 +75,7 @@ impl ContextPickerEntry {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerMode {
+pub(crate) enum ContextPickerMode {
File,
Symbol,
Fetch,
@@ -83,7 +84,7 @@ enum ContextPickerMode {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerAction {
+pub(crate) enum ContextPickerAction {
AddSelections,
}
@@ -531,7 +532,7 @@ impl ContextPicker {
return vec![];
};
- recent_context_picker_entries(
+ recent_context_picker_entries_with_store(
context_store,
self.thread_store.clone(),
self.text_thread_store.clone(),
@@ -585,7 +586,8 @@ impl Render for ContextPicker {
})
}
}
-enum RecentEntry {
+
+pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<str>,
@@ -593,7 +595,7 @@ enum RecentEntry {
Thread(ThreadContextEntry),
}
-fn available_context_picker_entries(
+pub(crate) fn available_context_picker_entries(
prompt_store: &Option<Entity<PromptStore>>,
thread_store: &Option<WeakEntity<ThreadStore>>,
workspace: &Entity<Workspace>,
@@ -630,24 +632,56 @@ fn available_context_picker_entries(
entries
}
-fn recent_context_picker_entries(
+fn recent_context_picker_entries_with_store(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>,
cx: &App,
+) -> Vec<RecentEntry> {
+ let project = workspace.read(cx).project();
+
+ let mut exclude_paths = context_store.read(cx).file_paths(cx);
+ exclude_paths.extend(exclude_path);
+
+ let exclude_paths = exclude_paths
+ .into_iter()
+ .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
+ .collect();
+
+ let exclude_threads = context_store.read(cx).thread_ids();
+
+ recent_context_picker_entries(
+ thread_store,
+ text_thread_store,
+ workspace,
+ &exclude_paths,
+ exclude_threads,
+ cx,
+ )
+}
+
+pub(crate) fn recent_context_picker_entries(
+ thread_store: Option<WeakEntity<ThreadStore>>,
+ text_thread_store: Option<WeakEntity<TextThreadStore>>,
+ workspace: Entity<Workspace>,
+ exclude_paths: &HashSet<PathBuf>,
+ exclude_threads: &HashSet<ThreadId>,
+ cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
- let mut current_files = context_store.read(cx).file_paths(cx);
- current_files.extend(exclude_path);
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
- .filter(|(path, _)| !current_files.contains(path))
+ .filter(|(_, abs_path)| {
+ abs_path
+ .as_ref()
+ .map_or(true, |path| !exclude_paths.contains(path.as_path()))
+ })
.take(4)
.filter_map(|(project_path, _)| {
project
@@ -659,8 +693,6 @@ fn recent_context_picker_entries(
}),
);
- let current_threads = context_store.read(cx).thread_ids();
-
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
@@ -672,7 +704,7 @@ fn recent_context_picker_entries(
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { id, .. } => {
- Some(id) != active_thread_id && !current_threads.contains(id)
+ Some(id) != active_thread_id && !exclude_threads.contains(id)
}
ThreadContextEntry::Context { .. } => true,
})
@@ -710,7 +742,7 @@ fn add_selections_as_context(
})
}
-fn selection_ranges(
+pub(crate) fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
@@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
- available_context_picker_entries, recent_context_picker_entries, selection_ranges,
+ available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
};
use crate::message_editor::ContextCreasesAddon;
@@ -787,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
.and_then(|b| b.read(cx).file())
.map(|file| ProjectPath::from_file(file.as_ref(), cx));
- let recent_entries = recent_context_picker_entries(
+ let recent_entries = recent_context_picker_entries_with_store(
context_store.clone(),
thread_store.clone(),
text_thread_store.clone(),
@@ -11,6 +11,9 @@ workspace = true
[lib]
path = "src/assistant_context.rs"
+[features]
+test-support = []
+
[dependencies]
agent_settings.workspace = true
anyhow.workspace = true
@@ -138,6 +138,27 @@ impl ContextStore {
})
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn fake(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
+ Self {
+ contexts: Default::default(),
+ contexts_metadata: Default::default(),
+ context_server_slash_command_ids: Default::default(),
+ host_contexts: Default::default(),
+ fs: project.read(cx).fs().clone(),
+ languages: project.read(cx).languages().clone(),
+ slash_commands: Arc::default(),
+ telemetry: project.read(cx).client().telemetry().clone(),
+ _watch_updates: Task::ready(None),
+ client: project.read(cx).client(),
+ project,
+ project_is_shared: false,
+ client_subscription: None,
+ _project_subscriptions: Default::default(),
+ prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
+ }
+ }
+
async fn handle_advertise_contexts(
this: Entity<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,
@@ -12176,6 +12176,8 @@ impl Editor {
let clipboard_text = Cow::Borrowed(text);
self.transact(window, cx, |this, window, cx| {
+ let had_active_edit_prediction = this.has_active_edit_prediction();
+
if let Some(mut clipboard_selections) = clipboard_selections {
let old_selections = this.selections.all::<usize>(cx);
let all_selections_were_entire_line =
@@ -12248,6 +12250,11 @@ impl Editor {
} else {
this.insert(&clipboard_text, window, cx);
}
+
+ let trigger_in_words =
+ this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
+
+ this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
});
}
@@ -90,6 +90,15 @@ impl From<Uuid> for UserPromptId {
}
}
+impl std::fmt::Display for PromptId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ PromptId::User { uuid } => write!(f, "{}", uuid.0),
+ PromptId::EditWorkflow => write!(f, "Edit workflow"),
+ }
+ }
+}
+
pub struct PromptStore {
env: heed::Env,
metadata_cache: RwLock<MetadataCache>,