perf: fast-path grapheme counts and cache terminal layouts

This commit is contained in:
2026-10-10 00:41:29 +02:00
parent 05c044bd02
commit 455108a909
5 changed files with 47 additions and 3 deletions
Generated
+7
View File
@@ -180,6 +180,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.8" version = "0.8.8"
@@ -1066,6 +1072,7 @@ checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"compact_str", "compact_str",
"critical-section",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"itertools", "itertools",
"kasuari", "kasuari",
+1 -1
View File
@@ -6,7 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
crossterm = "0.29" crossterm = "0.29"
ignore = "0.4" ignore = "0.4"
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "underline-color"] } ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "layout-cache", "underline-color"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tree-sitter = "0.27.1" tree-sitter = "0.27.1"
+2
View File
@@ -44,6 +44,8 @@ Redraws bracket Ratatui's buffer diff and final cursor placement with DEC synchr
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned. The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
Ratatui's bounded layout cache is enabled: unchanged geometry and constraints reuse solved rectangles during scrolling; resizes and panel-layout changes use their own cache keys.
The buffer viewport includes a fixed left gutter with right-aligned hybrid line numbers: the highlighted primary cursor line shows its one-based absolute number; every other line shows its distance from the primary. Secondary carets do not establish separate numbering origins. Gutter width follows the total line count plus one separator cell, including an empty final row after a trailing newline. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Rows beyond the buffer have no number. The gutter is hidden when it would leave no text column. The buffer viewport includes a fixed left gutter with right-aligned hybrid line numbers: the highlighted primary cursor line shows its one-based absolute number; every other line shows its distance from the primary. Secondary carets do not establish separate numbering origins. Gutter width follows the total line count plus one separator cell, including an empty final row after a trailing newline. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Rows beyond the buffer have no number. The gutter is hidden when it would leave no text column.
## Syntax caches ## Syntax caches
+2 -1
View File
@@ -40,13 +40,14 @@ Rust tests live alongside their modules; native LSP integration checks are in [`
- Redraw checks inspect actual Crossterm output during repeated scrolling: supporting terminals receive synchronized frames, terminals ignoring synchronization never paint with a visible hardware cursor, and the final caret remains visible. - Redraw checks inspect actual Crossterm output during repeated scrolling: supporting terminals receive synchronized frames, terminals ignoring synchronization never paint with a visible hardware cursor, and the final caret remains visible.
- Native LSP integration checks launch framed-protocol fixture subprocesses, covering initialization/settings, opt-in startup, synchronization, UTF-16/file-URI conversion, dirty targets, stale requests/diagnostics, and transport failures. Stack checks cover independent initialization, diagnostic merging/clearing, sibling failure isolation in active/parked buffers, same-revision rename/close identity, aggregate limits, first-server position routing, and both shutdowns. Semantic ordering uses protocol/file gates; elapsed deadlines only bound stuck subprocesses. - Native LSP integration checks launch framed-protocol fixture subprocesses, covering initialization/settings, opt-in startup, synchronization, UTF-16/file-URI conversion, dirty targets, stale requests/diagnostics, and transport failures. Stack checks cover independent initialization, diagnostic merging/clearing, sibling failure isolation in active/parked buffers, same-revision rename/close identity, aggregate limits, first-server position routing, and both shutdowns. Semantic ordering uses protocol/file gates; elapsed deadlines only bound stuck subprocesses.
- Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state. - Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state.
- Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets. - Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, ASCII tab and Unicode column bounds, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets.
Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard. Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard.
## Performance and process limits ## Performance and process limits
- Undo uses full-buffer snapshots and movement scans the current line. This favors straightforward correctness over large-file performance. - Undo uses full-buffer snapshots and movement scans the current line. This favors straightforward correctness over large-file performance.
- Annotation coordinate validation fast-paths ASCII grapheme lengths using byte lengths with CRLF pair accounting, retaining Unicode segmentation for non-ASCII text. Validation remains atomic and still checks every token; large Unicode buffers or diagnostic batches can require foreground work.
- Legacy synchronous plugin invocation may pause input for up to 500 ms before a host timeout. Async plugins opt into background replies; pending requests and transport queues are bounded. Config/plugin startup has a 2-second deadline per host. Buffer context/document snapshots must fit within a 4 MiB JSON frame; a too-large frame disables that plugin, not editing. - Legacy synchronous plugin invocation may pause input for up to 500 ms before a host timeout. Async plugins opt into background replies; pending requests and transport queues are bounded. Config/plugin startup has a 2-second deadline per host. Buffer context/document snapshots must fit within a 4 MiB JSON frame; a too-large frame disables that plugin, not editing.
See [syntax caches](architecture.md#syntax-caches) for the current full-tree highlighting ceiling. See [syntax caches](architecture.md#syntax-caches) for the current full-tree highlighting ceiling.
+35 -1
View File
@@ -4516,7 +4516,12 @@ fn split_lines(text: &str) -> Vec<String> {
} }
fn grapheme_count(line: &str) -> usize { fn grapheme_count(line: &str) -> usize {
line.graphemes(true).count() if line.is_ascii() {
// CRLF is the only multi-byte ASCII grapheme.
line.len() - line.matches("\r\n").count()
} else {
line.graphemes(true).count()
}
} }
fn grapheme_byte(line: &str, column: usize) -> usize { fn grapheme_byte(line: &str, column: usize) -> usize {
line.grapheme_indices(true) line.grapheme_indices(true)
@@ -5525,6 +5530,35 @@ mod tests {
assert!(editor.semantic_tokens().is_empty()); assert!(editor.semantic_tokens().is_empty());
} }
#[test]
fn semantic_column_bounds_preserve_ascii_tabs_and_unicode_graphemes() {
use crate::plugin::SemanticTokenKind;
for text in ["a\tb", "e\u{301}界👩‍🚀"] {
let mut editor = editor();
editor.apply(vec![Effect::Replace(text.into())]).unwrap();
let token = SemanticToken {
start: TextPosition { row: 0, column: 0 },
end: TextPosition { row: 0, column: 3 },
kind: SemanticTokenKind::Variable,
modifiers: 0,
};
editor
.replace_semantic_tokens(1, editor.text_revision(), Some(Arc::from([token])))
.unwrap();
let outside = SemanticToken {
end: TextPosition { row: 0, column: 4 },
..token
};
assert!(
editor
.replace_semantic_tokens(1, editor.text_revision(), Some(Arc::from([outside])))
.is_err(),
"{text:?}",
);
assert_eq!(editor.semantic_tokens(), &[token]);
}
}
#[test] #[test]
fn hover_scroll_and_escape_preserve_carets_and_edits_dismiss_normally() { fn hover_scroll_and_escape_preserve_carets_and_edits_dismiss_normally() {
let mut editor = editor(); let mut editor = editor();