Checkpoint: Adding fetch
This commit is contained in:
parent
5d5c419fa9
commit
2f6c9e3a2b
6 changed files with 204 additions and 84 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -392,6 +392,7 @@ dependencies = [
|
||||||
"ui",
|
"ui",
|
||||||
"ui_input",
|
"ui_input",
|
||||||
"unindent",
|
"unindent",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
@ -32,6 +32,9 @@ pub enum MentionUri {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
line_range: Range<u32>,
|
line_range: Range<u32>,
|
||||||
},
|
},
|
||||||
|
Fetch {
|
||||||
|
url: Url,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MentionUri {
|
impl MentionUri {
|
||||||
|
@ -97,6 +100,7 @@ impl MentionUri {
|
||||||
bail!("invalid zed url: {:?}", input);
|
bail!("invalid zed url: {:?}", input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"http" | "https" => Ok(MentionUri::Fetch { url }),
|
||||||
other => bail!("unrecognized scheme {:?}", other),
|
other => bail!("unrecognized scheme {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +119,7 @@ impl MentionUri {
|
||||||
MentionUri::Selection {
|
MentionUri::Selection {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
} => selection_name(path, line_range),
|
} => selection_name(path, line_range),
|
||||||
|
MentionUri::Fetch { url } => url.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +177,7 @@ impl MentionUri {
|
||||||
url.query_pairs_mut().append_pair("name", name);
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
MentionUri::Fetch { url } => url.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,11 +295,37 @@ mod tests {
|
||||||
assert_eq!(parsed.to_uri().to_string(), 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]
|
||||||
|
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]
|
#[test]
|
||||||
fn test_invalid_scheme() {
|
fn test_invalid_scheme() {
|
||||||
assert!(MentionUri::parse("http://example.com").is_err());
|
|
||||||
assert!(MentionUri::parse("https://example.com").is_err());
|
|
||||||
assert!(MentionUri::parse("ftp://example.com").is_err());
|
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]
|
#[test]
|
||||||
|
|
|
@ -1151,12 +1151,14 @@ impl AgentMessage {
|
||||||
const OPEN_FILES_TAG: &str = "<files>";
|
const OPEN_FILES_TAG: &str = "<files>";
|
||||||
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_RULES_TAG: &str =
|
const OPEN_RULES_TAG: &str =
|
||||||
"<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 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 rules_context = OPEN_RULES_TAG.to_string();
|
let mut rules_context = OPEN_RULES_TAG.to_string();
|
||||||
|
|
||||||
for chunk in &self.content {
|
for chunk in &self.content {
|
||||||
|
@ -1226,6 +1228,17 @@ impl AgentMessage {
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
write!(
|
||||||
|
&mut fetch_context,
|
||||||
|
"\n{}",
|
||||||
|
MarkdownCodeBlock {
|
||||||
|
tag: &format!("md {url}"),
|
||||||
|
text: &content
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
language_model::MessageContent::Text(uri.as_link().to_string())
|
language_model::MessageContent::Text(uri.as_link().to_string())
|
||||||
|
@ -1258,6 +1271,13 @@ impl AgentMessage {
|
||||||
.push(language_model::MessageContent::Text(thread_context));
|
.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() {
|
if rules_context.len() > OPEN_RULES_TAG.len() {
|
||||||
rules_context.push_str("</user_rules>\n");
|
rules_context.push_str("</user_rules>\n");
|
||||||
message
|
message
|
||||||
|
|
|
@ -93,6 +93,7 @@ time.workspace = true
|
||||||
time_format.workspace = true
|
time_format.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
|
url.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
|
@ -12,6 +12,7 @@ use file_icons::FileIcons;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{App, Entity, Task, WeakEntity};
|
use gpui::{App, Entity, Task, WeakEntity};
|
||||||
|
use http_client::HttpClientWithUrl;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use language::{Buffer, CodeLabel, HighlightId};
|
use language::{Buffer, CodeLabel, HighlightId};
|
||||||
use lsp::CompletionContext;
|
use lsp::CompletionContext;
|
||||||
|
@ -23,6 +24,7 @@ use prompt_store::PromptStore;
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use text::{Anchor, OffsetRangeExt as _, ToPoint as _};
|
use text::{Anchor, OffsetRangeExt as _, ToPoint as _};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
use url::Url;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use agent::{
|
use agent::{
|
||||||
|
@ -45,6 +47,7 @@ use crate::context_picker::{
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct MentionSet {
|
pub struct MentionSet {
|
||||||
uri_by_crease_id: HashMap<CreaseId, MentionUri>,
|
uri_by_crease_id: HashMap<CreaseId, MentionUri>,
|
||||||
|
fetch_results: HashMap<Url, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MentionSet {
|
impl MentionSet {
|
||||||
|
@ -67,95 +70,110 @@ impl MentionSet {
|
||||||
let contents = self
|
let contents = self
|
||||||
.uri_by_crease_id
|
.uri_by_crease_id
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(&crease_id, uri)| match uri {
|
.map(|(&crease_id, uri)| {
|
||||||
MentionUri::File(path) => {
|
match uri {
|
||||||
let uri = uri.clone();
|
MentionUri::File(path) => {
|
||||||
let path = path.to_path_buf();
|
let uri = uri.clone();
|
||||||
let buffer_task = project.update(cx, |project, cx| {
|
let path = path.to_path_buf();
|
||||||
let path = project
|
let buffer_task = project.update(cx, |project, cx| {
|
||||||
.find_project_path(path, cx)
|
let path = project
|
||||||
.context("Failed to find project path")?;
|
.find_project_path(path, cx)
|
||||||
anyhow::Ok(project.open_buffer(path, cx))
|
.context("Failed to find project path")?;
|
||||||
});
|
anyhow::Ok(project.open_buffer(path, cx))
|
||||||
|
});
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let buffer = buffer_task?.await?;
|
let buffer = buffer_task?.await?;
|
||||||
let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||||
|
|
||||||
anyhow::Ok((crease_id, Mention { uri, content }))
|
anyhow::Ok((crease_id, Mention { uri, content }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
MentionUri::Symbol {
|
MentionUri::Symbol {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
}
|
}
|
||||||
| MentionUri::Selection {
|
| MentionUri::Selection {
|
||||||
path, line_range, ..
|
path, line_range, ..
|
||||||
} => {
|
} => {
|
||||||
let uri = uri.clone();
|
let uri = uri.clone();
|
||||||
let path_buf = path.clone();
|
let path_buf = path.clone();
|
||||||
let line_range = line_range.clone();
|
let line_range = line_range.clone();
|
||||||
|
|
||||||
let buffer_task = project.update(cx, |project, cx| {
|
let buffer_task = project.update(cx, |project, cx| {
|
||||||
let path = project
|
let path = project
|
||||||
.find_project_path(&path_buf, cx)
|
.find_project_path(&path_buf, cx)
|
||||||
.context("Failed to find project path")?;
|
.context("Failed to find project path")?;
|
||||||
anyhow::Ok(project.open_buffer(path, cx))
|
anyhow::Ok(project.open_buffer(path, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let buffer = buffer_task?.await?;
|
let buffer = buffer_task?.await?;
|
||||||
let content = buffer.read_with(cx, |buffer, _cx| {
|
let content = buffer.read_with(cx, |buffer, _cx| {
|
||||||
buffer
|
buffer
|
||||||
.text_for_range(
|
.text_for_range(
|
||||||
Point::new(line_range.start, 0)
|
Point::new(line_range.start, 0)
|
||||||
..Point::new(
|
..Point::new(
|
||||||
line_range.end,
|
line_range.end,
|
||||||
buffer.line_len(line_range.end),
|
buffer.line_len(line_range.end),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
anyhow::Ok((crease_id, Mention { uri, content }))
|
anyhow::Ok((crease_id, Mention { uri, content }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
MentionUri::Thread { id: thread_id, .. } => {
|
MentionUri::Thread { id: thread_id, .. } => {
|
||||||
let open_task = thread_store.update(cx, |thread_store, cx| {
|
let open_task = thread_store.update(cx, |thread_store, cx| {
|
||||||
thread_store.open_thread(&thread_id, window, cx)
|
thread_store.open_thread(&thread_id, window, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let uri = uri.clone();
|
let uri = uri.clone();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let thread = open_task.await?;
|
let thread = open_task.await?;
|
||||||
let content = thread.read_with(cx, |thread, _cx| {
|
let content = thread.read_with(cx, |thread, _cx| {
|
||||||
thread.latest_detailed_summary_or_text().to_string()
|
thread.latest_detailed_summary_or_text().to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
anyhow::Ok((crease_id, Mention { uri, content }))
|
anyhow::Ok((crease_id, Mention { uri, content }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
MentionUri::TextThread { path, .. } => {
|
MentionUri::TextThread { path, .. } => {
|
||||||
let context = text_thread_store.update(cx, |text_thread_store, cx| {
|
let context = text_thread_store.update(cx, |text_thread_store, cx| {
|
||||||
text_thread_store.open_local_context(path.as_path().into(), cx)
|
text_thread_store.open_local_context(path.as_path().into(), cx)
|
||||||
});
|
});
|
||||||
let uri = uri.clone();
|
let uri = uri.clone();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let context = context.await?;
|
let context = context.await?;
|
||||||
let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
|
let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
|
||||||
anyhow::Ok((crease_id, Mention { uri, content: xml }))
|
anyhow::Ok((crease_id, Mention { uri, content: xml }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
MentionUri::Rule { id: prompt_id, .. } => {
|
MentionUri::Rule { id: prompt_id, .. } => {
|
||||||
let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() else {
|
let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
|
||||||
return Task::ready(Err(anyhow!("missing prompt store")));
|
else {
|
||||||
};
|
return Task::ready(Err(anyhow!("missing prompt store")));
|
||||||
let text_task = prompt_store.read(cx).load(prompt_id.clone(), cx);
|
};
|
||||||
let uri = uri.clone();
|
let text_task = prompt_store.read(cx).load(prompt_id.clone(), cx);
|
||||||
cx.spawn(async move |_| {
|
let uri = uri.clone();
|
||||||
// TODO: report load errors instead of just logging
|
cx.spawn(async move |_| {
|
||||||
let text = text_task.await?;
|
// TODO: report load errors instead of just logging
|
||||||
anyhow::Ok((crease_id, Mention { uri, content: text }))
|
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<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -177,6 +195,7 @@ pub(crate) enum Match {
|
||||||
File(FileMatch),
|
File(FileMatch),
|
||||||
Symbol(SymbolMatch),
|
Symbol(SymbolMatch),
|
||||||
Thread(ThreadMatch),
|
Thread(ThreadMatch),
|
||||||
|
Fetch(SharedString),
|
||||||
Rules(RulesContextEntry),
|
Rules(RulesContextEntry),
|
||||||
Entry(EntryMatch),
|
Entry(EntryMatch),
|
||||||
}
|
}
|
||||||
|
@ -194,6 +213,7 @@ impl Match {
|
||||||
Match::Thread(_) => 1.,
|
Match::Thread(_) => 1.,
|
||||||
Match::Symbol(_) => 1.,
|
Match::Symbol(_) => 1.,
|
||||||
Match::Rules(_) => 1.,
|
Match::Rules(_) => 1.,
|
||||||
|
Match::Fetch(_) => 1.,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -721,6 +741,40 @@ impl ContextPickerCompletionProvider {
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 url = url::Url::parse(url_to_fetch.as_ref()).ok()?;
|
||||||
|
let mention_uri = MentionUri::Fetch { url };
|
||||||
|
let new_text = format!("{} ", mention_uri.as_link());
|
||||||
|
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,
|
||||||
|
// todo! custom callback to fetch
|
||||||
|
confirm: Some(confirm_completion_callback(
|
||||||
|
IconName::ToolWeb.path().into(),
|
||||||
|
url_to_fetch.clone(),
|
||||||
|
excerpt_id,
|
||||||
|
source_range.start,
|
||||||
|
new_text_len - 1,
|
||||||
|
editor.clone(),
|
||||||
|
mention_set,
|
||||||
|
mention_uri,
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||||
|
@ -885,6 +939,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
mention_set.clone(),
|
mention_set.clone(),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
Match::Fetch(url) => Self::completion_for_fetch(
|
||||||
|
source_range.clone(),
|
||||||
|
url,
|
||||||
|
excerpt_id,
|
||||||
|
editor.clone(),
|
||||||
|
mention_set.clone(),
|
||||||
|
todo!(),
|
||||||
|
),
|
||||||
|
|
||||||
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
||||||
entry,
|
entry,
|
||||||
excerpt_id,
|
excerpt_id,
|
||||||
|
|
|
@ -2701,6 +2701,9 @@ impl AcpThreadView {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
cx.open_url(url.as_str());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
cx.open_url(&url);
|
cx.open_url(&url);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue