use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use hjkl_buffer::Sign;
use hjkl_engine::Query;
use hjkl_tree_sitter::{
CommentMarkerPass, DotFallbackTheme, Highlighter, InputEdit, LanguageConfig, LanguageRegistry,
Point, Theme,
};
pub type BufferId = u64;
#[derive(Debug, Clone)]
pub struct RenderOutput {
#[allow(dead_code)] pub buffer_id: BufferId,
pub spans: Vec<Vec<(usize, usize, ratatui::style::Style)>>,
pub signs: Vec<Sign>,
pub key: (u64, usize, usize),
pub perf: PerfBreakdown,
}
impl PartialEq for RenderOutput {
fn eq(&self, other: &Self) -> bool {
self.spans == other.spans
&& self.signs.len() == other.signs.len()
&& self
.signs
.iter()
.zip(other.signs.iter())
.all(|(a, b)| a.row == b.row && a.ch == b.ch && a.priority == b.priority)
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct PerfBreakdown {
pub source_build_us: u128,
pub parse_us: u128,
pub highlight_us: u128,
pub by_row_us: u128,
pub diag_us: u128,
}
struct RenderCache {
dirty_gen: u64,
len_bytes: usize,
line_count: u32,
source: Arc<String>,
row_starts: Arc<Vec<usize>>,
}
struct ParseRequest {
buffer_id: BufferId,
source: Arc<String>,
row_starts: Arc<Vec<usize>>,
edits: Vec<InputEdit>,
viewport_byte_range: std::ops::Range<usize>,
viewport_top: usize,
viewport_height: usize,
row_count: usize,
dirty_gen: u64,
reset: bool,
}
enum Msg {
SetLanguage(BufferId, Option<&'static LanguageConfig>),
#[allow(dead_code)] Reset(BufferId),
Forget(BufferId),
SetTheme(Arc<dyn Theme + Send + Sync>),
Parse(ParseRequest),
Quit,
}
struct Pending {
parse: Option<ParseRequest>,
controls: std::collections::VecDeque<Msg>,
}
impl Pending {
fn new() -> Self {
Self {
parse: None,
controls: std::collections::VecDeque::new(),
}
}
fn has_work(&self) -> bool {
self.parse.is_some() || !self.controls.is_empty()
}
}
pub struct SyntaxWorker {
pending: Arc<(Mutex<Pending>, Condvar)>,
rx: std::sync::mpsc::Receiver<RenderOutput>,
handle: Option<JoinHandle<()>>,
}
impl SyntaxWorker {
pub fn spawn(theme: Arc<dyn Theme + Send + Sync>) -> Self {
let pending = Arc::new((Mutex::new(Pending::new()), Condvar::new()));
let (tx, rx) = std::sync::mpsc::channel();
let pending_for_thread = Arc::clone(&pending);
let handle = thread::Builder::new()
.name("hjkl-syntax".into())
.spawn(move || worker_loop(pending_for_thread, tx, theme))
.expect("spawn syntax worker");
Self {
pending,
rx,
handle: Some(handle),
}
}
fn enqueue_control(&self, msg: Msg) {
let (lock, cvar) = &*self.pending;
let mut p = lock.lock().expect("syntax pending mutex poisoned");
p.controls.push_back(msg);
cvar.notify_one();
}
pub fn set_language(&self, id: BufferId, config: Option<&'static LanguageConfig>) {
self.enqueue_control(Msg::SetLanguage(id, config));
}
#[allow(dead_code)] pub fn reset(&self, id: BufferId) {
self.enqueue_control(Msg::Reset(id));
}
pub fn forget(&self, id: BufferId) {
self.enqueue_control(Msg::Forget(id));
}
pub fn set_theme(&self, theme: Arc<dyn Theme + Send + Sync>) {
self.enqueue_control(Msg::SetTheme(theme));
}
fn submit(&self, req: ParseRequest) {
let (lock, cvar) = &*self.pending;
let mut p = lock.lock().expect("syntax pending mutex poisoned");
p.parse = Some(req);
cvar.notify_one();
}
pub fn try_recv_latest(&self) -> Option<RenderOutput> {
let mut latest: Option<RenderOutput> = None;
while let Ok(out) = self.rx.try_recv() {
latest = Some(out);
}
latest
}
pub fn wait_for_latest(&self, timeout: std::time::Duration) -> Option<RenderOutput> {
let mut latest = self.rx.recv_timeout(timeout).ok();
while let Ok(out) = self.rx.try_recv() {
latest = Some(out);
}
latest
}
}
impl Drop for SyntaxWorker {
fn drop(&mut self) {
{
let (lock, cvar) = &*self.pending;
if let Ok(mut p) = lock.lock() {
p.controls.push_back(Msg::Quit);
cvar.notify_one();
}
}
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
struct WorkerBufferState {
highlighter: Highlighter,
last_parsed_dirty_gen: Option<u64>,
}
fn worker_loop(
pending: Arc<(Mutex<Pending>, Condvar)>,
tx: std::sync::mpsc::Sender<RenderOutput>,
initial_theme: Arc<dyn Theme + Send + Sync>,
) {
use std::time::Instant;
let mut buffers: HashMap<BufferId, WorkerBufferState> = HashMap::new();
let mut theme: Arc<dyn Theme + Send + Sync> = initial_theme;
let marker_pass = CommentMarkerPass::new();
loop {
let msg = {
let (lock, cvar) = &*pending;
let mut p = lock.lock().expect("syntax pending mutex poisoned");
while !p.has_work() {
p = cvar.wait(p).expect("syntax pending cvar poisoned");
}
if let Some(c) = p.controls.pop_front() {
c
} else {
Msg::Parse(p.parse.take().expect("has_work() implies parse present"))
}
};
match msg {
Msg::Quit => return,
Msg::SetLanguage(id, None) => {
buffers.remove(&id);
}
Msg::SetLanguage(id, Some(cfg)) => match Highlighter::new(cfg) {
Ok(h) => {
buffers.insert(
id,
WorkerBufferState {
highlighter: h,
last_parsed_dirty_gen: None,
},
);
}
Err(_) => {
buffers.remove(&id);
}
},
Msg::Reset(id) => {
if let Some(s) = buffers.get_mut(&id) {
s.highlighter.reset();
s.last_parsed_dirty_gen = None;
}
}
Msg::Forget(id) => {
buffers.remove(&id);
}
Msg::SetTheme(t) => {
theme = t;
}
Msg::Parse(req) => {
let Some(state) = buffers.get_mut(&req.buffer_id) else {
continue;
};
let h = &mut state.highlighter;
let mut perf = PerfBreakdown::default();
if req.reset {
h.reset();
state.last_parsed_dirty_gen = None;
}
let needs_parse = !req.edits.is_empty()
|| h.tree().is_none()
|| state.last_parsed_dirty_gen != Some(req.dirty_gen);
if needs_parse {
for e in &req.edits {
h.edit(e);
}
let bytes = req.source.as_bytes();
let t = Instant::now();
let parsed_ok = if h.tree().is_none() {
h.parse_initial(bytes);
true
} else {
h.parse_incremental(bytes)
};
if !parsed_ok {
continue;
}
perf.parse_us = t.elapsed().as_micros();
state.last_parsed_dirty_gen = Some(req.dirty_gen);
}
let bytes = req.source.as_bytes();
let t = Instant::now();
let mut flat_spans = h.highlight_range(bytes, req.viewport_byte_range.clone());
perf.highlight_us = t.elapsed().as_micros();
marker_pass.apply(&mut flat_spans, bytes);
let t = Instant::now();
let by_row = build_by_row(
&flat_spans,
bytes,
&req.row_starts,
req.row_count,
theme.as_ref(),
);
perf.by_row_us = t.elapsed().as_micros();
let t = Instant::now();
let signs = collect_diag_signs(h, bytes, req.viewport_byte_range, &req.row_starts);
perf.diag_us = t.elapsed().as_micros();
let key = (req.dirty_gen, req.viewport_top, req.viewport_height);
let _ = tx.send(RenderOutput {
buffer_id: req.buffer_id,
spans: by_row,
signs,
key,
perf,
});
}
}
}
}
fn build_by_row(
flat_spans: &[hjkl_tree_sitter::HighlightSpan],
bytes: &[u8],
row_starts: &[usize],
row_count: usize,
theme: &dyn Theme,
) -> Vec<Vec<(usize, usize, ratatui::style::Style)>> {
let mut by_row: Vec<Vec<(usize, usize, ratatui::style::Style)>> = vec![Vec::new(); row_count];
for span in flat_spans {
let style = match theme.style(span.capture()) {
Some(s) => s.to_ratatui(),
None => continue,
};
let span_start = span.byte_range.start;
let span_end = span.byte_range.end;
let start_row = row_starts
.partition_point(|&rs| rs <= span_start)
.saturating_sub(1);
let mut row = start_row;
while row < row_count {
let row_byte_start = row_starts[row];
let row_byte_end = row_starts
.get(row + 1)
.map(|&s| s.saturating_sub(1))
.unwrap_or(bytes.len());
if row_byte_start >= span_end {
break;
}
let local_start = span_start.saturating_sub(row_byte_start);
let local_end = span_end.min(row_byte_end) - row_byte_start;
if local_end > local_start {
by_row[row].push((local_start, local_end, style));
}
row += 1;
}
}
by_row
}
fn collect_diag_signs(
h: &mut Highlighter,
bytes: &[u8],
viewport_byte_range: std::ops::Range<usize>,
row_starts: &[usize],
) -> Vec<Sign> {
let errors = h.parse_errors_range(bytes, viewport_byte_range);
let mut signs: Vec<Sign> = Vec::new();
let mut last_row: Option<usize> = None;
let err_style = ratatui::style::Style::default().fg(ratatui::style::Color::Red);
for err in &errors {
let r = row_starts
.partition_point(|&rs| rs <= err.byte_range.start)
.saturating_sub(1);
if last_row == Some(r) {
continue;
}
last_row = Some(r);
signs.push(Sign {
row: r,
ch: 'E',
style: err_style,
priority: 100,
});
}
signs
}
#[derive(Default)]
struct BufferClient {
has_language: bool,
current_lang: Option<&'static LanguageConfig>,
cache: Option<RenderCache>,
pending_edits: Vec<InputEdit>,
pending_reset: bool,
last_submitted_dirty_gen: Option<u64>,
}
pub struct SyntaxLayer {
registry: LanguageRegistry,
theme: Arc<dyn Theme + Send + Sync>,
worker: SyntaxWorker,
clients: HashMap<BufferId, BufferClient>,
pub last_perf: PerfBreakdown,
}
impl SyntaxLayer {
pub fn new(theme: Arc<dyn Theme + Send + Sync>) -> Self {
let worker = SyntaxWorker::spawn(Arc::clone(&theme));
Self {
registry: LanguageRegistry::new(),
theme,
worker,
clients: HashMap::new(),
last_perf: PerfBreakdown::default(),
}
}
fn client_mut(&mut self, id: BufferId) -> &mut BufferClient {
self.clients.entry(id).or_default()
}
pub fn set_language_for_path(&mut self, id: BufferId, path: &Path) -> bool {
match self.registry.detect_for_path(path) {
Some(config) => {
self.worker.set_language(id, Some(config));
let c = self.client_mut(id);
c.current_lang = Some(config);
c.has_language = true;
true
}
None => {
self.worker.set_language(id, None);
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
false
}
}
}
pub fn forget(&mut self, id: BufferId) {
self.clients.remove(&id);
self.worker.forget(id);
}
pub fn set_theme(&mut self, theme: Arc<dyn Theme + Send + Sync>) {
self.theme = Arc::clone(&theme);
self.worker.set_theme(theme);
}
pub fn preview_render(
&self,
id: BufferId,
buffer: &impl Query,
viewport_top: usize,
viewport_height: usize,
) -> Option<RenderOutput> {
let cfg = self.clients.get(&id).and_then(|c| c.current_lang)?;
let row_count = buffer.line_count() as usize;
if row_count == 0 || viewport_height == 0 {
return None;
}
let vp_top = viewport_top.min(row_count);
let vp_end_row = (vp_top + viewport_height).min(row_count);
if vp_end_row <= vp_top {
return None;
}
let mut source = String::new();
for r in vp_top..vp_end_row {
if r > vp_top {
source.push('\n');
}
source.push_str(buffer.line(r as u32));
}
let bytes = source.as_bytes();
let mut row_starts: Vec<usize> = vec![0];
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
row_starts.push(i + 1);
}
}
let local_row_count = vp_end_row - vp_top;
let mut h = Highlighter::new(cfg).ok()?;
h.parse_initial(bytes);
let mut flat_spans = h.highlight_range(bytes, 0..bytes.len());
let marker_pass = CommentMarkerPass::new();
marker_pass.apply(&mut flat_spans, bytes);
let local_by_row = build_by_row(
&flat_spans,
bytes,
&row_starts,
local_row_count,
self.theme.as_ref(),
);
let mut spans: Vec<Vec<(usize, usize, ratatui::style::Style)>> = vec![Vec::new(); vp_top];
spans.extend(local_by_row);
Some(RenderOutput {
buffer_id: id,
spans,
signs: Vec::new(),
key: (buffer.dirty_gen(), viewport_top, viewport_height),
perf: PerfBreakdown::default(),
})
}
pub fn reset(&mut self, id: BufferId) {
self.client_mut(id).pending_reset = true;
}
pub fn apply_edits(&mut self, id: BufferId, edits: &[hjkl_engine::ContentEdit]) {
let c = self.client_mut(id);
if !c.has_language {
return;
}
for e in edits {
c.pending_edits.push(InputEdit {
start_byte: e.start_byte,
old_end_byte: e.old_end_byte,
new_end_byte: e.new_end_byte,
start_position: Point {
row: e.start_position.0 as usize,
column: e.start_position.1 as usize,
},
old_end_position: Point {
row: e.old_end_position.0 as usize,
column: e.old_end_position.1 as usize,
},
new_end_position: Point {
row: e.new_end_position.0 as usize,
column: e.new_end_position.1 as usize,
},
});
}
}
pub fn submit_render(
&mut self,
id: BufferId,
buffer: &impl Query,
viewport_top: usize,
viewport_height: usize,
) -> Option<u128> {
use std::time::Instant;
let c = self.client_mut(id);
if !c.has_language {
return None;
}
let dg = buffer.dirty_gen();
let lb = buffer.len_bytes();
let lc = buffer.line_count();
let row_count = lc as usize;
let needs_rebuild = match &c.cache {
Some(rc) => rc.dirty_gen != dg || rc.len_bytes != lb || rc.line_count != lc,
None => true,
};
let mut source_build_us = 0u128;
if needs_rebuild {
let t = Instant::now();
let mut source = String::with_capacity(lb);
for r in 0..row_count {
if r > 0 {
source.push('\n');
}
source.push_str(buffer.line(r as u32));
}
let mut row_starts: Vec<usize> = vec![0];
for (i, &b) in source.as_bytes().iter().enumerate() {
if b == b'\n' {
row_starts.push(i + 1);
}
}
c.cache = Some(RenderCache {
dirty_gen: dg,
len_bytes: lb,
line_count: lc,
source: Arc::new(source),
row_starts: Arc::new(row_starts),
});
source_build_us = t.elapsed().as_micros();
}
let cache = c.cache.as_ref().expect("cache populated above");
let bytes_len = cache.source.len();
let vp_start = buffer.byte_of_row(viewport_top);
let vp_end_row = viewport_top + viewport_height + 1;
let vp_end = buffer.byte_of_row(vp_end_row).min(bytes_len);
let vp_end = vp_end.max(vp_start);
let edits = std::mem::take(&mut c.pending_edits);
let reset = std::mem::replace(&mut c.pending_reset, false);
c.last_submitted_dirty_gen = Some(dg);
let source_arc = Arc::clone(&cache.source);
let row_starts_arc = Arc::clone(&cache.row_starts);
self.worker.submit(ParseRequest {
buffer_id: id,
source: source_arc,
row_starts: row_starts_arc,
edits,
viewport_byte_range: vp_start..vp_end,
viewport_top,
viewport_height,
row_count,
dirty_gen: dg,
reset,
});
Some(source_build_us)
}
pub fn take_result(&mut self) -> Option<RenderOutput> {
let out = self.worker.try_recv_latest()?;
self.last_perf = out.perf;
Some(out)
}
pub fn wait_result(&mut self, timeout: std::time::Duration) -> Option<RenderOutput> {
let out = self.worker.wait_for_latest(timeout)?;
self.last_perf = out.perf;
Some(out)
}
pub fn wait_for_initial_result(
&mut self,
timeout: std::time::Duration,
) -> Option<RenderOutput> {
self.wait_result(timeout)
}
#[cfg(test)]
pub fn wait_for_result(&mut self, timeout: std::time::Duration) -> Option<RenderOutput> {
self.wait_for_initial_result(timeout)
}
}
pub fn default_layer() -> SyntaxLayer {
SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()))
}
#[cfg(test)]
mod tests {
use super::*;
use hjkl_buffer::Buffer;
use std::path::Path;
use std::time::Duration;
fn submit_and_wait(
layer: &mut SyntaxLayer,
buf: &Buffer,
top: usize,
height: usize,
) -> Option<RenderOutput> {
layer.submit_render(TID, buf, top, height)?;
layer.wait_for_result(Duration::from_secs(5))
}
const TID: BufferId = 0;
#[test]
fn parse_and_render_small_rust_buffer() {
let buf = Buffer::from_str("fn main() { let x = 1; }\n");
let mut layer = default_layer();
assert!(layer.set_language_for_path(TID, Path::new("a.rs")));
let out = submit_and_wait(&mut layer, &buf, 0, 10).expect("worker output");
assert_eq!(out.spans.len(), buf.row_count());
assert!(
out.spans.iter().any(|r| !r.is_empty()),
"expected at least one styled span"
);
}
#[test]
fn submit_with_no_language_returns_none() {
let buf = Buffer::from_str("hello world");
let mut layer = default_layer();
assert!(!layer.set_language_for_path(TID, Path::new("a.unknownext")));
assert!(layer.submit_render(TID, &buf, 0, 10).is_none());
}
#[test]
fn apply_edits_with_no_language_is_noop() {
let mut layer = default_layer();
let edits = vec![hjkl_engine::ContentEdit {
start_byte: 0,
old_end_byte: 0,
new_end_byte: 1,
start_position: (0, 0),
old_end_position: (0, 0),
new_end_position: (0, 1),
}];
layer.apply_edits(TID, &edits);
assert!(
layer
.clients
.get(&TID)
.map(|c| c.pending_edits.is_empty())
.unwrap_or(true)
);
}
#[test]
fn first_load_highlights_entire_viewport() {
let mut content = String::new();
for i in 0..50 {
content.push_str(&format!("fn f{i}() {{ let x = {i}; }}\n"));
}
let buf = Buffer::from_str(content.strip_suffix('\n').unwrap_or(&content));
let mut layer = default_layer();
assert!(layer.set_language_for_path(TID, Path::new("a.rs")));
let out = submit_and_wait(&mut layer, &buf, 0, 30).unwrap();
for (r, row) in out.spans.iter().take(30).enumerate() {
assert!(
!row.is_empty(),
"row {r} has no highlight spans on first load (content: {:?})",
buf.line(r)
);
}
}
#[test]
fn first_load_full_viewport_matches_full_parse() {
let mut content = String::new();
for i in 0..50 {
content.push_str(&format!("fn f{i}() {{ let x = {i}; }}\n"));
}
let buf = Buffer::from_str(content.strip_suffix('\n').unwrap_or(&content));
let mut narrow = default_layer();
narrow.set_language_for_path(TID, Path::new("a.rs"));
let narrow_out = submit_and_wait(&mut narrow, &buf, 0, 30).unwrap();
let mut full = default_layer();
full.set_language_for_path(TID, Path::new("a.rs"));
let full_out = submit_and_wait(&mut full, &buf, 0, 100).unwrap();
for r in 0..30 {
assert_eq!(
narrow_out.spans[r], full_out.spans[r],
"row {r} differs between viewport-scoped and full parse"
);
}
}
#[test]
fn diagnostics_emit_sign_for_syntax_error() {
let buf = Buffer::from_str("fn main() {\nlet x = ;\n}\n");
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.rs"));
let out = submit_and_wait(&mut layer, &buf, 0, 10).unwrap();
assert!(
!out.signs.is_empty(),
"expected at least one diagnostic sign for `let x = ;`"
);
assert!(
out.signs.iter().any(|s| s.row == 1 && s.ch == 'E'),
"expected an 'E' sign on row 1; got {:?}",
out.signs
);
}
#[test]
fn diagnostics_clean_source_no_signs() {
let buf = Buffer::from_str("fn main() { let x = 1; }\n");
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.rs"));
let out = submit_and_wait(&mut layer, &buf, 0, 10).unwrap();
assert!(
out.signs.is_empty(),
"expected no signs; got {:?}",
out.signs
);
}
#[test]
fn incremental_path_matches_cold_for_small_edit() {
let pre = Buffer::from_str("fn main() { let x = 1; }");
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.rs"));
let _ = submit_and_wait(&mut layer, &pre, 0, 10).unwrap();
layer.apply_edits(
TID,
&[hjkl_engine::ContentEdit {
start_byte: 3,
old_end_byte: 3,
new_end_byte: 4,
start_position: (0, 3),
old_end_position: (0, 3),
new_end_position: (0, 4),
}],
);
let post = Buffer::from_str("fn Ymain() { let x = 1; }");
let inc = submit_and_wait(&mut layer, &post, 0, 10).unwrap();
let mut cold_layer = default_layer();
cold_layer.set_language_for_path(TID, Path::new("a.rs"));
let cold = submit_and_wait(&mut cold_layer, &post, 0, 10).unwrap();
assert_eq!(inc.spans, cold.spans);
}
#[test]
fn worker_handles_quit_cleanly() {
let layer = default_layer();
drop(layer);
}
#[test]
fn reset_pending_request_is_consumed_once() {
let buf = Buffer::from_str("fn main() {}");
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.rs"));
layer.reset(TID);
assert!(layer.clients.get(&TID).unwrap().pending_reset);
let _ = submit_and_wait(&mut layer, &buf, 0, 10).unwrap();
assert!(
!layer.clients.get(&TID).unwrap().pending_reset,
"pending_reset should clear after submit"
);
}
#[test]
fn forget_drops_buffer_state() {
let buf = Buffer::from_str("fn main() {}");
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.rs"));
let _ = submit_and_wait(&mut layer, &buf, 0, 10).unwrap();
assert!(layer.clients.contains_key(&TID));
layer.forget(TID);
assert!(!layer.clients.contains_key(&TID));
}
}
#[cfg(test)]
mod perf_smoke {
use super::*;
use hjkl_buffer::Buffer;
use std::path::Path;
use std::time::{Duration, Instant};
#[test]
fn big_rs_smoke() {
let path = Path::new("/tmp/big.rs");
if !path.exists() {
eprintln!("/tmp/big.rs not present; skipping perf smoke");
return;
}
let content = std::fs::read_to_string(path).unwrap();
let buf = Buffer::from_str(content.strip_suffix('\n').unwrap_or(&content));
let mut layer = default_layer();
const TID: BufferId = 0;
assert!(layer.set_language_for_path(TID, path));
let t0 = Instant::now();
layer.submit_render(TID, &buf, 0, 50);
let main_t = t0.elapsed();
let out = layer.wait_for_result(Duration::from_secs(10));
eprintln!(
"first submit_render main-thread: {:?}, worker turnaround total: {:?}",
main_t,
t0.elapsed()
);
assert!(out.is_some(), "first parse should produce output");
let t0 = Instant::now();
let mut main_total = Duration::ZERO;
for top in 0..100 {
let s = Instant::now();
layer.submit_render(TID, &buf, top * 100, 50);
main_total += s.elapsed();
}
while layer.take_result().is_some() {}
eprintln!(
"100 viewport scrolls: total wall {:?}, main-thread total {:?} (avg {:?}/submit)",
t0.elapsed(),
main_total,
main_total / 100
);
let lines = buf.lines().to_vec();
let mut new_lines = lines.clone();
new_lines[50_000].insert(0, 'X');
let post = Buffer::from_str(&new_lines.join("\n"));
let edit_byte = (0..50_000).map(|r| lines[r].len() + 1).sum::<usize>();
layer.apply_edits(
TID,
&[hjkl_engine::ContentEdit {
start_byte: edit_byte,
old_end_byte: edit_byte,
new_end_byte: edit_byte + 1,
start_position: (50_000, 0),
old_end_position: (50_000, 0),
new_end_position: (50_000, 1),
}],
);
let t = Instant::now();
layer.submit_render(TID, &post, 0, 50);
let main_us = t.elapsed();
let out = layer.wait_for_result(Duration::from_secs(10));
eprintln!(
"post-edit submit: main-thread {:?}, worker total {:?} (per-step: {:?})",
main_us,
t.elapsed(),
out.as_ref().map(|o| o.perf),
);
}
}