feat: add selection, active line, and incremental highlighting to code
view
This commit is contained in:
@@ -2,10 +2,10 @@ use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div,
|
||||
AppContext as _, Context, Entity, Focusable as _, IntoElement, ParentElement as _, Render,
|
||||
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
||||
};
|
||||
use gpui_native_editor::code_view::CodeViewState;
|
||||
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> {
|
||||
@@ -66,7 +66,11 @@ fn a_capture_spanning_newlines_styles_both_lines() {
|
||||
#[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| {
|
||||
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| {
|
||||
@@ -98,16 +102,19 @@ fn code_view_window(
|
||||
line_count: usize,
|
||||
) -> (TestAppContext, VisualTestContext, Entity<CodeViewState>) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(gpui_native_theme::Theme::init);
|
||||
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 = 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);
|
||||
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");
|
||||
@@ -145,3 +152,99 @@ fn rows_render_virtualized_and_scroll() {
|
||||
"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");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
ScrollDelta, ScrollEvent, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
||||
px,
|
||||
};
|
||||
use gpui_native_editor::code_view::CodeViewState;
|
||||
use gpui_native_editor::code_view::{CodeViewState, bind_code_view_keys};
|
||||
use gpui_native_editor::highlight::Language;
|
||||
|
||||
/// Deterministic pseudo-Rust with mixed tokens, roughly `lines` lines long.
|
||||
@@ -50,12 +50,17 @@ impl Render for Host {
|
||||
#[test]
|
||||
fn scrolling_a_large_file_is_not_pathologically_slow() {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(gpui_native_theme::Theme::init);
|
||||
cx.update(|cx| {
|
||||
gpui_native_theme::Theme::init(cx);
|
||||
bind_code_view_keys(cx);
|
||||
});
|
||||
|
||||
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, highlight_elapsed) = cx.update(|cx| {
|
||||
let state = CodeViewState::new("bench-code", source.clone(), Language::Rust, cx);
|
||||
(state, started.elapsed())
|
||||
});
|
||||
|
||||
let state_cell: Rc<RefCell<Option<Entity<CodeViewState>>>> = Rc::new(RefCell::new(None));
|
||||
let cell = state_cell.clone();
|
||||
|
||||
Reference in New Issue
Block a user