Improve importing font-family settings from VS Code (#39736)

John Tur created

Closes https://github.com/zed-industries/zed/issues/39259

- Fixes import of `editor.fontFamily` (we were looking for the wrong
key)
- Adds basic support for the CSS font-family syntax used by VS Code,
including font fallback

Release Notes:

- N/A

Change summary

crates/settings/src/settings_file.rs     |  4 +-
crates/settings/src/settings_store.rs    | 47 +++++++++++++++++++++++
crates/settings/src/vscode_import.rs     | 51 ++++++++++++++++++++++++++
crates/terminal/src/terminal_settings.rs |  8 ++-
crates/theme/src/settings.rs             |  8 ++-
5 files changed, 110 insertions(+), 8 deletions(-)

Detailed changes

crates/settings/src/settings_file.rs 🔗

@@ -22,7 +22,7 @@ pub fn test_settings() -> String {
             "buffer_font_family": "Courier",
             "buffer_font_features": {},
             "buffer_font_size": 14,
-            "buffer_font_fallback": [],
+            "buffer_font_fallbacks": [],
             "theme": EMPTY_THEME_NAME,
         }),
         &mut value,
@@ -37,7 +37,7 @@ pub fn test_settings() -> String {
             "buffer_font_family": "Courier New",
             "buffer_font_features": {},
             "buffer_font_size": 14,
-            "buffer_font_fallback": [],
+            "buffer_font_fallbacks": [],
             "theme": EMPTY_THEME_NAME,
         }),
         &mut value,

crates/settings/src/settings_store.rs 🔗

@@ -1201,6 +1201,32 @@ mod tests {
         }
     }
 
+    #[derive(Debug, PartialEq)]
+    struct ThemeSettings {
+        buffer_font_family: FontFamilyName,
+        buffer_font_fallbacks: Vec<FontFamilyName>,
+    }
+
+    impl Settings for ThemeSettings {
+        fn from_settings(content: &SettingsContent) -> Self {
+            let content = content.theme.clone();
+            ThemeSettings {
+                buffer_font_family: content.buffer_font_family.unwrap(),
+                buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(),
+            }
+        }
+
+        fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
+            let content = &mut content.theme;
+
+            vscode.font_family_setting(
+                "editor.fontFamily",
+                &mut content.buffer_font_family,
+                &mut content.buffer_font_fallbacks,
+            );
+        }
+    }
+
     #[gpui::test]
     fn test_settings_store_basic(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &default_settings());
@@ -1523,6 +1549,7 @@ mod tests {
         store.register_setting::<DefaultLanguageSettings>();
         store.register_setting::<ItemSettings>();
         store.register_setting::<AutoUpdateSetting>();
+        store.register_setting::<ThemeSettings>();
 
         // create settings that werent present
         check_vscode_import(
@@ -1594,6 +1621,26 @@ mod tests {
             .unindent(),
             cx,
         );
+
+        // font-family
+        check_vscode_import(
+            &mut store,
+            r#"{
+            }
+            "#
+            .unindent(),
+            r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(),
+            r#"{
+                "buffer_font_fallbacks": [
+                    "Consolas",
+                    "Courier New"
+                ],
+                "buffer_font_family": "Cascadia Code"
+            }
+            "#
+            .unindent(),
+            cx,
+        );
     }
 
     #[track_caller]

crates/settings/src/vscode_import.rs 🔗

@@ -4,6 +4,8 @@ use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
 use serde_json::{Map, Value};
 use std::{path::Path, sync::Arc};
 
+use crate::FontFamilyName;
+
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum VsCodeSettingsSource {
     VsCode,
@@ -145,4 +147,53 @@ impl VsCodeSettings {
     pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
         self.content.get(key).and_then(Value::as_str).and_then(f)
     }
+
+    pub fn font_family_setting(
+        &self,
+        key: &str,
+        font_family: &mut Option<FontFamilyName>,
+        font_fallbacks: &mut Option<Vec<FontFamilyName>>,
+    ) {
+        let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
+            return;
+        };
+
+        let mut name_buffer = String::new();
+        let mut quote_char: Option<char> = None;
+        let mut fonts = Vec::new();
+        let mut add_font = |buffer: &mut String| {
+            let trimmed = buffer.trim();
+            if !trimmed.is_empty() {
+                fonts.push(trimmed.to_string().into());
+            }
+
+            buffer.clear();
+        };
+
+        for ch in css_name.chars() {
+            match (ch, quote_char) {
+                ('"' | '\'', None) => {
+                    quote_char = Some(ch);
+                }
+                (_, Some(q)) if ch == q => {
+                    quote_char = None;
+                }
+                (',', None) => {
+                    add_font(&mut name_buffer);
+                }
+                _ => {
+                    name_buffer.push(ch);
+                }
+            }
+        }
+
+        add_font(&mut name_buffer);
+
+        let mut iter = fonts.into_iter();
+        *font_family = iter.next();
+        let fallbacks: Vec<_> = iter.collect();
+        if !fallbacks.is_empty() {
+            *font_fallbacks = Some(fallbacks);
+        }
+    }
 }

crates/terminal/src/terminal_settings.rs 🔗

@@ -123,9 +123,11 @@ impl settings::Settings for TerminalSettings {
         let name = |s| format!("terminal.integrated.{s}");
 
         vscode.f32_setting(&name("fontSize"), &mut current.font_size);
-        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
-            current.font_family = Some(FontFamilyName(font_family.into()));
-        }
+        vscode.font_family_setting(
+            &name("fontFamily"),
+            &mut current.font_family,
+            &mut current.font_fallbacks,
+        );
         vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
         vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
         vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);

crates/theme/src/settings.rs 🔗

@@ -731,9 +731,11 @@ impl settings::Settings for ThemeSettings {
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         vscode.from_f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight);
         vscode.from_f32_setting("editor.fontSize", &mut current.theme.buffer_font_size);
-        if let Some(font) = vscode.read_string("editor.font") {
-            current.theme.buffer_font_family = Some(FontFamilyName(font.into()));
-        }
+        vscode.font_family_setting(
+            "editor.fontFamily",
+            &mut current.theme.buffer_font_family,
+            &mut current.theme.buffer_font_fallbacks,
+        )
         // TODO: possibly map editor.fontLigatures to buffer_font_features?
     }
 }