Merge branch 'main' into message-editor

This commit is contained in:
Conrad Irwin 2025-08-13 15:37:54 -06:00
commit 2aa1255ddb
47 changed files with 796 additions and 259 deletions

View file

@ -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

3
Cargo.lock generated
View file

@ -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",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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.
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -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

View file

@ -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

View file

@ -32,6 +32,7 @@ use util::ResultExt;
pub struct UserMessage {
pub id: Option<UserMessageId>,
pub content: ContentBlock,
pub chunks: Vec<acp::ContentBlock>,
pub checkpoint: Option<GitStoreCheckpoint>,
}
@ -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,

View file

@ -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<u32>,
},
Thread {
id: ThreadId,
name: String,
},
TextThread {
path: PathBuf,
name: String,
},
Rule {
id: PromptId,
name: String,
},
Selection {
path: PathBuf,
line_range: Range<u32>,
},
Fetch {
url: Url,
},
}
impl MentionUri {
@ -17,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::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
path: path.into(),
line_range,
})
} else {
Ok(Self::Selection {
path: path.into(),
line_range,
})
}
} else {
let file_path =
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
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<Option<String>> {
let pairs = url.query_pairs().collect::<Vec<_>>();
match pairs.as_slice() {
[] => Ok(None),
[(k, v)] => {
if k != name {
bail!("invalid query parameter")
}
Ok(Some(v.to_string()))
}
_ => bail!("too many query pairs"),
}
}
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
format!(
"{} ({}:{})",
path.file_name().unwrap_or_default().display(),
line_range.start + 1,
line_range.end + 1
)
}
#[cfg(test)]
@ -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());
}
}

View file

@ -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<Project>,

View file

@ -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<Editor>,
project: Entity<Project>,
@ -39,7 +36,8 @@ pub struct MessageEditor {
}
pub enum MessageEditorEvent {
Chat,
Send,
Cancel,
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@ -48,6 +46,7 @@ impl MessageEditor {
pub fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
mode: EditorMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) {
cx.emit(MessageEditorEvent::Chat)
cx.emit(MessageEditorEvent::Send)
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
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<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
project: Entity<Project>,
message: Option<&[acp::ContentBlock]>,
pub fn set_message(
&mut self,
message: &[acp::ContentBlock],
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<BufferSnapshot> {
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());

View file

@ -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<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
@ -68,10 +71,17 @@ pub struct AcpThreadView {
plan_expanded: bool,
editor_expanded: bool,
terminal_expanded: bool,
editing_message: Option<EditingMessage>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 2],
}
struct EditingMessage {
message_id: UserMessageId,
editor: Entity<MessageEditor>,
_subscription: Subscription,
}
enum ThreadState {
Loading {
_task: Task<()>,
@ -97,8 +107,18 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) {
pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
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>) {
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<Self>,
) {
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<Self>) {
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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<anyhow::Result<Vec<acp::ContentBlock>>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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>) {
self.editing_message.take();
cx.notify();
}
fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>) -> 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<Self>,
) -> 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<Self>) -> 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();

View file

@ -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 => {}
}

View file

@ -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()
}

View file

@ -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));

View file

@ -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);

View file

@ -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(),

View file

@ -213,11 +213,7 @@ impl CosmicTextSystemState {
features: &FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
// 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

View file

@ -211,11 +211,7 @@ impl MacTextSystemState {
features: &FontFeatures,
fallbacks: Option<&FontFallbacks>,
) -> Result<SmallVec<[FontId; 4]>> {
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

View file

@ -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(),

View file

@ -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<FontId> {
fn font_id(&self, font: &Font) -> Result<FontId> {
fn clone_font_id_result(font_id: &Result<FontId>) -> Result<FontId> {
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,
}
}

View file

@ -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())
}

View file

@ -77,16 +77,16 @@ impl Render for MarkdownExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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()

View file

@ -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()

View file

@ -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())
}
}

View file

@ -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')

View file

@ -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(),
])

View file

@ -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",

View file

@ -1,56 +0,0 @@
# Fonts
<!--
TBD: WIP. Zed Fonts documentation. This is currently not linked from SUMMARY.md are so unpublished.
-->
Zed ships two fonts: Zed Plex Mono and Zed Plex Sans. These are based on IBM Plex Mono and IBM Plex Sans, respectively.
<!--
TBD: Document how Zed Plex font files were created. Repo links, etc.
-->
## Settings
<!--
TBD: Explain various font settings in Zed.
-->
- 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.)

View file

@ -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 }`

View file

@ -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.";

View file

@ -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";