Merge pull request #2300 from zed-industries/ligatures

Antonio Scandurra created

Allow customization of OpenType features

Change summary

Cargo.lock                                      |  28 +
Cargo.toml                                      |   1 
assets/settings/default.json                    |   5 
crates/auto_update/Cargo.toml                   |   1 
crates/cli/Cargo.toml                           |   1 
crates/client/Cargo.toml                        |   3 
crates/collab/Cargo.toml                        |   1 
crates/collab_ui/Cargo.toml                     |   1 
crates/db/Cargo.toml                            |   1 
crates/editor/Cargo.toml                        |   1 
crates/editor/src/display_map.rs                |  33 +
crates/editor/src/display_map/block_map.rs      |  15 
crates/editor/src/display_map/wrap_map.rs       |   4 
crates/editor/src/movement.rs                   |   5 
crates/editor/src/test.rs                       |   5 
crates/feedback/Cargo.toml                      |   3 
crates/fs/Cargo.toml                            |   1 
crates/gpui/Cargo.toml                          |   2 
crates/gpui/examples/text.rs                    |   5 
crates/gpui/src/elements/label.rs               |   2 
crates/gpui/src/font_cache.rs                   |  35 +
crates/gpui/src/fonts.rs                        |  51 ++
crates/gpui/src/platform.rs                     |   7 
crates/gpui/src/platform/mac/fonts.rs           |  25 
crates/gpui/src/platform/mac/fonts/open_type.rs | 395 +++++++++++++++++++
crates/gpui/src/text_layout.rs                  |   8 
crates/language/Cargo.toml                      |   1 
crates/live_kit_client/Cargo.toml               |   4 
crates/live_kit_server/Cargo.toml               |   1 
crates/lsp/Cargo.toml                           |   1 
crates/plugin/Cargo.toml                        |   1 
crates/plugin_macros/Cargo.toml                 |   1 
crates/plugin_runtime/Cargo.toml                |   1 
crates/project/Cargo.toml                       |   1 
crates/rpc/Cargo.toml                           |   1 
crates/search/Cargo.toml                        |   1 
crates/settings/Cargo.toml                      |   1 
crates/settings/src/settings.rs                 |  44 +
crates/terminal/Cargo.toml                      |   1 
crates/terminal_view/Cargo.toml                 |   1 
crates/terminal_view/src/terminal_element.rs    |  15 
crates/theme/Cargo.toml                         |   1 
crates/theme/src/theme_registry.rs              |  11 
crates/vim/Cargo.toml                           |   1 
crates/workspace/Cargo.toml                     |   3 
crates/zed/Cargo.toml                           |   1 
plugins/Cargo.lock                              |   3 
plugins/json_language/Cargo.toml                |   1 
styles/src/styleTree/components.ts              |  74 +++
49 files changed, 750 insertions(+), 59 deletions(-)

Detailed changes

Cargo.lock šŸ”—

