feat: add editor component
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div,
|
||||
};
|
||||
use gpui_native_editor::code_view::CodeViewState;
|
||||
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();
|
||||
let state = cx.new(|_| CodeViewState::new("test-code", "fn main() {}\n", Language::Rust));
|
||||
|
||||
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(gpui_native_theme::Theme::init);
|
||||
let source: String = (0..line_count)
|
||||
.map(|line| format!("fn line_{line}() {{}}\n"))
|
||||
.collect();
|
||||
let state = CodeViewState::new("test-code", source, Language::Rust);
|
||||
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
||||
let cell = state_cell.clone();
|
||||
let window = cx.add_window(move |_, cx| {
|
||||
let code = cx.new(|_| state);
|
||||
cell.borrow_mut().replace(code.clone());
|
||||
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"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, IntoElement, Modifiers, ParentElement as _, Render,
|
||||
ScrollDelta, ScrollEvent, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
||||
px,
|
||||
};
|
||||
use gpui_native_editor::code_view::CodeViewState;
|
||||
use gpui_native_editor::highlight::Language;
|
||||
|
||||
/// Deterministic pseudo-Rust with mixed tokens, roughly `lines` lines long.
|
||||
fn large_source(lines: usize) -> String {
|
||||
let mut source = String::with_capacity(lines * 48);
|
||||
let mut index = 0usize;
|
||||
while source.lines().count() < lines {
|
||||
let i = index;
|
||||
source.push_str(&format!(
|
||||
"// Section {i}: generated for the scroll performance test.\n"
|
||||
));
|
||||
source.push_str(&format!("struct Section{i} {{\n"));
|
||||
source.push_str(" id: u64,\n");
|
||||
source.push_str(" label: String,\n");
|
||||
source.push_str(" scores: Vec<f64>,\n");
|
||||
source.push_str("}\n\n");
|
||||
source.push_str(&format!("impl Section{i} {{\n"));
|
||||
source.push_str(" /// A one-line summary of this section.\n");
|
||||
source.push_str(" fn describe(&self) -> String {\n");
|
||||
source.push_str(&format!(
|
||||
" format!(\"section {i}: {{}} at {{:?}}\", self.label, self.scores)\n"
|
||||
));
|
||||
source.push_str(" }\n");
|
||||
source.push_str("}\n\n");
|
||||
index += 1;
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_a_large_file_is_not_pathologically_slow() {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(gpui_native_theme::Theme::init);
|
||||
|
||||
let source = large_source(5000);
|
||||
let started = Instant::now();
|
||||
let state = CodeViewState::new("bench-code", source.clone(), Language::Rust);
|
||||
let highlight_elapsed = started.elapsed();
|
||||
|
||||
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
||||
let cell = state_cell.clone();
|
||||
let window = cx.add_window(move |_, cx| {
|
||||
let code = cx.new(|_| state);
|
||||
cell.borrow_mut().replace(code.clone());
|
||||
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);
|
||||
});
|
||||
|
||||
let line_count = cx.update(|cx| state.read(cx).code().lines.len());
|
||||
assert!(
|
||||
(5000..=5020).contains(&line_count),
|
||||
"the whole file highlighted, got {line_count} lines"
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
for _ in 0..30 {
|
||||
visual.simulate_event(ScrollEvent {
|
||||
position: point(px(300.), px(300.)),
|
||||
delta: ScrollDelta::Pixels(point(px(0.), px(-240.))),
|
||||
modifiers: Modifiers::default(),
|
||||
..Default::default()
|
||||
});
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
}
|
||||
let scroll_elapsed = started.elapsed();
|
||||
|
||||
println!(
|
||||
"highlighting {line_count} lines took {highlight_elapsed:?}; 30 wheel frames over {} lines took {scroll_elapsed:?} ({:?} per frame)",
|
||||
line_count,
|
||||
scroll_elapsed / 30
|
||||
);
|
||||
assert!(
|
||||
highlight_elapsed < Duration::from_secs(3),
|
||||
"highlighting took {highlight_elapsed:?}"
|
||||
);
|
||||
assert!(
|
||||
scroll_elapsed < Duration::from_secs(2),
|
||||
"scrolling 30 frames took {scroll_elapsed:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user