use std::cell::RefCell; use std::rc::Rc; use gpui::{ AppContext as _, Context, Entity, Focusable as _, IntoElement, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, }; use gpui_native_editor::code_view::{CodeViewState, bind_code_view_keys}; use gpui_native_editor::highlight::{HighlightedCode, Language, Role, highlight}; fn spans_with_role(code: &HighlightedCode, role: Role) -> Vec<&str> { code.lines .iter() .flat_map(|line| line.spans.iter()) .filter(|span| span.role == role) .map(|span| span.text.as_ref()) .collect() } #[test] fn a_rust_snippet_gets_its_roles_from_the_grammar() { let source = "// a comment\nfn main() {\n let greeting = \"hello\";\n}\n"; let code = highlight(source, Language::Rust); assert_eq!(code.lines.len(), 5, "a trailing newline ends an empty line"); assert!(spans_with_role(&code, Role::Comment).contains(&"// a comment")); assert!( spans_with_role(&code, Role::Keyword) .iter() .any(|text| text.contains("fn")) ); assert!( spans_with_role(&code, Role::Keyword) .iter() .any(|text| text.contains("let")) ); assert!(spans_with_role(&code, Role::Function).contains(&"main")); assert!( spans_with_role(&code, Role::String) .iter() .any(|text| text.contains("hello")) ); } #[test] fn a_capture_spanning_newlines_styles_both_lines() { let source = "let text = \"a\nb\";\n"; let code = highlight(source, Language::Rust); assert!( code.lines[0] .spans .iter() .any(|span| span.role == Role::String && span.text.contains("\"a")), "the string opens on the first line" ); assert!( code.lines[1] .spans .iter() .any(|span| span.role == Role::String && span.text.contains("b\"")), "and continues on the second" ); } #[test] fn set_source_rehighlights_with_the_new_language() { let mut cx = TestAppContext::single(); cx.update(|cx| { gpui_native_theme::Theme::init(cx); bind_code_view_keys(cx); }); let state = cx.new(|cx| CodeViewState::new("test-code", "fn main() {}\n", Language::Rust, cx)); cx.update(|cx| { state.update(cx, |state, cx| { state.set_source("def main():\n pass\n", Language::Python, cx); }); }); let code = cx.update(|cx| state.read(cx).code().clone()); assert!( spans_with_role(&code, Role::Keyword) .iter() .any(|text| text.contains("def")), "python's def highlights as a keyword" ); assert!(spans_with_role(&code, Role::Function).contains(&"main")); } struct Host { code: Entity, } impl Render for Host { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child(self.code.clone()) } } fn code_view_window( line_count: usize, ) -> (TestAppContext, VisualTestContext, Entity) { let mut cx = TestAppContext::single(); cx.update(|cx| { gpui_native_theme::Theme::init(cx); bind_code_view_keys(cx); }); let source: String = (0..line_count) .map(|line| format!("fn line_{line}() {{}}\n")) .collect(); let state_cell: Rc>>> = Rc::new(RefCell::new(None)); let cell = state_cell.clone(); let window = cx.add_window(move |window, cx| { let code = cx.new(|cx| CodeViewState::new("test-code", source, Language::Rust, cx)); cell.borrow_mut().replace(code.clone()); window.focus(&code.read(cx).focus_handle(cx), cx); Host { code } }); let state = state_cell.borrow().clone().expect("state captured"); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, state) } #[test] fn rows_render_virtualized_and_scroll() { let (_cx, mut visual, state) = code_view_window(80); assert!( visual.debug_bounds("test-code-row-0").is_some(), "the first row renders" ); assert!( visual.debug_bounds("test-code-row-79").is_none(), "rows past the viewport never render" ); state.update_in(&mut visual, |state, _window, _cx| state.scroll_to_line(79)); visual.update(|window, cx| { window.draw(cx).clear(cx); }); assert!( visual.debug_bounds("test-code-row-79").is_some(), "scrolling brings the last row in" ); assert!( visual.debug_bounds("test-code-row-0").is_none(), "the first row scrolled out" ); } #[test] fn hovering_a_line_activates_it() { let (cx, mut visual, state) = code_view_window(80); let row = visual .debug_bounds("test-code-row-3") .expect("row 3 renders in the viewport"); visual.simulate_event(gpui::MouseMoveEvent { position: row.center(), ..Default::default() }); let active = cx.update(|cx| state.read(cx).active_line()); assert_eq!(active, Some(3), "the hovered line becomes the active line"); } #[test] fn selecting_then_copying_writes_the_clipboard() { let (cx, mut visual, state) = code_view_window(10); cx.update(|cx| { state.update(cx, |state, cx| { state.select_range( gpui_native_editor::code_view::SelectionPoint { row: 0, col: 0 }, gpui_native_editor::code_view::SelectionPoint { row: 0, col: 3 }, cx, ); }); }); visual.simulate_keystrokes("cmd-c"); let clipboard = cx.update(|cx| { cx.read_from_clipboard() .and_then(|item| item.text().map(|text| text.to_string())) }); assert_eq!( clipboard.as_deref(), Some("fn "), "the copy chord copies the selection" ); } #[test] fn update_line_replaces_a_single_line_and_rehighlights() { let mut cx = TestAppContext::single(); cx.update(|cx| { gpui_native_theme::Theme::init(cx); }); let state = cx.new(|cx| { CodeViewState::new( "test-code", "fn a() {}\nfn b() {}\nfn c() {}\n", Language::Rust, cx, ) }); cx.update(|cx| { state.update(cx, |state, cx| state.update_line(1, "struct S;", cx)); }); let (source, line_count, keyword_on_line_1) = cx.update(|cx| { let state = state.read(cx); ( state.source().to_string(), state.code().lines.len(), state.code().lines[1] .spans .iter() .any(|span| span.role == Role::Keyword), ) }); assert_eq!(source, "fn a() {}\nstruct S;\nfn c() {}\n"); assert_eq!(line_count, 4, "the line count survives the replacement"); assert!(keyword_on_line_1, "struct highlights as a keyword"); } #[test] fn update_line_splices_multiline_text() { let mut cx = TestAppContext::single(); cx.update(|cx| { gpui_native_theme::Theme::init(cx); }); let state = cx.new(|cx| CodeViewState::new("test-code", "fn a() {}\nfn b() {}\n", Language::Rust, cx)); cx.update(|cx| { state.update(cx, |state, cx| state.update_line(1, "one\ntwo\nthree", cx)); }); let source = cx.update(|cx| state.read(cx).source().to_string()); assert_eq!(source, "fn a() {}\none\ntwo\nthree\n"); }