Files
gpui-native-kit/crates/editor/tests/code_view.rs
T

251 lines
7.4 KiB
Rust

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<CodeViewState>,
}
impl Render for Host {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.code.clone())
}
}
fn code_view_window(
line_count: usize,
) -> (TestAppContext, VisualTestContext, Entity<CodeViewState>) {
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<RefCell<Option<Entity<CodeViewState>>>> = 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");
}