pub mod canvas;
pub mod config;
pub mod editor;
pub mod fps;
pub mod input;
pub mod layout_cache;
pub mod prompt_bar;
pub mod theme;
pub use canvas::{EditorCanvas, LineMetrics, RunFonts, build_line_text_runs};
pub use config::EditorConfig;
pub use editor::{Editor, FaceAvailability, SelectionGranularity, VisibleLineLayout, VisibleLink};
pub use fps::{FrameStats, fps_badge};
pub use layout_cache::{CachedInput, LayoutCache};
pub use prompt_bar::PromptBar;
pub use theme::{EditorTheme, ResolvedTokenStyle, SyntaxTheme};
#[cfg(test)]
mod tests {
use super::*;
use gpui::{Font, FontStyle, FontWeight, px};
use twrite_core::{HighlightTag, StyleSpan};
#[test]
fn test_line_metrics_quote_detection_when_concealed() {
let spans = vec![StyleSpan::tag(0..2, HighlightTag::Blockquote)];
let metrics = LineMetrics::for_line("> Quote", "Quote", &spans, px(16.0), px(22.0));
assert!(metrics.is_quote);
assert!(!metrics.is_code_block);
assert_eq!(metrics.line_height, px(22.0));
let plain = LineMetrics::for_line("> Quote", "> Quote", &[], px(16.0), px(22.0));
assert!(!plain.is_quote);
}
#[test]
fn test_line_metrics_code_fence() {
let active_spans = vec![StyleSpan::tag(0..7, HighlightTag::Code)];
let metrics =
LineMetrics::for_line("```rust", "```rust", &active_spans, px(16.0), px(22.0));
assert_eq!(metrics.line_height, px(22.0));
assert!(metrics.is_code_block);
}
#[test]
fn test_line_metrics_code_empty_line_inside_block() {
let spans = vec![StyleSpan::tag(0..0, HighlightTag::Code)];
let metrics = LineMetrics::for_line("", "", &spans, px(16.0), px(22.0));
assert!(metrics.is_code_block);
assert_eq!(metrics.line_height, px(22.0));
}
#[test]
fn test_build_line_text_runs_code_block_disables_text_bg() {
let theme = EditorTheme::default();
let font: Font = gpui::font(".SystemUIFont");
let spans = vec![StyleSpan::tag(0..4, HighlightTag::Code)];
let runs_block = build_line_text_runs(
"test",
&spans,
None,
&RunFonts {
base: &font,
code: &font,
},
&theme,
true,
false,
);
assert_eq!(runs_block.len(), 1);
assert!(runs_block[0].background_color.is_none());
let runs_inline = build_line_text_runs(
"test",
&spans,
None,
&RunFonts {
base: &font,
code: &font,
},
&theme,
false,
false,
);
assert_eq!(runs_inline.len(), 1);
assert_eq!(runs_inline[0].background_color, Some(theme.syntax.code_bg));
let runs_task_checked = build_line_text_runs(
"test",
&spans,
None,
&RunFonts {
base: &font,
code: &font,
},
&theme,
false,
true,
);
assert_eq!(runs_task_checked.len(), 1);
assert!(runs_task_checked[0].strikethrough.is_some());
}
#[test]
fn test_line_metrics_task_state_detection() {
let unchecked_spans = vec![StyleSpan::tag(0..6, HighlightTag::TaskUnchecked)];
let unchecked =
LineMetrics::for_line("- [ ] Todo", "Todo", &unchecked_spans, px(16.0), px(22.0));
assert_eq!(unchecked.task_state, Some(false));
let checked_spans = vec![StyleSpan::tag(0..6, HighlightTag::TaskChecked)];
let checked =
LineMetrics::for_line("- [x] Done", "Done", &checked_spans, px(16.0), px(22.0));
assert_eq!(checked.task_state, Some(true));
let plain = LineMetrics::for_line("Plain text", "Plain text", &[], px(16.0), px(22.0));
assert_eq!(plain.task_state, None);
let raw_only = LineMetrics::for_line("- [ ] Todo", "- [ ] Todo", &[], px(16.0), px(22.0));
assert_eq!(raw_only.task_state, None);
}
#[test]
fn test_line_metrics_thematic_break_detection() {
let spans = vec![StyleSpan::tag(0..3, HighlightTag::HorizontalRule)];
let metrics = LineMetrics::for_line("---", "---", &spans, px(16.0), px(22.0));
assert!(metrics.is_thematic_break);
let plain = LineMetrics::for_line("---", "---", &[], px(16.0), px(22.0));
assert!(!plain.is_thematic_break);
}
#[test]
fn test_visible_link_layout() {
let link = VisibleLink {
bounds: gpui::Bounds::new(
gpui::point(px(50.0), px(20.0)),
gpui::size(px(60.0), px(20.0)),
),
url: "https://example.com".to_string(),
};
assert!(link.bounds.contains(&gpui::point(px(60.0), px(25.0))));
assert!(!link.bounds.contains(&gpui::point(px(120.0), px(25.0))));
assert_eq!(link.url, "https://example.com");
}
fn run_font_at(runs: &[gpui::TextRun], text: &str, idx: usize) -> gpui::Font {
let mut offset = 0;
for run in runs {
if idx < offset + run.len {
return run.font.clone();
}
offset += run.len;
}
panic!("idx {idx} out of runs for {text:?}");
}
#[test]
fn test_build_line_text_runs_bold_italic_fonts() {
let theme = EditorTheme::default();
let font: Font = gpui::font(".SystemUIFont");
let text = "Hi bold and ital!";
let spans = vec![
StyleSpan::tag(3..7, HighlightTag::Bold),
StyleSpan::tag(12..16, HighlightTag::Italic),
];
let runs = build_line_text_runs(
text,
&spans,
None,
&RunFonts {
base: &font,
code: &font,
},
&theme,
false,
false,
);
let plain = run_font_at(&runs, text, 0);
assert_eq!(plain.weight, FontWeight::NORMAL);
assert_eq!(plain.style, FontStyle::Normal);
let bold = run_font_at(&runs, text, 4);
assert_eq!(bold.weight, FontWeight::BOLD);
assert_eq!(bold.style, FontStyle::Normal);
let italic = run_font_at(&runs, text, 13);
assert_eq!(italic.weight, FontWeight::NORMAL);
assert_eq!(italic.style, FontStyle::Italic);
}
#[test]
fn test_build_line_text_runs_code_spans_use_code_font() {
let theme = EditorTheme::default();
let font: Font = gpui::font(".SystemUIFont");
let code_font = Font {
family: "CodeFam".into(),
..gpui::font(".SystemUIFont")
};
let text = "a `b` c";
let spans = vec![StyleSpan::tag(2..5, HighlightTag::Code)];
let runs = build_line_text_runs(
text,
&spans,
None,
&RunFonts {
base: &font,
code: &code_font,
},
&theme,
false,
false,
);
assert_eq!(
run_font_at(&runs, text, 0).family.as_ref(),
font.family.as_ref()
);
assert_eq!(run_font_at(&runs, text, 3).family.as_ref(), "CodeFam");
assert_eq!(
run_font_at(&runs, text, 6).family.as_ref(),
font.family.as_ref()
);
}
#[test]
fn test_line_metrics_heading_levels() {
let h1 = LineMetrics::for_line(
"# A",
"# A",
&[StyleSpan::tag(0..3, HighlightTag::Heading(1))],
px(16.0),
px(22.0),
);
assert_eq!(h1.font_size, px(16.0) * 2.0);
let h4 = LineMetrics::for_line(
"#### D",
"#### D",
&[StyleSpan::tag(0..6, HighlightTag::Heading(4))],
px(16.0),
px(22.0),
);
assert_eq!(h4.font_size, px(16.0) * 1.125);
let mixed = LineMetrics::for_line(
"mix",
"mix",
&[
StyleSpan::tag(0..3, HighlightTag::Heading(3)),
StyleSpan::tag(0..3, HighlightTag::Heading(1)),
],
px(16.0),
px(22.0),
);
assert_eq!(mixed.font_size, px(16.0) * 2.0);
let odd = LineMetrics::for_line(
"odd",
"odd",
&[StyleSpan::tag(0..3, HighlightTag::Heading(9))],
px(16.0),
px(22.0),
);
assert_eq!(odd.font_size, px(16.0));
assert_eq!(odd.line_height, px(22.0));
}
#[test]
fn test_custom_tag_colors() {
let mut theme = EditorTheme::default();
assert_eq!(
theme.tag_color(HighlightTag::Custom("speaker")),
theme.foreground
);
theme
.syntax
.set_custom_tag_color("speaker", gpui::rgb(0xf9e2af).into());
assert_eq!(
theme.tag_color(HighlightTag::Custom("speaker")),
gpui::rgb(0xf9e2af).into()
);
assert_eq!(
theme.tag_color(HighlightTag::Custom("dialogue")),
theme.foreground
);
}
#[test]
fn test_heading_level_colors() {
let theme = EditorTheme::default();
assert_eq!(
theme.tag_color(HighlightTag::Heading(1)),
theme.syntax.heading1
);
assert_eq!(
theme.tag_color(HighlightTag::Heading(2)),
theme.syntax.heading2
);
assert_eq!(
theme.tag_color(HighlightTag::Heading(4)),
theme.syntax.heading3
);
}
}