@@ -518,6 +518,7 @@ dependencies = [
  "menu",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smol",
@@ -1097,6 +1098,7 @@ dependencies = [
  "ipc-channel",
  "plist",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -1119,6 +1121,7 @@ dependencies = [
  "rand 0.8.5",
  "rpc",
  "serde",
+ "serde_derive",
  "settings",
  "smol",
  "sum_tree",
@@ -1228,6 +1231,7 @@ dependencies = [
  "sea-orm",
  "sea-query",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "sha-1 0.9.8",
@@ -1269,6 +1273,7 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "settings",
  "theme",
  "util",
@@ -1739,6 +1744,7 @@ dependencies = [
  "log",
  "parking_lot 0.11.2",
  "serde",
+ "serde_derive",
  "smol",
  "sqlez",
  "sqlez_macros",
@@ -1947,6 +1953,7 @@ dependencies = [
  "rand 0.8.5",
  "rpc",
  "serde",
+ "serde_derive",
  "settings",
  "smallvec",
  "smol",
@@ -2100,6 +2107,7 @@ dependencies = [
  "project",
  "search",
  "serde",
+ "serde_derive",
  "settings",
  "sysinfo",
  "theme",
@@ -2296,6 +2304,7 @@ dependencies = [
  "regex",
  "rope",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "tempfile",
@@ -2664,8 +2673,10 @@ dependencies = [
  "postage",
  "rand 0.8.5",
  "resvg",
+ "schemars",
  "seahash",
  "serde",
+ "serde_derive",
  "serde_json",
  "simplelog",
  "smallvec",
@@ -3275,6 +3286,7 @@ dependencies = [
  "regex",
  "rpc",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "similar",
@@ -3470,6 +3482,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "serde",
+ "serde_derive",
  "serde_json",
  "sha2 0.10.6",
  "simplelog",
@@ -3490,6 +3503,7 @@ dependencies = [
  "prost-types 0.8.0",
  "reqwest",
  "serde",
+ "serde_derive",
  "sha2 0.10.6",
 ]
 
@@ -3530,6 +3544,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "unindent",
@@ -4449,6 +4464,7 @@ dependencies = [
  "bincode",
  "plugin_macros",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -4459,6 +4475,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
+ "serde_derive",
  "syn",
 ]
 
@@ -4470,6 +4487,7 @@ dependencies = [
  "bincode",
  "pollster",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "wasi-common",
@@ -4627,6 +4645,7 @@ dependencies = [
  "regex",
  "rpc",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "sha2 0.10.6",
@@ -5249,6 +5268,7 @@ dependencies = [
  "rand 0.8.5",
  "rsa",
  "serde",
+ "serde_derive",
  "smol",
  "smol-timeout",
  "tempdir",
@@ -5672,6 +5692,7 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smallvec",
@@ -5860,6 +5881,7 @@ dependencies = [
  "postage",
  "schemars",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "sqlez",
@@ -6480,6 +6502,7 @@ dependencies = [
  "procinfo",
  "rand 0.8.5",
  "serde",
+ "serde_derive",
  "settings",
  "shellexpand",
  "smallvec",
@@ -6511,6 +6534,7 @@ dependencies = [
  "project",
  "rand 0.8.5",
  "serde",
+ "serde_derive",
  "settings",
  "shellexpand",
  "smallvec",
@@ -6570,6 +6594,7 @@ dependencies = [
  "indexmap",
  "parking_lot 0.11.2",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "toml",
@@ -7563,6 +7588,7 @@ dependencies = [
  "project",
  "search",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "tokio",
@@ -8346,6 +8372,7 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smallvec",
@@ -8472,6 +8499,7 @@ dependencies = [
  "rust-embed",
  "search",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "settings",

Cargo.toml šŸ”—

@@ -68,6 +68,7 @@ resolver = "2"
 
 [workspace.dependencies]
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
 

assets/settings/default.json šŸ”—

@@ -3,6 +3,11 @@
     "theme": "One Dark",
     // The name of a font to use for rendering text in the editor
     "buffer_font_family": "Zed Mono",
+    // The OpenType features to enable for text in the editor.
+    "buffer_font_features": {
+        // Disable ligatures:
+        // "calt": false
+    },
     // The default font size for text in the editor
     "buffer_font_size": 15,
     // The factor to grow the active pane by. Defaults to 1.0

crates/auto_update/Cargo.toml šŸ”—

@@ -23,6 +23,7 @@ isahc = "1.7"
 lazy_static = "1.4"
 log = "0.4"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 smol = "1.2.5"
 tempdir = "0.3.7"

crates/cli/Cargo.toml šŸ”—

@@ -18,6 +18,7 @@ clap = { version = "3.1", features = ["derive"] }
 dirs = "3.0"
 ipc-channel = "0.16"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation = "0.9"

crates/client/Cargo.toml šŸ”—

@@ -35,7 +35,8 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
 tiny_http = "0.8"
 uuid = { version = "1.1.2", features = ["v4"] }
 url = "2.2"
-serde = { version = "*", features = ["derive"] }
+serde = { version = "*", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 settings = { path = "../settings" }
 tempfile = "3"
 

crates/collab/Cargo.toml šŸ”—

@@ -42,6 +42,7 @@ scrypt = "0.7"
 sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
 sea-query = "0.27"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = "1.0"
 sha-1 = "0.9"
 sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }

crates/collab_ui/Cargo.toml šŸ”—

@@ -44,6 +44,7 @@ futures = "0.3"
 log = "0.4"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

crates/db/Cargo.toml šŸ”—

@@ -24,6 +24,7 @@ lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 serde = { version = "1.0", features = ["derive"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 smol = "1.2"
 
 [dev-dependencies]

crates/editor/Cargo.toml šŸ”—

@@ -54,6 +54,7 @@ parking_lot = "0.11"
 postage = { version = "0.4", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }
 serde = { workspace = true }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 tree-sitter-rust = { version = "*", optional = true }

crates/editor/src/display_map.rs šŸ”—

@@ -785,7 +785,9 @@ pub mod tests {
         let mut tab_size = rng.gen_range(1..=4);
         let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
         let excerpt_header_height = rng.gen_range(1..=5);
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1042,7 +1044,9 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1131,7 +1135,10 @@ pub mod tests {
         cx.set_global(Settings::test(cx));
         let text = sample_text(6, 6, 'a');
         let buffer = MultiBuffer::build_simple(&text, cx);
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1214,7 +1221,9 @@ pub mod tests {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1302,7 +1311,9 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let family_id = font_cache.load_family(&["Courier"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1374,7 +1385,9 @@ pub mod tests {
         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Courier"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1490,7 +1503,9 @@ pub mod tests {
         let text = "āœ…\t\tα\nβ\t\nšŸ€Ī²\t\tγ";
         let buffer = MultiBuffer::build_simple(text, cx);
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1548,7 +1563,9 @@ pub mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();

crates/editor/src/display_map/block_map.rs šŸ”—

@@ -1015,7 +1015,10 @@ mod tests {
     fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
 
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1185,7 +1188,10 @@ mod tests {
     fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
 
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1241,7 +1247,10 @@ mod tests {
             Some(rng.gen_range(0.0..=100.0))
         };
         let tab_size = 1.try_into().unwrap();
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())

crates/editor/src/display_map/wrap_map.rs šŸ”—

@@ -1053,7 +1053,9 @@ mod tests {
             Some(rng.gen_range(0.0..=1000.0))
         };
         let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();

crates/editor/src/movement.rs šŸ”—

@@ -587,7 +587,10 @@ mod tests {
     #[gpui::test]
     fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())

crates/editor/src/test.rs šŸ”—

@@ -25,7 +25,10 @@ pub fn marked_display_snapshot(
 ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
     let (unmarked_text, markers) = marked_text_offsets(text);
 
-    let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+    let family_id = cx
+        .font_cache()
+        .load_family(&["Helvetica"], &Default::default())
+        .unwrap();
     let font_id = cx
         .font_cache()
         .select_font(family_id, &Default::default())

crates/feedback/Cargo.toml šŸ”—

@@ -25,10 +25,11 @@ postage = { version = "0.4", features = ["futures-traits"] }
 project = { path = "../project" }
 search = { path = "../search" }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 settings = { path = "../settings" }
 sysinfo = "0.27.1"
 theme = { path = "../theme" }
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { path = "../workspace" }

crates/fs/Cargo.toml šŸ”—

@@ -24,6 +24,7 @@ smol = "1.2.5"
 regex = "1.5"
 git2 = { version = "0.15", default-features = false }
 serde = { workspace = true }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { workspace = true }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 libc = "0.2"

crates/gpui/Cargo.toml šŸ”—

@@ -39,8 +39,10 @@ pathfinder_geometry = "0.5"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"
 resvg = "0.14"
+schemars = "0.8"
 seahash = "4.1"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = "1.0"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"

crates/gpui/examples/text.rs šŸ”—

@@ -56,7 +56,10 @@ impl gpui::Element for TextElement {
         cx: &mut gpui::PaintContext,
     ) -> Self::PaintState {
         let font_size = 12.;
-        let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap();
+        let family = cx
+            .font_cache
+            .load_family(&["SF Pro Display"], &Default::default())
+            .unwrap();
         let normal = RunStyle {
             font_id: cx
                 .font_cache

crates/gpui/src/elements/label.rs šŸ”—

@@ -216,6 +216,7 @@ mod tests {
             12.,
             Default::default(),
             Default::default(),
+            Default::default(),
             Color::black(),
             cx.font_cache(),
         )
@@ -225,6 +226,7 @@ mod tests {
             12.,
             *FontProperties::new().weight(Weight::BOLD),
             Default::default(),
+            Default::default(),
             Color::new(255, 0, 0, 255),
             cx.font_cache(),
         )

crates/gpui/src/font_cache.rs šŸ”—

@@ -1,5 +1,5 @@
 use crate::{
-    fonts::{FontId, Metrics, Properties},
+    fonts::{Features, FontId, Metrics, Properties},
     geometry::vector::{vec2f, Vector2F},
     platform,
     text_layout::LineWrapper,
@@ -18,6 +18,7 @@ pub struct FamilyId(usize);
 
 struct Family {
     name: Arc<str>,
+    font_features: Features,
     font_ids: Vec<FontId>,
 }
 
@@ -58,17 +59,21 @@ impl FontCache {
             .map(|family| family.name.clone())
     }
 
-    pub fn load_family(&self, names: &[&str]) -> Result<FamilyId> {
+    pub fn load_family(&self, names: &[&str], features: &Features) -> Result<FamilyId> {
         for name in names {
             let state = self.0.upgradable_read();
 
-            if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
+            if let Some(ix) = state
+                .families
+                .iter()
+                .position(|f| f.name.as_ref() == *name && f.font_features == *features)
+            {
                 return Ok(FamilyId(ix));
             }
 
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
 
-            if let Ok(font_ids) = state.fonts.load_family(name) {
+            if let Ok(font_ids) = state.fonts.load_family(name, features) {
                 if font_ids.is_empty() {
                     continue;
                 }
@@ -82,6 +87,7 @@ impl FontCache {
 
                 state.families.push(Family {
                     name: Arc::from(*name),
+                    font_features: features.clone(),
                     font_ids,
                 });
                 return Ok(family_id);
@@ -254,7 +260,15 @@ mod tests {
     fn test_select_font() {
         let platform = test::platform();
         let fonts = FontCache::new(platform.fonts());
-        let arial = fonts.load_family(&["Arial"]).unwrap();
+        let arial = fonts
+            .load_family(
+                &["Arial"],
+                &Features {
+                    calt: Some(false),
+                    ..Default::default()
+                },
+            )
+            .unwrap();
         let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
         let arial_italic = fonts
             .select_font(arial, Properties::new().style(Style::Italic))
@@ -265,5 +279,16 @@ mod tests {
         assert_ne!(arial_regular, arial_italic);
         assert_ne!(arial_regular, arial_bold);
         assert_ne!(arial_italic, arial_bold);
+
+        let arial_with_calt = fonts
+            .load_family(
+                &["Arial"],
+                &Features {
+                    calt: Some(true),
+                    ..Default::default()
+                },
+            )
+            .unwrap();
+        assert_ne!(arial_with_calt, arial);
     }
 }

crates/gpui/src/fonts.rs šŸ”—

@@ -11,7 +11,8 @@ pub use font_kit::{
     properties::{Properties, Stretch, Style, Weight},
 };
 use ordered_float::OrderedFloat;
-use serde::{de, Deserialize};
+use schemars::JsonSchema;
+use serde::{de, Deserialize, Serialize};
 use serde_json::Value;
 use std::{cell::RefCell, sync::Arc};
 
@@ -20,6 +21,44 @@ pub struct FontId(pub usize);
 
 pub type GlyphId = u32;
 
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct Features {
+    pub calt: Option<bool>,
+    pub case: Option<bool>,
+    pub cpsp: Option<bool>,
+    pub frac: Option<bool>,
+    pub liga: Option<bool>,
+    pub onum: Option<bool>,
+    pub ordn: Option<bool>,
+    pub pnum: Option<bool>,
+    pub ss01: Option<bool>,
+    pub ss02: Option<bool>,
+    pub ss03: Option<bool>,
+    pub ss04: Option<bool>,
+    pub ss05: Option<bool>,
+    pub ss06: Option<bool>,
+    pub ss07: Option<bool>,
+    pub ss08: Option<bool>,
+    pub ss09: Option<bool>,
+    pub ss10: Option<bool>,
+    pub ss11: Option<bool>,
+    pub ss12: Option<bool>,
+    pub ss13: Option<bool>,
+    pub ss14: Option<bool>,
+    pub ss15: Option<bool>,
+    pub ss16: Option<bool>,
+    pub ss17: Option<bool>,
+    pub ss18: Option<bool>,
+    pub ss19: Option<bool>,
+    pub ss20: Option<bool>,
+    pub subs: Option<bool>,
+    pub sups: Option<bool>,
+    pub swsh: Option<bool>,
+    pub titl: Option<bool>,
+    pub tnum: Option<bool>,
+    pub zero: Option<bool>,
+}
+
 #[derive(Clone, Debug)]
 pub struct TextStyle {
     pub color: Color,
@@ -71,6 +110,8 @@ thread_local! {
 struct TextStyleJson {
     color: Color,
     family: String,
+    #[serde(default)]
+    features: Features,
     weight: Option<WeightJson>,
     size: f32,
     #[serde(default)]
@@ -107,12 +148,13 @@ impl TextStyle {
         font_family_name: impl Into<Arc<str>>,
         font_size: f32,
         font_properties: Properties,
+        font_features: Features,
         underline: Underline,
         color: Color,
         font_cache: &FontCache,
     ) -> Result<Self> {
         let font_family_name = font_family_name.into();
-        let font_family_id = font_cache.load_family(&[&font_family_name])?;
+        let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
         let font_id = font_cache.select_font(font_family_id, &font_properties)?;
         Ok(Self {
             color,
@@ -175,6 +217,7 @@ impl TextStyle {
                     json.family,
                     json.size,
                     font_properties,
+                    json.features,
                     underline_from_json(json.underline),
                     json.color,
                     font_cache,
@@ -253,7 +296,9 @@ impl Default for TextStyle {
                 .expect("TextStyle::default can only be called within a call to with_font_cache");
 
             let font_family_name = Arc::from("Courier");
-            let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+            let font_family_id = font_cache
+                .load_family(&[&font_family_name], &Default::default())
+                .unwrap();
             let font_id = font_cache
                 .select_font(font_family_id, &Default::default())
                 .unwrap();

crates/gpui/src/platform.rs šŸ”—

@@ -9,7 +9,10 @@ pub mod current {
 
 use crate::{
     executor,
-    fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
+    fonts::{
+        Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
+        Properties as FontProperties,
+    },
     geometry::{
         rect::{RectF, RectI},
         vector::Vector2F,
@@ -335,7 +338,7 @@ pub enum RasterizationOptions {
 
 pub trait FontSystem: Send + Sync {
     fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
-    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
+    fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
     fn select_font(
         &self,
         font_ids: &[FontId],

crates/gpui/src/platform/mac/fonts.rs šŸ”—

@@ -1,5 +1,7 @@
+mod open_type;
+
 use crate::{
-    fonts::{FontId, GlyphId, Metrics, Properties},
+    fonts::{Features, FontId, GlyphId, Metrics, Properties},
     geometry::{
         rect::{RectF, RectI},
         transform2d::Transform2F,
@@ -64,8 +66,8 @@ impl platform::FontSystem for FontSystem {
         self.0.write().add_fonts(fonts)
     }
 
-    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
-        self.0.write().load_family(name)
+    fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
+        self.0.write().load_family(name, features)
     }
 
     fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
@@ -126,7 +128,7 @@ impl FontSystemState {
         Ok(())
     }
 
-    fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
+    fn load_family(&mut self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
         let mut font_ids = Vec::new();
 
         let family = self
@@ -134,7 +136,8 @@ impl FontSystemState {
             .select_family_by_name(name)
             .or_else(|_| self.system_source.select_family_by_name(name))?;
         for font in family.fonts() {
-            let font = font.load()?;
+            let mut font = font.load()?;
+            open_type::apply_features(&mut font, features);
             let font_id = FontId(self.fonts.len());
             font_ids.push(font_id);
             let postscript_name = font.postscript_name().unwrap();
@@ -503,7 +506,7 @@ mod tests {
     fn test_layout_str(_: &mut MutableAppContext) {
         // This is failing intermittently on CI and we don't have time to figure it out
         let fonts = FontSystem::new();
-        let menlo = fonts.load_family("Menlo").unwrap();
+        let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
         let menlo_regular = RunStyle {
             font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
             color: Default::default(),
@@ -544,13 +547,13 @@ mod tests {
     #[test]
     fn test_glyph_offsets() -> anyhow::Result<()> {
         let fonts = FontSystem::new();
-        let zapfino = fonts.load_family("Zapfino")?;
+        let zapfino = fonts.load_family("Zapfino", &Default::default())?;
         let zapfino_regular = RunStyle {
             font_id: fonts.select_font(&zapfino, &Properties::new())?,
             color: Default::default(),
             underline: Default::default(),
         };
-        let menlo = fonts.load_family("Menlo")?;
+        let menlo = fonts.load_family("Menlo", &Default::default())?;
         let menlo_regular = RunStyle {
             font_id: fonts.select_font(&menlo, &Properties::new())?,
             color: Default::default(),
@@ -584,7 +587,7 @@ mod tests {
         use std::{fs::File, io::BufWriter, path::Path};
 
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Fira Code").unwrap();
+        let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
         let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
 
@@ -618,7 +621,7 @@ mod tests {
     #[test]
     fn test_wrap_line() {
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Helvetica").unwrap();
+        let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
 
         let line = "one two three four five\n";
@@ -636,7 +639,7 @@ mod tests {
     #[test]
     fn test_layout_line_bom_char() {
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Helvetica").unwrap();
+        let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
         let style = RunStyle {
             font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
             color: Default::default(),

crates/gpui/src/platform/mac/fonts/open_type.rs šŸ”—

@@ -0,0 +1,395 @@
+#![allow(unused, non_upper_case_globals)]
+
+use std::ptr;
+
+use crate::fonts::Features;
+use cocoa::appkit::CGFloat;
+use core_foundation::{base::TCFType, number::CFNumber};
+use core_graphics::geometry::CGAffineTransform;
+use core_text::{
+    font::{CTFont, CTFontRef},
+    font_descriptor::{
+        CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
+    },
+};
+use font_kit::font::Font;
+
+const kCaseSensitiveLayoutOffSelector: i32 = 1;
+const kCaseSensitiveLayoutOnSelector: i32 = 0;
+const kCaseSensitiveLayoutType: i32 = 33;
+const kCaseSensitiveSpacingOffSelector: i32 = 3;
+const kCaseSensitiveSpacingOnSelector: i32 = 2;
+const kCharacterAlternativesType: i32 = 17;
+const kCommonLigaturesOffSelector: i32 = 3;
+const kCommonLigaturesOnSelector: i32 = 2;
+const kContextualAlternatesOffSelector: i32 = 1;
+const kContextualAlternatesOnSelector: i32 = 0;
+const kContextualAlternatesType: i32 = 36;
+const kContextualLigaturesOffSelector: i32 = 19;
+const kContextualLigaturesOnSelector: i32 = 18;
+const kContextualSwashAlternatesOffSelector: i32 = 5;
+const kContextualSwashAlternatesOnSelector: i32 = 4;
+const kDefaultLowerCaseSelector: i32 = 0;
+const kDefaultUpperCaseSelector: i32 = 0;
+const kDiagonalFractionsSelector: i32 = 2;
+const kFractionsType: i32 = 11;
+const kHistoricalLigaturesOffSelector: i32 = 21;
+const kHistoricalLigaturesOnSelector: i32 = 20;
+const kHojoCharactersSelector: i32 = 12;
+const kInferiorsSelector: i32 = 2;
+const kJIS2004CharactersSelector: i32 = 11;
+const kLigaturesType: i32 = 1;
+const kLowerCasePetiteCapsSelector: i32 = 2;
+const kLowerCaseSmallCapsSelector: i32 = 1;
+const kLowerCaseType: i32 = 37;
+const kLowerCaseNumbersSelector: i32 = 0;
+const kMathematicalGreekOffSelector: i32 = 11;
+const kMathematicalGreekOnSelector: i32 = 10;
+const kMonospacedNumbersSelector: i32 = 0;
+const kNLCCharactersSelector: i32 = 13;
+const kNoFractionsSelector: i32 = 0;
+const kNormalPositionSelector: i32 = 0;
+const kNoStyleOptionsSelector: i32 = 0;
+const kNumberCaseType: i32 = 21;
+const kNumberSpacingType: i32 = 6;
+const kOrdinalsSelector: i32 = 3;
+const kProportionalNumbersSelector: i32 = 1;
+const kQuarterWidthTextSelector: i32 = 4;
+const kScientificInferiorsSelector: i32 = 4;
+const kSlashedZeroOffSelector: i32 = 5;
+const kSlashedZeroOnSelector: i32 = 4;
+const kStyleOptionsType: i32 = 19;
+const kStylisticAltEighteenOffSelector: i32 = 37;
+const kStylisticAltEighteenOnSelector: i32 = 36;
+const kStylisticAltEightOffSelector: i32 = 17;
+const kStylisticAltEightOnSelector: i32 = 16;
+const kStylisticAltElevenOffSelector: i32 = 23;
+const kStylisticAltElevenOnSelector: i32 = 22;
+const kStylisticAlternativesType: i32 = 35;
+const kStylisticAltFifteenOffSelector: i32 = 31;
+const kStylisticAltFifteenOnSelector: i32 = 30;
+const kStylisticAltFiveOffSelector: i32 = 11;
+const kStylisticAltFiveOnSelector: i32 = 10;
+const kStylisticAltFourOffSelector: i32 = 9;
+const kStylisticAltFourOnSelector: i32 = 8;
+const kStylisticAltFourteenOffSelector: i32 = 29;
+const kStylisticAltFourteenOnSelector: i32 = 28;
+const kStylisticAltNineOffSelector: i32 = 19;
+const kStylisticAltNineOnSelector: i32 = 18;
+const kStylisticAltNineteenOffSelector: i32 = 39;
+const kStylisticAltNineteenOnSelector: i32 = 38;
+const kStylisticAltOneOffSelector: i32 = 3;
+const kStylisticAltOneOnSelector: i32 = 2;
+const kStylisticAltSevenOffSelector: i32 = 15;
+const kStylisticAltSevenOnSelector: i32 = 14;
+const kStylisticAltSeventeenOffSelector: i32 = 35;
+const kStylisticAltSeventeenOnSelector: i32 = 34;
+const kStylisticAltSixOffSelector: i32 = 13;
+const kStylisticAltSixOnSelector: i32 = 12;
+const kStylisticAltSixteenOffSelector: i32 = 33;
+const kStylisticAltSixteenOnSelector: i32 = 32;
+const kStylisticAltTenOffSelector: i32 = 21;
+const kStylisticAltTenOnSelector: i32 = 20;
+const kStylisticAltThirteenOffSelector: i32 = 27;
+const kStylisticAltThirteenOnSelector: i32 = 26;
+const kStylisticAltThreeOffSelector: i32 = 7;
+const kStylisticAltThreeOnSelector: i32 = 6;
+const kStylisticAltTwelveOffSelector: i32 = 25;
+const kStylisticAltTwelveOnSelector: i32 = 24;
+const kStylisticAltTwentyOffSelector: i32 = 41;
+const kStylisticAltTwentyOnSelector: i32 = 40;
+const kStylisticAltTwoOffSelector: i32 = 5;
+const kStylisticAltTwoOnSelector: i32 = 4;
+const kSuperiorsSelector: i32 = 1;
+const kSwashAlternatesOffSelector: i32 = 3;
+const kSwashAlternatesOnSelector: i32 = 2;
+const kTitlingCapsSelector: i32 = 4;
+const kTypographicExtrasType: i32 = 14;
+const kVerticalFractionsSelector: i32 = 1;
+const kVerticalPositionType: i32 = 10;
+
+pub fn apply_features(font: &mut Font, features: &Features) {
+    // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
+    // for a reference implementation.
+    toggle_open_type_feature(
+        font,
+        features.calt,
+        kContextualAlternatesType,
+        kContextualAlternatesOnSelector,
+        kContextualAlternatesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.case,
+        kCaseSensitiveLayoutType,
+        kCaseSensitiveLayoutOnSelector,
+        kCaseSensitiveLayoutOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.cpsp,
+        kCaseSensitiveLayoutType,
+        kCaseSensitiveSpacingOnSelector,
+        kCaseSensitiveSpacingOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.frac,
+        kFractionsType,
+        kDiagonalFractionsSelector,
+        kNoFractionsSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.liga,
+        kLigaturesType,
+        kCommonLigaturesOnSelector,
+        kCommonLigaturesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.onum,
+        kNumberCaseType,
+        kLowerCaseNumbersSelector,
+        2,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ordn,
+        kVerticalPositionType,
+        kOrdinalsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.pnum,
+        kNumberSpacingType,
+        kProportionalNumbersSelector,
+        4,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss01,
+        kStylisticAlternativesType,
+        kStylisticAltOneOnSelector,
+        kStylisticAltOneOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss02,
+        kStylisticAlternativesType,
+        kStylisticAltTwoOnSelector,
+        kStylisticAltTwoOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss03,
+        kStylisticAlternativesType,
+        kStylisticAltThreeOnSelector,
+        kStylisticAltThreeOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss04,
+        kStylisticAlternativesType,
+        kStylisticAltFourOnSelector,
+        kStylisticAltFourOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss05,
+        kStylisticAlternativesType,
+        kStylisticAltFiveOnSelector,
+        kStylisticAltFiveOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss06,
+        kStylisticAlternativesType,
+        kStylisticAltSixOnSelector,
+        kStylisticAltSixOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss07,
+        kStylisticAlternativesType,
+        kStylisticAltSevenOnSelector,
+        kStylisticAltSevenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss08,
+        kStylisticAlternativesType,
+        kStylisticAltEightOnSelector,
+        kStylisticAltEightOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss09,
+        kStylisticAlternativesType,
+        kStylisticAltNineOnSelector,
+        kStylisticAltNineOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss10,
+        kStylisticAlternativesType,
+        kStylisticAltTenOnSelector,
+        kStylisticAltTenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss11,
+        kStylisticAlternativesType,
+        kStylisticAltElevenOnSelector,
+        kStylisticAltElevenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss12,
+        kStylisticAlternativesType,
+        kStylisticAltTwelveOnSelector,
+        kStylisticAltTwelveOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss13,
+        kStylisticAlternativesType,
+        kStylisticAltThirteenOnSelector,
+        kStylisticAltThirteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss14,
+        kStylisticAlternativesType,
+        kStylisticAltFourteenOnSelector,
+        kStylisticAltFourteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss15,
+        kStylisticAlternativesType,
+        kStylisticAltFifteenOnSelector,
+        kStylisticAltFifteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss16,
+        kStylisticAlternativesType,
+        kStylisticAltSixteenOnSelector,
+        kStylisticAltSixteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss17,
+        kStylisticAlternativesType,
+        kStylisticAltSeventeenOnSelector,
+        kStylisticAltSeventeenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss18,
+        kStylisticAlternativesType,
+        kStylisticAltEighteenOnSelector,
+        kStylisticAltEighteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss19,
+        kStylisticAlternativesType,
+        kStylisticAltNineteenOnSelector,
+        kStylisticAltNineteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss20,
+        kStylisticAlternativesType,
+        kStylisticAltTwentyOnSelector,
+        kStylisticAltTwentyOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.subs,
+        kVerticalPositionType,
+        kInferiorsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.sups,
+        kVerticalPositionType,
+        kSuperiorsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.swsh,
+        kContextualAlternatesType,
+        kSwashAlternatesOnSelector,
+        kSwashAlternatesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.titl,
+        kStyleOptionsType,
+        kTitlingCapsSelector,
+        kNoStyleOptionsSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.tnum,
+        kNumberSpacingType,
+        kMonospacedNumbersSelector,
+        4,
+    );
+    toggle_open_type_feature(
+        font,
+        features.zero,
+        kTypographicExtrasType,
+        kSlashedZeroOnSelector,
+        kSlashedZeroOffSelector,
+    );
+}
+
+fn toggle_open_type_feature(
+    font: &mut Font,
+    enabled: Option<bool>,
+    type_identifier: i32,
+    on_selector_identifier: i32,
+    off_selector_identifier: i32,
+) {
+    if let Some(enabled) = enabled {
+        let native_font = font.native_font();
+        unsafe {
+            let selector_identifier = if enabled {
+                on_selector_identifier
+            } else {
+                off_selector_identifier
+            };
+            let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
+                native_font.copy_descriptor().as_concrete_TypeRef(),
+                CFNumber::from(type_identifier).as_concrete_TypeRef(),
+                CFNumber::from(selector_identifier).as_concrete_TypeRef(),
+            );
+            let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
+            let new_font = CTFontCreateCopyWithAttributes(
+                font.native_font().as_concrete_TypeRef(),
+                0.0,
+                ptr::null(),
+                new_descriptor.as_concrete_TypeRef(),
+            );
+            let new_font = CTFont::wrap_under_create_rule(new_font);
+            *font = Font::from_native_font(new_font);
+        }
+    }
+}
+
+#[link(name = "CoreText", kind = "framework")]
+extern "C" {
+    fn CTFontCreateCopyWithAttributes(
+        font: CTFontRef,
+        size: CGFloat,
+        matrix: *const CGAffineTransform,
+        attributes: CTFontDescriptorRef,
+    ) -> CTFontRef;
+}

crates/gpui/src/text_layout.rs šŸ”—

@@ -663,7 +663,9 @@ mod tests {
     fn test_wrap_line(cx: &mut crate::MutableAppContext) {
         let font_cache = cx.font_cache().clone();
         let font_system = cx.platform().fonts();
-        let family = font_cache.load_family(&["Courier"]).unwrap();
+        let family = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache.select_font(family, &Default::default()).unwrap();
 
         let mut wrapper = LineWrapper::new(font_id, 16., font_system);
@@ -725,7 +727,9 @@ mod tests {
         let font_system = cx.platform().fonts();
         let text_layout_cache = TextLayoutCache::new(font_system.clone());
 
-        let family = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache.select_font(family, &Default::default()).unwrap();
         let normal = RunStyle {
             font_id,

crates/language/Cargo.toml šŸ”—

@@ -47,6 +47,7 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }
 regex = "1.5"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1", features = ["preserve_order"] }
 similar = "1.3"
 smallvec = { version = "1.6", features = ["union"] }

crates/live_kit_client/Cargo.toml šŸ”—

@@ -14,7 +14,7 @@ name = "test_app"
 
 [features]
 test-support = [
-    "async-trait", 
+    "async-trait",
     "collections/test-support",
     "gpui/test-support",
     "lazy_static",
@@ -63,9 +63,11 @@ lazy_static = "1.4"
 objc = "0.2"
 parking_lot = "0.11.1"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 sha2 = "0.10"
 simplelog = "0.9"
 
 [build-dependencies]
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }

crates/live_kit_server/Cargo.toml šŸ”—

@@ -20,6 +20,7 @@ prost = "0.8"
 prost-types = "0.8"
 reqwest = "0.11"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 sha2 = "0.10"
 
 [build-dependencies]

crates/lsp/Cargo.toml šŸ”—

@@ -23,6 +23,7 @@ lsp-types = "0.91"
 parking_lot = "0.11"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["raw_value"] }
 smol = "1.2"
 

crates/plugin/Cargo.toml šŸ”—

@@ -6,5 +6,6 @@ publish = false
 
 [dependencies]
 serde = "1.0"
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 bincode = "1.3"
 plugin_macros = { path = "../plugin_macros" }

crates/plugin_macros/Cargo.toml šŸ”—

@@ -12,4 +12,5 @@ syn = { version = "1.0", features = ["full",  "extra-traits"] }
 quote = "1.0"
 proc-macro2 = "1.0"
 serde = "1.0"
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 bincode = "1.3"

crates/plugin_runtime/Cargo.toml šŸ”—

@@ -10,6 +10,7 @@ wasmtime-wasi = "0.38"
 wasi-common = "0.38"
 anyhow = { version = "1.0", features = ["std"] }
 serde = "1.0"
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = "1.0"
 bincode = "1.3"
 pollster = "0.2.5"

crates/project/Cargo.toml šŸ”—

@@ -49,6 +49,7 @@ pulldown-cmark = { version = "0.9.1", default-features = false }
 rand = "0.8.3"
 regex = "1.5"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 sha2 = "0.10"
 similar = "1.3"

crates/rpc/Cargo.toml šŸ”—

@@ -27,6 +27,7 @@ prost = "0.8"
 rand = "0.8"
 rsa = "0.4"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 smol-timeout = "0.6"
 tracing = { version = "0.1.34", features = ["log"] }
 zstd = "0.11"

crates/search/Cargo.toml šŸ”—

@@ -24,6 +24,7 @@ futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 

crates/settings/Cargo.toml šŸ”—

@@ -25,6 +25,7 @@ json_comments = "0.2"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 schemars = "0.8"
 serde = { workspace = true }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { workspace = true }
 serde_path_to_error = "0.1.4"
 toml = "0.5"

crates/settings/src/settings.rs šŸ”—

@@ -5,7 +5,7 @@ pub mod watched_json;
 use anyhow::{bail, Result};
 use gpui::{
     font_cache::{FamilyId, FontCache},
-    AssetSource,
+    fonts, AssetSource,
 };
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
@@ -28,6 +28,8 @@ pub use watched_json::watch_files;
 
 #[derive(Clone)]
 pub struct Settings {
+    pub buffer_font_family_name: String,
+    pub buffer_font_features: fonts::Features,
     pub buffer_font_family: FamilyId,
     pub default_buffer_font_size: f32,
     pub buffer_font_size: f32,
@@ -225,6 +227,7 @@ pub struct TerminalSettings {
     pub working_directory: Option<WorkingDirectory>,
     pub font_size: Option<f32>,
     pub font_family: Option<String>,
+    pub font_features: Option<fonts::Features>,
     pub env: Option<HashMap<String, String>>,
     pub blinking: Option<TerminalBlink>,
     pub alternate_scroll: Option<AlternateScroll>,
@@ -332,6 +335,8 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
     #[serde(default)]
+    pub buffer_font_features: Option<fonts::Features>,
+    #[serde(default)]
     pub active_pane_magnification: Option<f32>,
     #[serde(default)]
     pub cursor_blink: Option<bool>,
@@ -396,10 +401,16 @@ impl Settings {
         )
         .unwrap();
 
+        let buffer_font_features = defaults.buffer_font_features.unwrap();
         Self {
             buffer_font_family: font_cache
-                .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
+                .load_family(
+                    &[defaults.buffer_font_family.as_ref().unwrap()],
+                    &buffer_font_features,
+                )
                 .unwrap(),
+            buffer_font_family_name: defaults.buffer_font_family.unwrap(),
+            buffer_font_features,
             buffer_font_size: defaults.buffer_font_size.unwrap(),
             active_pane_magnification: defaults.active_pane_magnification.unwrap(),
             default_buffer_font_size: defaults.buffer_font_size.unwrap(),
@@ -451,11 +462,24 @@ impl Settings {
         theme_registry: &ThemeRegistry,
         font_cache: &FontCache,
     ) {
-        if let Some(value) = &data.buffer_font_family {
-            if let Some(id) = font_cache.load_family(&[value]).log_err() {
+        let mut family_changed = false;
+        if let Some(value) = data.buffer_font_family {
+            self.buffer_font_family_name = value;
+            family_changed = true;
+        }
+        if let Some(value) = data.buffer_font_features {
+            self.buffer_font_features = value;
+            family_changed = true;
+        }
+        if family_changed {
+            if let Some(id) = font_cache
+                .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
+                .log_err()
+            {
                 self.buffer_font_family = id;
             }
         }
+
         if let Some(value) = &data.theme {
             if let Some(theme) = theme_registry.get(value).log_err() {
                 self.theme = theme;
@@ -480,11 +504,6 @@ impl Settings {
         merge(&mut self.default_dock_anchor, data.default_dock_anchor);
         merge(&mut self.base_keymap, data.base_keymap);
 
-        // Ensure terminal font is loaded, so we can request it in terminal_element layout
-        if let Some(terminal_font) = &data.terminal.font_family {
-            font_cache.load_family(&[terminal_font]).log_err();
-        }
-
         self.editor_overrides = data.editor;
         self.git_overrides = data.git.unwrap_or_default();
         self.journal_overrides = data.journal;
@@ -616,7 +635,12 @@ impl Settings {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &gpui::AppContext) -> Settings {
         Settings {
-            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+            buffer_font_family_name: "Monaco".to_string(),
+            buffer_font_features: Default::default(),
+            buffer_font_family: cx
+                .font_cache()
+                .load_family(&["Monaco"], &Default::default())
+                .unwrap(),
             buffer_font_size: 14.,
             active_pane_magnification: 1.,
             default_buffer_font_size: 14.,

crates/terminal/Cargo.toml šŸ”—

@@ -30,6 +30,7 @@ anyhow = "1"
 thiserror = "1.0"
 lazy_static = "1.4.0"
 serde = { version = "1.0", features = ["derive"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 
 [dev-dependencies]
 rand = "0.8.5"

crates/terminal_view/Cargo.toml šŸ”—

@@ -34,6 +34,7 @@ anyhow = "1"
 thiserror = "1.0"
 lazy_static = "1.4.0"
 serde = { version = "1.0", features = ["derive"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 
 
 

crates/terminal_view/src/terminal_element.rs šŸ”—

@@ -505,13 +505,22 @@ impl TerminalElement {
 
     ///Configures a text style from the current settings.
     pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
-        // Pull the font family from settings properly overriding
-        let family_id = settings
+        let font_family_name = settings
             .terminal_overrides
             .font_family
             .as_ref()
             .or(settings.terminal_defaults.font_family.as_ref())
-            .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
+            .unwrap_or(&settings.buffer_font_family_name);
+        let font_features = settings
+            .terminal_overrides
+            .font_features
+            .as_ref()
+            .or(settings.terminal_defaults.font_features.as_ref())
+            .unwrap_or(&settings.buffer_font_features);
+
+        let family_id = font_cache
+            .load_family(&[font_family_name], &font_features)
+            .log_err()
             .unwrap_or(settings.buffer_font_family);
 
         let font_size = settings

crates/theme/Cargo.toml šŸ”—

@@ -14,6 +14,7 @@ anyhow = "1.0.38"
 indexmap = "1.6.2"
 parking_lot = "0.11.1"
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 serde_path_to_error = "0.1.4"
 toml = "0.5"

crates/theme/src/theme_registry.rs šŸ”—

@@ -2,6 +2,7 @@ use crate::{Theme, ThemeMeta};
 use anyhow::{Context, Result};
 use gpui::{fonts, AssetSource, FontCache};
 use parking_lot::Mutex;
+use serde::Deserialize;
 use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
 
@@ -56,12 +57,16 @@ impl ThemeRegistry {
             .with_context(|| format!("failed to load theme file {}", asset_path))?;
 
         // Allocate into the heap directly, the Theme struct is too large to fit in the stack.
-        let mut theme: Arc<Theme> = fonts::with_font_cache(self.font_cache.clone(), || {
-            serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
+        let mut theme = fonts::with_font_cache(self.font_cache.clone(), || {
+            let mut theme = Box::new(Theme::default());
+            let mut deserializer = serde_json::Deserializer::from_slice(&theme_json);
+            let result = Theme::deserialize_in_place(&mut deserializer, &mut theme);
+            result.map(|_| theme)
         })?;
 
         // Reset name to be the file path, so that we can use it to access the stored themes
-        Arc::get_mut(&mut theme).unwrap().meta.name = name.into();
+        theme.meta.name = name.into();
+        let theme: Arc<Theme> = theme.into();
         self.themes.lock().insert(name.to_string(), theme.clone());
         Ok(theme)
     }

crates/vim/Cargo.toml šŸ”—

@@ -13,6 +13,7 @@ neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
 
 [dependencies]
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 itertools = "0.10"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 

crates/workspace/Cargo.toml šŸ”—

@@ -45,6 +45,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
 indoc = "1.0.4"
@@ -57,4 +58,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
-db = { path = "../db", features = ["test-support"] }
+db = { path = "../db", features = ["test-support"] }

crates/zed/Cargo.toml šŸ”—

@@ -88,6 +88,7 @@ regex = "1.5"
 rsa = "0.4"
 rust-embed = { version = "6.3", features = ["include-exclude"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 serde_path_to_error = "0.1.4"
 simplelog = "0.9"

plugins/Cargo.lock šŸ”—

@@ -23,6 +23,7 @@ version = "0.1.0"
 dependencies = [
  "plugin",
  "serde",
+ "serde_derive",
  "serde_json",
 ]
 
@@ -33,6 +34,7 @@ dependencies = [
  "bincode",
  "plugin_macros",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -43,6 +45,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
+ "serde_derive",
  "syn",
 ]
 

plugins/json_language/Cargo.toml šŸ”—

@@ -6,6 +6,7 @@ edition = "2021"
 [dependencies]
 plugin = { path = "../../crates/plugin" }
 serde = { version = "1.0", features = ["derive"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = "1.0"
 
 [lib]

styles/src/styleTree/components.ts šŸ”—

@@ -97,7 +97,79 @@ export interface TextProperties {
     size?: keyof typeof fontSizes
     weight?: FontWeight
     underline?: boolean
-    color?: string
+    color?: string,
+    features?: FontFeatures,
+}
+
+interface FontFeatures {
+    /** Contextual Alternates: Applies a second substitution feature based on a match of a character pattern within a context of surrounding patterns */
+    calt?: boolean;
+    /** Case-Sensitive Forms: Shifts various punctuation marks up to a position that works better with all-capital sequences */
+    case?: boolean;
+    /** Capital Spacing: Adjusts inter-glyph spacing for all-capital text */
+    cpsp?: boolean;
+    /** Fractions: Replaces figures separated by a slash with diagonal fractions */
+    frac?: boolean;
+    /** Standard Ligatures: Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes */
+    liga?: boolean;
+    /** Oldstyle Figures: Changes selected figures from the default or lining style to oldstyle form. */
+    onum?: boolean;
+    /** Ordinals: Replaces default alphabetic glyphs with the corresponding ordinal forms for use after figures */
+    ordn?: boolean;
+    /** Proportional Figures: Replaces figure glyphs set on uniform (tabular) widths with corresponding glyphs set on proportional widths */
+    pnum?: boolean;
+    /** Stylistic set 01 */
+    ss01?: boolean;
+    /** Stylistic set 02 */
+    ss02?: boolean;
+    /** Stylistic set 03 */
+    ss03?: boolean;
+    /** Stylistic set 04 */
+    ss04?: boolean;
+    /** Stylistic set 05 */
+    ss05?: boolean;
+    /** Stylistic set 06 */
+    ss06?: boolean;
+    /** Stylistic set 07 */
+    ss07?: boolean;
+    /** Stylistic set 08 */
+    ss08?: boolean;
+    /** Stylistic set 09 */
+    ss09?: boolean;
+    /** Stylistic set 10 */
+    ss10?: boolean;
+    /** Stylistic set 11 */
+    ss11?: boolean;
+    /** Stylistic set 12 */
+    ss12?: boolean;
+    /** Stylistic set 13 */
+    ss13?: boolean;
+    /** Stylistic set 14 */
+    ss14?: boolean;
+    /** Stylistic set 15 */
+    ss15?: boolean;
+    /** Stylistic set 16 */
+    ss16?: boolean;
+    /** Stylistic set 17 */
+    ss17?: boolean;
+    /** Stylistic set 18 */
+    ss18?: boolean;
+    /** Stylistic set 19 */
+    ss19?: boolean;
+    /** Stylistic set 20 */
+    ss20?: boolean;
+    /** Subscript: Replaces default glyphs with subscript glyphs */
+    subs?: boolean;
+    /** Superscript: Replaces default glyphs with superscript glyphs */
+    sups?: boolean;
+    /** Swash: Replaces default glyphs with swash glyphs for stylistic purposes */
+    swsh?: boolean;
+    /** Titling: Replaces default glyphs with titling glyphs for use in large-size settings */
+    titl?: boolean;
+    /** Tabular Figures: Replaces figure glyphs set on proportional widths with corresponding glyphs set on uniform (tabular) widths */
+    tnum?: boolean;
+    /** Slashed Zero: Replaces default zero with a slashed zero for better distinction between "0" and "O" */
+    zero?: boolean;
 }
 
 export function text(