1pub mod canvas;
5pub mod config;
7pub mod editor;
9pub mod fps;
11pub mod input;
13pub mod layout_cache;
15pub mod prompt_bar;
17pub mod theme;
19
20pub use canvas::{EditorCanvas, LineMetrics, RunFonts, build_line_text_runs};
21pub use config::EditorConfig;
22pub use editor::{Editor, FaceAvailability, SelectionGranularity, VisibleLineLayout, VisibleLink};
23pub use fps::{FrameStats, fps_badge};
24pub use layout_cache::{CachedInput, LayoutCache};
25pub use prompt_bar::PromptBar;
26pub use theme::{EditorTheme, ResolvedTokenStyle, SyntaxTheme};
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31 use gpui::{Font, FontStyle, FontWeight, px};
32 use twrite_core::{HighlightTag, StyleSpan};
33
34 #[test]
35 fn test_line_metrics_quote_detection_when_concealed() {
36 let spans = vec![StyleSpan::tag(0..2, HighlightTag::Blockquote)];
37 let metrics = LineMetrics::for_line("> Quote", "Quote", &spans, px(16.0), px(22.0));
38 assert!(metrics.is_quote);
39 assert!(!metrics.is_code_block);
40 assert_eq!(metrics.line_height, px(22.0));
41
42 let plain = LineMetrics::for_line("> Quote", "> Quote", &[], px(16.0), px(22.0));
44 assert!(!plain.is_quote);
45 }
46
47 #[test]
48 fn test_line_metrics_code_fence() {
49 let active_spans = vec![StyleSpan::tag(0..7, HighlightTag::Code)];
50 let metrics =
51 LineMetrics::for_line("```rust", "```rust", &active_spans, px(16.0), px(22.0));
52 assert_eq!(metrics.line_height, px(22.0));
53 assert!(metrics.is_code_block);
54 }
55
56 #[test]
57 fn test_line_metrics_code_empty_line_inside_block() {
58 let spans = vec![StyleSpan::tag(0..0, HighlightTag::Code)];
59 let metrics = LineMetrics::for_line("", "", &spans, px(16.0), px(22.0));
60 assert!(metrics.is_code_block);
61 assert_eq!(metrics.line_height, px(22.0));
62 }
63
64 #[test]
65 fn test_build_line_text_runs_code_block_disables_text_bg() {
66 let theme = EditorTheme::default();
67 let font: Font = gpui::font(".SystemUIFont");
68 let spans = vec![StyleSpan::tag(0..4, HighlightTag::Code)];
69
70 let runs_block = build_line_text_runs(
71 "test",
72 &spans,
73 None,
74 &RunFonts {
75 base: &font,
76 code: &font,
77 },
78 &theme,
79 true,
80 false,
81 );
82 assert_eq!(runs_block.len(), 1);
83 assert!(runs_block[0].background_color.is_none());
84
85 let runs_inline = build_line_text_runs(
86 "test",
87 &spans,
88 None,
89 &RunFonts {
90 base: &font,
91 code: &font,
92 },
93 &theme,
94 false,
95 false,
96 );
97 assert_eq!(runs_inline.len(), 1);
98 assert_eq!(runs_inline[0].background_color, Some(theme.syntax.code_bg));
99
100 let runs_task_checked = build_line_text_runs(
101 "test",
102 &spans,
103 None,
104 &RunFonts {
105 base: &font,
106 code: &font,
107 },
108 &theme,
109 false,
110 true,
111 );
112 assert_eq!(runs_task_checked.len(), 1);
113 assert!(runs_task_checked[0].strikethrough.is_some());
114 }
115
116 #[test]
117 fn test_line_metrics_task_state_detection() {
118 let unchecked_spans = vec![StyleSpan::tag(0..6, HighlightTag::TaskUnchecked)];
119 let unchecked =
120 LineMetrics::for_line("- [ ] Todo", "Todo", &unchecked_spans, px(16.0), px(22.0));
121 assert_eq!(unchecked.task_state, Some(false));
122
123 let checked_spans = vec![StyleSpan::tag(0..6, HighlightTag::TaskChecked)];
124 let checked =
125 LineMetrics::for_line("- [x] Done", "Done", &checked_spans, px(16.0), px(22.0));
126 assert_eq!(checked.task_state, Some(true));
127
128 let plain = LineMetrics::for_line("Plain text", "Plain text", &[], px(16.0), px(22.0));
129 assert_eq!(plain.task_state, None);
130
131 let raw_only = LineMetrics::for_line("- [ ] Todo", "- [ ] Todo", &[], px(16.0), px(22.0));
133 assert_eq!(raw_only.task_state, None);
134 }
135
136 #[test]
137 fn test_line_metrics_thematic_break_detection() {
138 let spans = vec![StyleSpan::tag(0..3, HighlightTag::HorizontalRule)];
139 let metrics = LineMetrics::for_line("---", "---", &spans, px(16.0), px(22.0));
140 assert!(metrics.is_thematic_break);
141
142 let plain = LineMetrics::for_line("---", "---", &[], px(16.0), px(22.0));
143 assert!(!plain.is_thematic_break);
144 }
145
146 #[test]
147 fn test_visible_link_layout() {
148 let link = VisibleLink {
149 bounds: gpui::Bounds::new(
150 gpui::point(px(50.0), px(20.0)),
151 gpui::size(px(60.0), px(20.0)),
152 ),
153 url: "https://example.com".to_string(),
154 };
155 assert!(link.bounds.contains(&gpui::point(px(60.0), px(25.0))));
156 assert!(!link.bounds.contains(&gpui::point(px(120.0), px(25.0))));
157 assert_eq!(link.url, "https://example.com");
158 }
159
160 fn run_font_at(runs: &[gpui::TextRun], text: &str, idx: usize) -> gpui::Font {
162 let mut offset = 0;
163 for run in runs {
164 if idx < offset + run.len {
165 return run.font.clone();
166 }
167 offset += run.len;
168 }
169 panic!("idx {idx} out of runs for {text:?}");
170 }
171
172 #[test]
173 fn test_build_line_text_runs_bold_italic_fonts() {
174 let theme = EditorTheme::default();
178 let font: Font = gpui::font(".SystemUIFont");
179 let text = "Hi bold and ital!";
180 let spans = vec![
181 StyleSpan::tag(3..7, HighlightTag::Bold),
182 StyleSpan::tag(12..16, HighlightTag::Italic),
183 ];
184 let runs = build_line_text_runs(
185 text,
186 &spans,
187 None,
188 &RunFonts {
189 base: &font,
190 code: &font,
191 },
192 &theme,
193 false,
194 false,
195 );
196
197 let plain = run_font_at(&runs, text, 0);
198 assert_eq!(plain.weight, FontWeight::NORMAL);
199 assert_eq!(plain.style, FontStyle::Normal);
200
201 let bold = run_font_at(&runs, text, 4);
202 assert_eq!(bold.weight, FontWeight::BOLD);
203 assert_eq!(bold.style, FontStyle::Normal);
204
205 let italic = run_font_at(&runs, text, 13);
206 assert_eq!(italic.weight, FontWeight::NORMAL);
207 assert_eq!(italic.style, FontStyle::Italic);
208 }
209
210 #[test]
211 fn test_build_line_text_runs_code_spans_use_code_font() {
212 let theme = EditorTheme::default();
213 let font: Font = gpui::font(".SystemUIFont");
214 let code_font = Font {
215 family: "CodeFam".into(),
216 ..gpui::font(".SystemUIFont")
217 };
218 let text = "a `b` c";
219 let spans = vec![StyleSpan::tag(2..5, HighlightTag::Code)];
220 let runs = build_line_text_runs(
221 text,
222 &spans,
223 None,
224 &RunFonts {
225 base: &font,
226 code: &code_font,
227 },
228 &theme,
229 false,
230 false,
231 );
232
233 assert_eq!(
234 run_font_at(&runs, text, 0).family.as_ref(),
235 font.family.as_ref()
236 );
237 assert_eq!(run_font_at(&runs, text, 3).family.as_ref(), "CodeFam");
238 assert_eq!(
239 run_font_at(&runs, text, 6).family.as_ref(),
240 font.family.as_ref()
241 );
242 }
243
244 #[test]
245 fn test_line_metrics_heading_levels() {
246 let h1 = LineMetrics::for_line(
247 "# A",
248 "# A",
249 &[StyleSpan::tag(0..3, HighlightTag::Heading(1))],
250 px(16.0),
251 px(22.0),
252 );
253 assert_eq!(h1.font_size, px(16.0) * 2.0);
254
255 let h4 = LineMetrics::for_line(
256 "#### D",
257 "#### D",
258 &[StyleSpan::tag(0..6, HighlightTag::Heading(4))],
259 px(16.0),
260 px(22.0),
261 );
262 assert_eq!(h4.font_size, px(16.0) * 1.125);
263
264 let mixed = LineMetrics::for_line(
266 "mix",
267 "mix",
268 &[
269 StyleSpan::tag(0..3, HighlightTag::Heading(3)),
270 StyleSpan::tag(0..3, HighlightTag::Heading(1)),
271 ],
272 px(16.0),
273 px(22.0),
274 );
275 assert_eq!(mixed.font_size, px(16.0) * 2.0);
276
277 let odd = LineMetrics::for_line(
279 "odd",
280 "odd",
281 &[StyleSpan::tag(0..3, HighlightTag::Heading(9))],
282 px(16.0),
283 px(22.0),
284 );
285 assert_eq!(odd.font_size, px(16.0));
286 assert_eq!(odd.line_height, px(22.0));
287 }
288
289 #[test]
290 fn test_custom_tag_colors() {
291 let mut theme = EditorTheme::default();
292 assert_eq!(
294 theme.tag_color(HighlightTag::Custom("speaker")),
295 theme.foreground
296 );
297
298 theme
299 .syntax
300 .set_custom_tag_color("speaker", gpui::rgb(0xf9e2af).into());
301 assert_eq!(
302 theme.tag_color(HighlightTag::Custom("speaker")),
303 gpui::rgb(0xf9e2af).into()
304 );
305 assert_eq!(
307 theme.tag_color(HighlightTag::Custom("dialogue")),
308 theme.foreground
309 );
310 }
311
312 #[test]
313 fn test_heading_level_colors() {
314 let theme = EditorTheme::default();
315 assert_eq!(
316 theme.tag_color(HighlightTag::Heading(1)),
317 theme.syntax.heading1
318 );
319 assert_eq!(
320 theme.tag_color(HighlightTag::Heading(2)),
321 theme.syntax.heading2
322 );
323 assert_eq!(
324 theme.tag_color(HighlightTag::Heading(4)),
325 theme.syntax.heading3
326 );
327 }
328}