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