diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b70271e57f508a12b012ae9adcaaf07546a0bf1..f4ba227168fb9cec10e1b5e23223b48e7a4ca222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -718,7 +718,7 @@ jobs: timeout-minutes: 60 runs-on: github-8vcpu-ubuntu-2404 if: | - ( startsWith(github.ref, 'refs/tags/v') + false && ( startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [linux_tests] name: Build Zed on FreeBSD diff --git a/Cargo.lock b/Cargo.lock index 3b1337eece07fcd58b578054205dc2da64900b97..47446b6a13fd62dc99c19b890ca8961645d28b70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,12 +7,14 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "action_log", + "agent", "agent-client-protocol", "anyhow", "buffer_diff", "collections", "editor", "env_logger 0.11.8", + "file_icons", "futures 0.3.31", "gpui", "indoc", @@ -21,6 +23,7 @@ dependencies = [ "markdown", "parking_lot", "project", + "prompt_store", "rand 0.8.5", "serde", "serde_json", diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1d66b1a2e96d5e33d6f5fa8e81e76b4e5621d240 Binary files /dev/null and b/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e07bc1f527dd77ba8fd967ef00a54371da7bfaf0 Binary files /dev/null and b/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..efe8a1fb9de848891cd6e7d70280e1f47019c0ca Binary files /dev/null and b/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd6817d5202895da5ac4fad88de3da71e652881a Binary files /dev/null and b/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf differ diff --git a/assets/fonts/plex-mono/license.txt b/assets/fonts/ibm-plex-sans/license.txt similarity index 100% rename from assets/fonts/plex-mono/license.txt rename to assets/fonts/ibm-plex-sans/license.txt diff --git a/assets/fonts/lilex/Lilex-Bold.ttf b/assets/fonts/lilex/Lilex-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..45930ee30b81a78e964c3ff494e05ea45147ed9b Binary files /dev/null and b/assets/fonts/lilex/Lilex-Bold.ttf differ diff --git a/assets/fonts/lilex/Lilex-BoldItalic.ttf b/assets/fonts/lilex/Lilex-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..10c6ab5f74ab2192f0258ba637c05de7d54b632f Binary files /dev/null and b/assets/fonts/lilex/Lilex-BoldItalic.ttf differ diff --git a/assets/fonts/lilex/Lilex-Italic.ttf b/assets/fonts/lilex/Lilex-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7aef10f7e7c6803199551463d0ab456d9c9e03a Binary files /dev/null and b/assets/fonts/lilex/Lilex-Italic.ttf differ diff --git a/assets/fonts/lilex/Lilex-Regular.ttf b/assets/fonts/lilex/Lilex-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..cb98a69b0ffb1976805bf5af3047307fe2422b1d Binary files /dev/null and b/assets/fonts/lilex/Lilex-Regular.ttf differ diff --git a/assets/fonts/plex-sans/license.txt b/assets/fonts/lilex/OFL.txt similarity index 96% rename from assets/fonts/plex-sans/license.txt rename to assets/fonts/lilex/OFL.txt index f72f76504cd73cf2c00b140b2743c138320597b6..156240bc907aa14571afb18b4b1bde62d3ea33e0 100644 --- a/assets/fonts/plex-sans/license.txt +++ b/assets/fonts/lilex/OFL.txt @@ -1,8 +1,9 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" +Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL +https://scripts.sil.org/OFL + ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 @@ -89,4 +90,4 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf b/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf deleted file mode 100644 index d5f4b5e2855fe9e581155d864835570d2e5a06bf..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf and /dev/null differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf b/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf deleted file mode 100644 index 05eaf7cccde1c6f79558395d4cc06ed685342a60..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf and /dev/null differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf b/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf deleted file mode 100644 index 3b078217578da8d88ddfd158133a11ed7f1780ee..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf and /dev/null differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf b/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf deleted file mode 100644 index 61dbb583619e200262c29589ef037538b2360d78..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf and /dev/null differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf b/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf deleted file mode 100644 index f1e66392f7c5a329fbaa14ae9f6d3ee0d48960cd..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf and /dev/null differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf b/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf deleted file mode 100644 index 7612dc516742f8255ce432cfee67f8225ed6ae62..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf and /dev/null differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf b/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf deleted file mode 100644 index 8769c232ee69236158ed03a1306ff7f2be4ebdd8..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf and /dev/null differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf b/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf deleted file mode 100644 index 3ea293d59a31d2d80f0a1b35ef2c4507e7e4eec2..0000000000000000000000000000000000000000 Binary files a/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf and /dev/null differ diff --git a/assets/settings/default.json b/assets/settings/default.json index 0f1818ac7ffb05d40c010c08d6f79eb5192fb5a5..2c3bf6930d04a2668d65c20a191de17033de4aac 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -28,7 +28,9 @@ "edit_prediction_provider": "zed" }, // The name of a font to use for rendering text in the editor - "buffer_font_family": "Zed Plex Mono", + // ".ZedMono" currently aliases to Lilex + // but this may change in the future. + "buffer_font_family": ".ZedMono", // Set the buffer text's font fallbacks, this will be merged with // the platform's default fallbacks. "buffer_font_fallbacks": null, @@ -54,7 +56,9 @@ "buffer_line_height": "comfortable", // The name of a font to use for rendering text in the UI // You can set this to ".SystemUIFont" to use the system font - "ui_font_family": "Zed Plex Sans", + // ".ZedSans" currently aliases to "IBM Plex Sans", but this may + // change in the future + "ui_font_family": ".ZedSans", // Set the UI's font fallbacks, this will be merged with the platform's // default font fallbacks. "ui_font_fallbacks": null, @@ -1402,7 +1406,7 @@ // "font_size": 15, // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Plex Mono", + // "font_family": ".ZedMono", // Set the terminal's font fallbacks. If this option is not included, // the terminal will default to matching the buffer's font fallbacks. // This will be merged with the platform's default font fallbacks diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index b3ec217bad1738eb8e6756d4aa057dd8c3b1ba23..2d0fe2d2645a3f61152211ec439105d4aa73f7ec 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -18,16 +18,19 @@ 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 editor.workspace = true +file_icons.workspace = true futures.workspace = true gpui.workspace = true 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 diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f8a5bf8032a388f1ee4ccfb3358a2fbad02c0fa8..b82fe74f1b53b0480a29df58c52edefe3f03325b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -32,6 +32,7 @@ use util::ResultExt; pub struct UserMessage { pub id: Option, pub content: ContentBlock, + pub chunks: Vec, pub checkpoint: Option, } @@ -419,9 +420,9 @@ impl ContentBlock { fn resource_link_to_content(uri: &str) -> String { if let Some(uri) = MentionUri::parse(&uri).log_err() { - uri.to_link() + uri.as_link().to_string() } else { - uri.to_string().clone() + uri.to_string() } } @@ -804,18 +805,25 @@ impl AcpThread { let entries_len = self.entries.len(); if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry + && let AgentThreadEntry::UserMessage(UserMessage { + id, + content, + chunks, + .. + }) = last_entry { *id = message_id.or(id.take()); - content.append(chunk, &language_registry, cx); + content.append(chunk.clone(), &language_registry, cx); + chunks.push(chunk); let idx = entries_len - 1; cx.emit(AcpThreadEvent::EntryUpdated(idx)); } else { - let content = ContentBlock::new(chunk, &language_registry, cx); + let content = ContentBlock::new(chunk.clone(), &language_registry, cx); self.push_entry( AgentThreadEntry::UserMessage(UserMessage { id: message_id, content, + chunks: vec![chunk], checkpoint: None, }), cx, @@ -1150,6 +1158,7 @@ impl AcpThread { AgentThreadEntry::UserMessage(UserMessage { id: message_id.clone(), content: block, + chunks: message.clone(), checkpoint: None, }), cx, diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 59c479d87b7503fd9d343aa902754e2a5026fda7..5dc6e13786de701cd008fc98c5d2d4108542e296 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,13 +1,45 @@ -use agent_client_protocol as acp; -use anyhow::{Result, bail}; -use std::path::PathBuf; +use agent::ThreadId; +use anyhow::{Context as _, Result, bail}; +use file_icons::FileIcons; +use prompt_store::{PromptId, UserPromptId}; +use std::{ + fmt, + ops::Range, + path::{Path, PathBuf}, +}; +use ui::{App, IconName, SharedString}; +use url::Url; #[derive(Clone, Debug, PartialEq, Eq)] pub enum MentionUri { - File(PathBuf), - Symbol(PathBuf, String), - Thread(acp::SessionId), - Rule(String), + File { + abs_path: PathBuf, + is_directory: bool, + }, + Symbol { + path: PathBuf, + name: String, + line_range: Range, + }, + Thread { + id: ThreadId, + name: String, + }, + TextThread { + path: PathBuf, + name: String, + }, + Rule { + id: PromptId, + name: String, + }, + Selection { + path: PathBuf, + line_range: Range, + }, + Fetch { + url: Url, + }, } impl MentionUri { @@ -17,109 +49,403 @@ 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::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..end + .parse::() + .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)); + let is_directory = input.ends_with("/"); - Ok(Self::File(file_path)) + Ok(Self::File { + abs_path: file_path, + is_directory, + }) } } "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 { 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 { abs_path, .. } => abs_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 icon_path(&self, cx: &mut App) -> SharedString { + match self { + MentionUri::File { + abs_path, + 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::Symbol { .. } => IconName::Code.path().into(), + MentionUri::Thread { .. } => IconName::Thread.path().into(), + MentionUri::TextThread { .. } => IconName::Thread.path().into(), + MentionUri::Rule { .. } => IconName::Reader.path().into(), + MentionUri::Selection { .. } => IconName::Reader.path().into(), + MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), + } } - pub fn to_uri(&self) -> String { + pub fn as_link<'a>(&'a self) -> MentionLink<'a> { + MentionLink(self) + } + + pub fn to_uri(&self) -> Url { match self { - MentionUri::File(path) => { - format!("file://{}", path.display()) + MentionUri::File { + abs_path, + is_directory, + } => { + let mut url = Url::parse("file:///").unwrap(); + let mut path = abs_path.to_string_lossy().to_string(); + if *is_directory && !path.ends_with("/") { + path.push_str("/"); + } + url.set_path(&path); + url + } + 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::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::Symbol(path, name) => { - format!("file://{}#{}", path.display(), name) + 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::Thread(thread) => { - format!("zed:///agent/thread/{}", thread.0) + 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(rule) => { - format!("zed:///agent/rule/{}", rule) + 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> { + let pairs = url.query_pairs().collect::>(); + 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) -> 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"), + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); + assert!(!is_directory); + } + _ => panic!("Expected File variant"), + } + assert_eq!(parsed.to_uri().to_string(), file_uri); + } + + #[test] + fn test_parse_directory_uri() { + let file_uri = "file:///path/to/dir/"; + let parsed = MentionUri::parse(file_uri).unwrap(); + match &parsed { + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir"); + assert!(is_directory); + } _ => panic!("Expected File variant"), } - assert_eq!(parsed.to_uri(), file_uri); + assert_eq!(parsed.to_uri().to_string(), file_uri); + } + + #[test] + fn test_to_directory_uri_with_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir/"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } - // Test symbol URI - let symbol_uri = "file:///path/to/file.rs#MySymbol"; + #[test] + fn test_to_directory_uri_without_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } + + #[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] + 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 invalid scheme - assert!(MentionUri::parse("http://example.com").is_err()); + #[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()); } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 3c2bea53a73f173c54f2c19d6a3c181b562e0307..e1dfaca0508bd201a96df89cd9b5e30f36ea8f7b 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -38,6 +38,10 @@ impl MentionSet { self.paths_by_crease_id.drain().map(|(id, _)| id) } + pub fn clear(&mut self) { + self.paths_by_crease_id.clear(); + } + pub fn contents( &self, project: Entity, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a79356d9ac3ab59db6466a11b816b6ca5832c25f..400a54b491bd38caeaed77f5d76d98e22c4ee734 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -12,8 +12,8 @@ use file_icons::FileIcons; use gpui::{ AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, }; +use language::Buffer; use language::Language; -use language::{Buffer, BufferSnapshot}; use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::Settings; @@ -29,9 +29,6 @@ use util::ResultExt; use workspace::Workspace; use zed_actions::agent::Chat; -pub const MIN_EDITOR_LINES: usize = 4; -pub const MAX_EDITOR_LINES: usize = 8; - pub struct MessageEditor { editor: Entity, project: Entity, @@ -39,7 +36,8 @@ pub struct MessageEditor { } pub enum MessageEditorEvent { - Chat, + Send, + Cancel, } impl EventEmitter for MessageEditor {} @@ -48,6 +46,7 @@ impl MessageEditor { pub fn new( workspace: WeakEntity, project: Entity, + mode: EditorMode, window: &mut Window, cx: &mut Context, ) -> Self { @@ -64,16 +63,7 @@ impl MessageEditor { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let mut editor = Editor::new( - editor::EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }, - buffer, - None, - window, - cx, - ); + let mut editor = Editor::new(mode, buffer, None, window, cx); editor.set_placeholder_text("Message the agent - @ to include files", cx); editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); @@ -161,7 +151,11 @@ impl MessageEditor { } fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { - cx.emit(MessageEditorEvent::Chat) + cx.emit(MessageEditorEvent::Send) + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(MessageEditorEvent::Cancel) } pub fn insert_dragged_files( @@ -219,37 +213,19 @@ impl MessageEditor { } } - pub fn set_expanded(&mut self, expanded: bool, cx: &mut Context) { + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { - if expanded { - editor.set_mode(EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: false, - }) - } else { - editor.set_mode(EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }) - } + editor.set_mode(mode); cx.notify() }); } - #[allow(unused)] - fn set_draft_message( - message_editor: Entity, - mention_set: Arc>, - project: Entity, - message: Option<&[acp::ContentBlock]>, + pub fn set_message( + &mut self, + message: &[acp::ContentBlock], window: &mut Window, cx: &mut Context, - ) -> Option { - cx.notify(); - - let message = message?; - + ) { let mut text = String::new(); let mut mentions = Vec::new(); @@ -262,26 +238,15 @@ impl MessageEditor { resource: acp::EmbeddedResourceResource::TextResourceContents(resource), .. }) => { - if let Some(ref mention @ MentionUri::File(ref abs_path)) = - MentionUri::parse(&resource.uri).log_err() - { - let project_path = project + if let Some(mention) = MentionUri::parse(&resource.uri).log_err() { + let project_path = self + .project .read(cx) .project_path_for_absolute_path(&abs_path, cx); let start = text.len(); - let content = mention.to_uri(); - text.push_str(&content); + write!(text, "{}", mention.as_link()); let end = text.len(); - if let Some(project_path) = project_path { - let filename: SharedString = project_path - .path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string() - .into(); - mentions.push((start..end, project_path, filename)); - } + mentions.push((start..end, project_path, filename)); } } acp::ContentBlock::Image(_) @@ -291,11 +256,12 @@ impl MessageEditor { } } - let snapshot = message_editor.update(cx, |editor, cx| { + let snapshot = self.editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); editor.buffer().read(cx).snapshot(cx) }); + self.mention_set.lock().clear(); for (range, project_path, filename) in mentions { let crease_icon_path = if project_path.path.is_dir() { FileIcons::get_folder_icon(false, cx) @@ -306,26 +272,24 @@ impl MessageEditor { }; let anchor = snapshot.anchor_before(range.start); - if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) { + if let Some(project_path) = self.project.read(cx).absolute_path(&project_path, cx) { let crease_id = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, filename, crease_icon_path, - message_editor.clone(), + self.editor.clone(), window, cx, ); if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, project_path); + self.mention_set.lock().insert(crease_id, project_path); } } } - - let snapshot = snapshot.as_singleton().unwrap().2.clone(); - Some(snapshot) + cx.notify(); } #[cfg(test)] @@ -347,6 +311,7 @@ impl Render for MessageEditor { div() .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::cancel)) .flex_1() .child({ let settings = ThemeSettings::get_global(cx); @@ -384,6 +349,7 @@ mod tests { use std::path::Path; use agent_client_protocol as acp; + use editor::EditorMode; use fs::FakeFs; use gpui::{AppContext, TestAppContext}; use lsp::{CompletionContext, CompletionTriggerKind}; @@ -406,7 +372,18 @@ mod tests { cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let message_editor = cx.update(|window, cx| { - cx.new(|cx| MessageEditor::new(workspace.downgrade(), project.clone(), window, cx)) + cx.new(|cx| { + MessageEditor::new( + workspace.downgrade(), + project.clone(), + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ) + }) }); let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 21f9f10db371ae849cf21f270d9f146f9445d3cf..cdd8a51221cc94a8e0b83a3ab439bda4f6b225bb 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,6 +1,6 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, + LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -13,11 +13,11 @@ use collections::{HashMap, HashSet}; use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; use file_icons::FileIcons; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, - SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, - Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, - linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, + EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, + PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::Buffer; use language::language_settings::SoftWrap; @@ -47,6 +47,9 @@ use crate::{ const RESPONSE_PADDING_X: Pixels = px(19.); +pub const MIN_EDITOR_LINES: usize = 4; +pub const MAX_EDITOR_LINES: usize = 8; + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -68,10 +71,17 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, terminal_expanded: bool, + editing_message: Option, _cancel_task: Option>, _subscriptions: [Subscription; 2], } +struct EditingMessage { + message_id: UserMessageId, + editor: Entity, + _subscription: Subscription, +} + enum ThreadState { Loading { _task: Task<()>, @@ -97,8 +107,18 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Self { - let message_editor = - cx.new(|cx| MessageEditor::new(workspace.clone(), project.clone(), window, cx)); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace.clone(), + project.clone(), + editor::EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }, + window, + cx, + ) + }); let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); @@ -124,6 +144,7 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, @@ -276,7 +297,7 @@ impl AcpThreadView { } } - pub fn cancel(&mut self, cx: &mut Context) { + pub fn cancel_generation(&mut self, cx: &mut Context) { self.last_error.take(); if let Some(thread) = self.thread() { @@ -297,7 +318,24 @@ impl AcpThreadView { fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { self.editor_expanded = is_expanded; self.message_editor.update(cx, |editor, cx| { - editor.set_expanded(is_expanded, cx); + if is_expanded { + editor.set_mode( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: false, + }, + cx, + ) + } else { + editor.set_mode( + EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }, + cx, + ) + } }); cx.notify(); } @@ -310,11 +348,12 @@ impl AcpThreadView { cx: &mut Context, ) { match event { - MessageEditorEvent::Chat => self.chat(window, cx), + MessageEditorEvent::Send => self.send(window, cx), + MessageEditorEvent::Cancel => self.cancel_generation(cx), } } - fn chat(&mut self, window: &mut Window, cx: &mut Context) { + fn send(&mut self, window: &mut Window, cx: &mut Context) { self.last_error.take(); let Some(thread) = self.thread().cloned() else { @@ -355,6 +394,76 @@ impl AcpThreadView { .detach(); } + fn send_impl( + &mut self, + contents: Task>>, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread().cloned() else { + return; + }; + let task = cx.spawn_in(window, async move |this, cx| { + let contents = contents.await?; + + if contents.is_empty() { + return Ok(()); + } + + this.update_in(cx, |this, window, cx| { + this.set_editor_is_expanded(false, cx); + this.scroll_to_bottom(cx); + this.message_editor.update(cx, |message_editor, cx| { + message_editor.clear(window, cx); + }); + })?; + let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + send.await + }); + + cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |this, cx| { + this.last_error = + Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); + cx.notify() + }) + .ok(); + } + }) + .detach(); + } + + fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { + self.editing_message.take(); + cx.notify(); + } + + fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + let Some(editing_message) = self.editing_message.take() else { + return; + }; + + self.last_error.take(); + + let Some(thread) = self.thread().cloned() else { + return; + }; + + let rewind = thread.update(cx, |thread, cx| { + thread.rewind(editing_message.message_id, cx) + }); + + let contents = editing_message + .editor + .update(cx, |message_editor, cx| message_editor.contents(cx)); + let task = cx.foreground_executor().spawn(async move { + rewind.await?; + contents.await + }); + self.send_impl(task, window, cx); + } + fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context) { if let Some(thread) = self.thread() { AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); @@ -611,6 +720,16 @@ impl AcpThreadView { cx.notify(); } + fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + thread + .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx)) + .detach_and_log_err(cx); + cx.notify(); + } + fn render_entry( &self, index: usize, @@ -621,8 +740,23 @@ impl AcpThreadView { ) -> AnyElement { let primary = match &entry { AgentThreadEntry::UserMessage(message) => div() + .id(("user_message", index)) .py_4() .px_2() + .children(message.id.clone().and_then(|message_id| { + message.checkpoint.as_ref()?; + + Some( + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })), + ) + })) .child( v_flex() .p_3() @@ -633,12 +767,28 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .children(message.content.markdown().map(|md| { - self.render_markdown( - md.clone(), - user_message_markdown_style(window, cx), - ) - })), + .id("message") + .on_click(cx.listener({ + move |this, _, window, cx| this.start_editing_message(index, window, cx) + })) + .children( + if let Some(editing) = self.editing_message.as_ref() + && Some(&editing.message_id) == message.id.as_ref() + { + Some( + self.render_edit_message_editor(editing, cx) + .into_any_element(), + ) + } else { + message.content.markdown().map(|md| { + self.render_markdown( + md.clone(), + user_message_markdown_style(window, cx), + ) + .into_any_element() + }) + }, + ), ) .into_any(), AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { @@ -2271,6 +2421,112 @@ impl AcpThreadView { .into_any() } + fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index) + else { + return; + }; + let Some(message_id) = message.id.clone() else { + return; + }; + let chunks = message.chunks.clone(); + let editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + editor.set_message(&chunks, window, cx); + editor + }); + let subscription = + cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { + MessageEditorEvent::Send => { + this.regenerate(&Default::default(), window, cx); + } + MessageEditorEvent::Cancel => { + this.cancel_editing(&Default::default(), window, cx); + } + }); + + self.editing_message.replace(EditingMessage { + message_id: message_id.clone(), + editor, + _subscription: subscription, + }); + cx.notify(); + } + + fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context) -> Div { + v_flex().child(editing.editor.clone()).child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Warning)) + .child( + Label::new("Editing will restart the thread from this point.") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(self.render_editing_message_editor_buttons(editing, cx)), + ) + } + + fn render_editing_message_editor_buttons( + &self, + editing: &EditingMessage, + cx: &Context, + ) -> Div { + h_flex() + .gap_0p5() + .child( + IconButton::new("cancel-edit-message", IconName::Close) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Error) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Cancel Edit", + &menu::Cancel, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::cancel_editing)), + ) + .child( + IconButton::new("confirm-edit-message", IconName::Return) + .disabled(editing.editor.read(cx).is_empty(cx)) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Regenerate", + &menu::Confirm, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::regenerate)), + ) + } + fn render_send_button(&self, cx: &mut Context) -> AnyElement { if self.thread().map_or(true, |thread| { thread.read(cx).status() == ThreadStatus::Idle @@ -2287,7 +2543,7 @@ impl AcpThreadView { button.tooltip(Tooltip::text("Type a message to submit")) }) .on_click(cx.listener(|this, _, window, cx| { - this.chat(window, cx); + this.send(window, cx); })) .into_any_element() } else { @@ -2297,7 +2553,7 @@ impl AcpThreadView { .tooltip(move |window, cx| { Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) + .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() } } @@ -3104,7 +3360,7 @@ pub(crate) mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3131,7 +3387,7 @@ pub(crate) mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3177,7 +3433,7 @@ pub(crate) mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 04166c5f31cab2c4a744c85e0bc5c6a4dd706f65..453460726f363efd2332e8e98c69aca7c225cf3b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -819,7 +819,9 @@ impl AgentPanel { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx)); + thread_view.update(cx, |thread_element, cx| { + thread_element.cancel_generation(cx) + }); } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index fad0c58b73c61c692a34f899aff70fb9bd6bed98..5c7e671159a62f40a36ea2ea857fd5a93995bf04 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -58,9 +58,7 @@ impl Assets { pub fn load_test_fonts(&self, cx: &App) { cx.text_system() .add_fonts(vec![ - self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf") - .unwrap() - .unwrap(), + self.load("fonts/lilex/Lilex-Regular.ttf").unwrap().unwrap(), ]) .unwrap() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index e25c02432d10c16969c963455188474e10ed04a6..c4c9f2004adedcda0a2215aa3f073ef10f5aa78e 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2290,8 +2290,6 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(init_test); - let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); - let text = "one two three\nfour five six\nseven eight"; let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 269f8f0c409cee7fa87b64cccb5310e4beb1edd2..caa4882a6ebbb00aaa1e498e49dfb530153a0e8e 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1223,7 +1223,7 @@ mod tests { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let font = test_font(); - let _font_id = text_system.font_id(&font); + let _font_id = text_system.resolve_font(&font); let font_size = px(14.0); log::info!("Tab size: {}", tab_size); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 0a9d5e9535d2b2d29e33ee49a8afa46a387d773e..f328945dbe6ae961d3fcb1ef5c80055b6adb0afb 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -53,7 +53,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = Font { - family: "Zed Plex Mono".into(), + family: ".ZedMono".into(), features: FontFeatures::default(), fallbacks: None, weight: FontWeight::default(), diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs index e6f6e9a6809d8bb7b2063abbb23d3b9b1567497d..f66a2e71d49f39c0e82770e23aa8eca752970daf 100644 --- a/crates/gpui/src/platform/linux/text_system.rs +++ b/crates/gpui/src/platform/linux/text_system.rs @@ -213,11 +213,7 @@ impl CosmicTextSystemState { features: &FontFeatures, ) -> Result> { // TODO: Determine the proper system UI font. - let name = if name == ".SystemUIFont" { - "Zed Plex Sans" - } else { - name - }; + let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans"); let families = self .font_system diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index c45888bce7172db0ec940d78549937e77657d6e3..849925c72772b70162f09fc680c0be2d6510878a 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -211,11 +211,7 @@ impl MacTextSystemState { features: &FontFeatures, fallbacks: Option<&FontFallbacks>, ) -> Result> { - let name = if name == ".SystemUIFont" { - ".AppleSystemUIFont" - } else { - name - }; + let name = crate::text_system::font_name_with_fallbacks(name, ".AppleSystemUIFont"); let mut font_ids = SmallVec::new(); let family = self diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 587cb7b4a6c18aca2ee4f8a10e453eeca3ade037..75cb50243b9c8ec845e256f4095cdedc40d2eea2 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -498,8 +498,9 @@ impl DirectWriteState { ) .unwrap() } else { + let family = self.system_ui_font_name.clone(); self.find_font_id( - target_font.family.as_ref(), + font_name_with_fallbacks(target_font.family.as_ref(), family.as_ref()), target_font.weight, target_font.style, &target_font.features, @@ -512,7 +513,6 @@ impl DirectWriteState { } #[cfg(not(any(test, feature = "test-support")))] { - let family = self.system_ui_font_name.clone(); log::error!("{} not found, use {} instead.", target_font.family, family); self.get_font_id_from_font_collection( family.as_ref(), diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index ed1307c6cdb797d85b5a48b0d5ef033f1ebddc36..b48c3a29350ab3c770c5eca765f7019ae1afd8f3 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -65,7 +65,7 @@ impl TextSystem { font_runs_pool: Mutex::default(), fallback_font_stack: smallvec![ // TODO: Remove this when Linux have implemented setting fallbacks. - font("Zed Plex Mono"), + font(".ZedMono"), font("Helvetica"), font("Segoe UI"), // Windows font("Cantarell"), // Gnome @@ -96,7 +96,7 @@ impl TextSystem { } /// Get the FontId for the configure font family and style. - pub fn font_id(&self, font: &Font) -> Result { + fn font_id(&self, font: &Font) -> Result { fn clone_font_id_result(font_id: &Result) -> Result { match font_id { Ok(font_id) => Ok(*font_id), @@ -844,3 +844,16 @@ impl FontMetrics { (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) } } + +#[allow(unused)] +pub(crate) fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { + // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" + // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, + // and so retained here for backward compatibility. + match name { + ".SystemUIFont" => system, + ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", + ".ZedMono" | "Zed Plex Mono" => "Lilex", + _ => name, + } +} diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 5de26511d333bb75515ba3f7b0c9c22e39f5be3f..648d714c89765d09623f154ef55ddd44d9716028 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -327,7 +327,7 @@ mod tests { fn build_wrapper() -> LineWrapper { let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); let cx = TestAppContext::build(dispatcher, None); - let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap(); + let id = cx.text_system().resolve_font(&font(".ZedMono")); LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) } diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index bf685bd9acfe9f678454dffc538ca66e3ca0910d..c651c7921d4d92562e68bfea6cb1954132c215bc 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -77,16 +77,16 @@ impl Render for MarkdownExample { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let markdown_style = MarkdownStyle { base_text_style: gpui::TextStyle { - font_family: "Zed Plex Sans".into(), + font_family: ".ZedSans".into(), color: cx.theme().colors().terminal_ansi_black, ..Default::default() }, code_block: StyleRefinement::default() - .font_family("Zed Plex Mono") + .font_family(".ZedMono") .m(rems(1.)) .bg(rgb(0xAAAAAAA)), inline_code: gpui::TextStyleRefinement { - font_family: Some("Zed Mono".into()), + font_family: Some(".ZedMono".into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(cx.theme().colors().editor_background), ..Default::default() diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs index f9cb26ed9a28304295806b8c8719f472290044ab..1a739d77668d5d241068b84b8b7f2211757b0953 100644 --- a/crates/prompt_store/src/prompt_store.rs +++ b/crates/prompt_store/src/prompt_store.rs @@ -58,6 +58,15 @@ pub enum PromptId { EditWorkflow, } +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"), + } + } +} + impl PromptId { pub fn new() -> PromptId { UserPromptId::new().into() diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 4c5b6272ef1f26d1fd065f76032e327ce59d1e12..ac01e6c5c88d042353a17f18281cd4d467c7c491 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -128,7 +128,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() - .font_family("Zed Plex Mono") + .font_family(".ZedMono") .child(self.story.clone()) } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 661bb71c917ab924cc2374561ebbff50b6387ea5..51bf2dd131c0183820f310c9e3b5fa4625aeb7f4 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -284,9 +284,7 @@ pub fn init(cx: &mut App) { let count = Vim::take_count(cx).unwrap_or(1) as f32; Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); - let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { - return; - }; + let font_id = window.text_system().resolve_font(&theme.buffer_font); let Ok(width) = window .text_system() .advance(font_id, theme.buffer_font_size(cx), 'm') @@ -300,9 +298,7 @@ pub fn init(cx: &mut App) { let count = Vim::take_count(cx).unwrap_or(1) as f32; Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); - let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { - return; - }; + let font_id = window.text_system().resolve_font(&theme.buffer_font); let Ok(width) = window .text_system() .advance(font_id, theme.buffer_font_size(cx), 'm') diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 23020d3a9b115e53638e5bbf96d4c0ba84720392..ceda403fdd204848cde573f3fb6dec461f1c0458 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4401,11 +4401,11 @@ mod tests { cx.text_system() .add_fonts(vec![ Assets - .load("fonts/plex-mono/ZedPlexMono-Regular.ttf") + .load("fonts/lilex/Lilex-Regular.ttf") .unwrap() .unwrap(), Assets - .load("fonts/plex-sans/ZedPlexSans-Regular.ttf") + .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") .unwrap() .unwrap(), ]) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5d11dfe8335f91d39a69fe583ed5dcdb24dcbbd6..b4cb1fcb9b99e3a23c31922cf50f7155992868e5 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -294,11 +294,11 @@ Define extensions which should be installed (`true`) or never installed (`false` - Description: The name of a font to use for rendering text in the editor. - Setting: `buffer_font_family` -- Default: `Zed Plex Mono` +- Default: `.ZedMono`. This currently aliases to [Lilex](https://lilex.myrt.co). **Options** -The name of any font family installed on the user's system +The name of any font family installed on the user's system, or `".ZedMono"`. ## Buffer Font Features @@ -3511,11 +3511,11 @@ Float values between `0.0` and `0.9`, where: - Description: The name of the font to use for text in the UI. - Setting: `ui_font_family` -- Default: `Zed Plex Sans` +- Default: `.ZedSans`. This currently aliases to [IBM Plex](https://www.ibm.com/plex/). **Options** -The name of any font family installed on the system. +The name of any font family installed on the system, `".ZedSans"` to use the Zed-provided default, or `".SystemUIFont"` to use the system's default UI font (on macOS and Windows). ## UI Font Features @@ -3603,7 +3603,7 @@ For example, to use `Nerd Font` as a fallback, add the following to your setting "soft_wrap": "none", "buffer_font_size": 18, - "buffer_font_family": "Zed Plex Mono", + "buffer_font_family": ".ZedMono", "autosave": "on_focus_change", "format_on_save": "off", diff --git a/docs/src/fonts.md b/docs/src/fonts.md deleted file mode 100644 index 93c687b13424ebdc08a923b02d0585c862189a8b..0000000000000000000000000000000000000000 --- a/docs/src/fonts.md +++ /dev/null @@ -1,56 +0,0 @@ -# Fonts - - - -Zed ships two fonts: Zed Plex Mono and Zed Plex Sans. These are based on IBM Plex Mono and IBM Plex Sans, respectively. - - - -## Settings - - - -- Buffer fonts - - `buffer-font-family` - - `buffer-font-features` - - `buffer-font-size` - - `buffer-line-height` -- UI fonts - - `ui_font_family` - - `ui_font_fallbacks` - - `ui_font_features` - - `ui_font_weight` - - `ui_font_size` -- Terminal fonts - - `terminal.font-size` - - `terminal.font-family` - - `terminal.font-features` - -## Old Zed Fonts - -Previously, Zed shipped with `Zed Mono` and `Zed Sans`, customized versions of the [Iosevka](https://typeof.net/Iosevka/) typeface. You can find more about them in the [zed-fonts](https://github.com/zed-industries/zed-fonts/) repository. - -Here's how you can use the old Zed fonts instead of `Zed Plex Mono` and `Zed Plex Sans`: - -1. Download [zed-app-fonts-1.2.0.zip](https://github.com/zed-industries/zed-fonts/releases/download/1.2.0/zed-app-fonts-1.2.0.zip) from the [zed-fonts releases](https://github.com/zed-industries/zed-fonts/releases) page. -2. Open macOS `Font Book.app` -3. Unzip the file and drag the `ttf` files into the Font Book app. -4. Update your settings `ui_font_family` and `buffer_font_family` to use `Zed Mono` or `Zed Sans` in your `settings.json` file. - -```json -{ - "ui_font_family": "Zed Sans Extended", - "buffer_font_family": "Zed Mono Extend", - "terminal": { - "font-family": "Zed Mono Extended" - } -} -``` - -5. Note there will be red squiggles under the font name. (this is a bug, but harmless.) diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 46de078d8940aa8ab75b3144a6a60cbbeaa3c329..7e75f6287d9c9aec741074cc5936641c178efd74 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -39,13 +39,15 @@ If you would like to use distinct themes for light mode/dark mode that can be se ## Fonts ```json - // UI Font. Use ".SystemUIFont" to use the default system font (SF Pro on macOS) - "ui_font_family": "Zed Plex Sans", + // UI Font. Use ".SystemUIFont" to use the default system font (SF Pro on macOS), + // or ".ZedSans" for the bundled default (currently IBM Plex) + "ui_font_family": ".SystemUIFont", "ui_font_weight": 400, // Font weight in standard CSS units from 100 to 900. "ui_font_size": 16, // Buffer Font - Used by editor buffers - "buffer_font_family": "Zed Plex Mono", // Font name for editor buffers + // use ".ZedMono" for the bundled default monospace (currently Lilex) + "buffer_font_family": "Berkeley Mono", // Font name for editor buffers "buffer_font_size": 15, // Font size for editor buffers "buffer_font_weight": 400, // Font weight in CSS units [100-900] // Line height "comfortable" (1.618), "standard" (1.3) or custom: `{ "custom": 2 }` @@ -53,7 +55,7 @@ If you would like to use distinct themes for light mode/dark mode that can be se // Terminal Font Settings "terminal": { - "font_family": "Zed Plex Mono", + "font_family": "", "font_size": 15, // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` "line_height": "comfortable", @@ -473,7 +475,7 @@ See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settin "show": null // Show/hide: (auto, system, always, never) }, // Terminal Font Settings - "font_family": "Zed Plex Mono", + "font_family": "Fira Code", "font_size": 15, "font_weight": 400, // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` diff --git a/nix/build.nix b/nix/build.nix index 70b4f76932fe3f0330b0c53163b668e3ff2c9d66..03403cc1c97f2dca0a42d7fb09bc5936d67e7cab 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -171,8 +171,8 @@ let ZSTD_SYS_USE_PKG_CONFIG = true; FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ - ../assets/fonts/plex-mono - ../assets/fonts/plex-sans + ../assets/fonts/lilex + ../assets/fonts/ibm-plex-sans ]; }; ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; diff --git a/nix/shell.nix b/nix/shell.nix index b78eb5c00138579db6da861d4e58232b12895da0..b6f1efd366b32cdb246f3884856643977d1b3552 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -46,8 +46,8 @@ # outside the nix store instead of to `$src` FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ - "./assets/fonts/plex-mono" - "./assets/fonts/plex-sans" + "./assets/fonts/lilex" + "./assets/fonts/ibm-plex-sans" ]; }; PROTOC = "${protobuf}/bin/protoc";