1use gpui::{Font, Pixels, SharedString, px};
2
3#[derive(Debug, Clone)]
5pub struct EditorConfig {
6 pub line_numbers: bool,
8 pub line_height: Pixels,
10 pub font_size: Pixels,
12 pub tab_size: usize,
14 pub highlight_active_line: bool,
16 pub block_cursor: bool,
18 pub cursor_blink: bool,
20 pub context_menu: bool,
22 pub show_default_menu_items: bool,
25 pub line_wrap: bool,
27 pub font_family: Option<SharedString>,
35 pub code_font_family: Option<SharedString>,
37 #[cfg(feature = "markdown")]
39 pub markdown: twrite_core::markdown::MarkdownConfig,
40}
41
42impl Default for EditorConfig {
43 fn default() -> Self {
44 Self {
45 line_numbers: false,
46 line_height: px(22.0),
47 font_size: px(16.0),
48 tab_size: 4,
49 highlight_active_line: false,
50 block_cursor: false,
51 cursor_blink: true,
52 context_menu: true,
53 show_default_menu_items: true,
54 line_wrap: true,
55 font_family: None,
56 code_font_family: None,
57 #[cfg(feature = "markdown")]
58 markdown: twrite_core::markdown::MarkdownConfig::default(),
59 }
60 }
61}
62
63impl EditorConfig {
64 pub fn platform_monospace_candidates() -> Vec<SharedString> {
70 if cfg!(target_os = "macos") {
71 vec!["Menlo".into(), "Monaco".into(), "Courier New".into()]
72 } else if cfg!(target_os = "windows") {
73 vec![
74 "Consolas".into(),
75 "Cascadia Mono".into(),
76 "Courier New".into(),
77 ]
78 } else {
79 vec![
80 "Liberation Mono".into(),
81 "DejaVu Sans Mono".into(),
82 "Noto Sans Mono".into(),
83 "monospace".into(),
84 ]
85 }
86 }
87
88 pub fn font_candidates(&self) -> Vec<SharedString> {
91 match &self.font_family {
92 Some(family) => vec![family.clone()],
93 None => Self::platform_monospace_candidates(),
94 }
95 }
96
97 pub fn pick_family(
101 candidates: &[SharedString],
102 mut probe: impl FnMut(&str) -> (bool, bool),
103 ) -> Option<&SharedString> {
104 let mut partial = None;
105 for candidate in candidates {
106 match probe(candidate.as_ref()) {
107 (true, true) => return Some(candidate),
108 (false, false) => {}
109 _ => {
110 if partial.is_none() {
111 partial = Some(candidate);
112 }
113 }
114 }
115 }
116 partial
117 }
118
119 pub fn base_font(&self, host: &Font, selected: Option<&SharedString>) -> Font {
121 let mut font = host.clone();
122 if let Some(family) = self.font_family.as_ref().or(selected) {
123 font.family = family.clone();
124 }
125 font
126 }
127
128 pub fn code_font(&self, host: &Font, selected: Option<&SharedString>) -> Font {
131 let mut font = self.base_font(host, selected);
132 if let Some(family) = &self.code_font_family {
133 font.family = family.clone();
134 }
135 font
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn probe_for(
144 full: Vec<&'static str>,
145 partial: Vec<&'static str>,
146 ) -> impl FnMut(&str) -> (bool, bool) {
147 move |name: &str| {
148 if full.contains(&name) {
149 (true, true)
150 } else if partial.contains(&name) {
151 (true, false)
152 } else {
153 (false, false)
154 }
155 }
156 }
157
158 #[test]
159 fn pick_family_prefers_full_faces() {
160 let candidates: Vec<SharedString> = vec!["A".into(), "B".into(), "C".into()];
161 let picked = EditorConfig::pick_family(&candidates, probe_for(vec!["B"], vec!["A"]));
162 assert_eq!(picked.map(|s| s.as_ref()), Some("B"));
163 }
164
165 #[test]
166 fn pick_family_falls_back_to_partial_then_none() {
167 let candidates: Vec<SharedString> = vec!["A".into(), "B".into()];
168 let picked = EditorConfig::pick_family(&candidates, probe_for(vec![], vec!["B"]));
169 assert_eq!(picked.map(|s| s.as_ref()), Some("B"));
170
171 let picked = EditorConfig::pick_family(&candidates, probe_for(vec![], vec![]));
172 assert!(picked.is_none());
173 }
174
175 #[test]
176 fn explicit_candidates_shortcircuit_to_single_family() {
177 let config = EditorConfig {
178 font_family: Some("Mine".into()),
179 ..EditorConfig::default()
180 };
181 assert_eq!(config.font_candidates(), vec![SharedString::from("Mine")]);
182 }
183
184 #[test]
185 fn base_font_precedence_is_explicit_selected_host() {
186 use gpui::Font;
187 let host: Font = gpui::font(".SystemUIFont");
190 let selected: SharedString = "Selected".into();
191 let config = EditorConfig::default();
192
193 assert_eq!(
194 config.base_font(&host, Some(&selected)).family.as_ref(),
195 "Selected"
196 );
197 assert_eq!(
198 config.base_font(&host, None).family.as_ref(),
199 host.family.as_ref()
200 );
201
202 let config = EditorConfig {
203 font_family: Some("Explicit".into()),
204 ..EditorConfig::default()
205 };
206 assert_eq!(
207 config.base_font(&host, Some(&selected)).family.as_ref(),
208 "Explicit"
209 );
210 assert_eq!(
212 config.code_font(&host, Some(&selected)).family.as_ref(),
213 "Explicit"
214 );
215 let config = EditorConfig::default();
216 assert_eq!(
217 config.code_font(&host, Some(&selected)).family.as_ref(),
218 "Selected"
219 );
220 }
221
222 #[test]
223 fn cursor_blink_defaults_to_true() {
224 let config = EditorConfig::default();
225 assert!(config.cursor_blink);
226 }
227}