diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b70271e57..f4ba227168 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 3b1337eece..47446b6a13 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 0000000000..1d66b1a2e9 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 0000000000..e07bc1f527 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 0000000000..efe8a1fb9d 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 0000000000..bd6817d520 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 0000000000..45930ee30b 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 0000000000..10c6ab5f74 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 0000000000..e7aef10f7e 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 0000000000..cb98a69b0f 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 f72f76504c..156240bc90 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 d5f4b5e285..0000000000 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 05eaf7cccd..0000000000 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 3b07821757..0000000000 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 61dbb58361..0000000000 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 f1e66392f7..0000000000 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 7612dc5167..0000000000 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 8769c232ee..0000000000 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 3ea293d59a..0000000000 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 0f1818ac7f..2c3bf6930d 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 b3ec217bad..2d0fe2d264 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 f8a5bf8032..b82fe74f1b 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 59c479d87b..5dc6e13786 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,58 +49,211 @@ 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 to_uri(&self) -> String { + pub fn icon_path(&self, cx: &mut App) -> SharedString { match self { - MentionUri::File(path) => { - format!("file://{}", path.display()) - } - MentionUri::Symbol(path, name) => { - format!("file://{}#{}", path.display(), name) - } - MentionUri::Thread(thread) => { - format!("zed:///agent/thread/{}", thread.0) - } - MentionUri::Rule(rule) => { - format!("zed:///agent/rule/{}", rule) + 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 as_link<'a>(&'a self) -> MentionLink<'a> { + MentionLink(self) + } + + pub fn to_uri(&self) -> Url { + match self { + 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::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> { + 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)] @@ -76,50 +261,191 @@ 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(), 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_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().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] + 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 thread URI - let thread_uri = "zed:///agent/thread/session123"; + #[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] + 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 invalid scheme - assert!(MentionUri::parse("http://example.com").is_err()); + #[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 zed path + #[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] + 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 3c2bea53a7..e1dfaca050 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 a79356d9ac..400a54b491 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 21f9f10db3..cdd8a51221 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 04166c5f31..453460726f 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 fad0c58b73..5c7e671159 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 e25c02432d..c4c9f2004a 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 269f8f0c40..caa4882a6e 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 0a9d5e9535..f328945dbe 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 e6f6e9a680..f66a2e71d4 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 c45888bce7..849925c727 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 587cb7b4a6..75cb50243b 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 ed1307c6cd..b48c3a2935 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 5de26511d3..648d714c89 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 bf685bd9ac..c651c7921d 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 f9cb26ed9a..1a739d7766 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 4c5b6272ef..ac01e6c5c8 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 661bb71c91..51bf2dd131 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 23020d3a9b..ceda403fdd 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 5d11dfe833..b4cb1fcb9b 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 93c687b134..0000000000 --- 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 46de078d89..7e75f6287d 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 70b4f76932..03403cc1c9 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 b78eb5c001..b6f1efd366 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";