use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use hjkl_bonsai::runtime::{Grammar, LoadHandle};
use hjkl_bonsai::{CommentMarkerPass, DotFallbackTheme, Highlighter, InputEdit, Point, Theme};
use hjkl_engine::Query;
use hjkl_lang::{GrammarRequest, LanguageDirectory};
pub use hjkl_theme::{Color, Modifiers, StyleSpec};
pub use hjkl_buffer::BufferId;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseKind {
Viewport,
Top,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DiagSign {
pub row: usize,
pub ch: char,
pub priority: u8,
}
impl Default for DiagSign {
fn default() -> Self {
Self {
row: 0,
ch: 'E',
priority: 0,
}
}
}
impl DiagSign {
pub fn new(row: usize, ch: char, priority: u8) -> Self {
Self { row, ch, priority }
}
}
#[derive(Default, Debug, Clone, Copy)]
#[non_exhaustive]
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,
}
impl PerfBreakdown {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RenderOutput {
pub buffer_id: BufferId,
pub spans: Vec<Vec<(usize, usize, StyleSpec)>>,
pub signs: Vec<DiagSign>,
pub key: (u64, usize, usize),
pub perf: PerfBreakdown,
pub kind: ParseKind,
}
impl RenderOutput {
pub fn new(
buffer_id: BufferId,
spans: Vec<Vec<(usize, usize, StyleSpec)>>,
signs: Vec<DiagSign>,
key: (u64, usize, usize),
perf: PerfBreakdown,
kind: ParseKind,
) -> Self {
Self {
buffer_id,
spans,
signs,
key,
perf,
kind,
}
}
}
impl PartialEq for RenderOutput {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind
&& 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)
}
}
#[non_exhaustive]
pub enum SetLanguageOutcome {
Ready,
Loading(#[allow(dead_code)] String),
Unknown,
}
impl SetLanguageOutcome {
pub fn is_known(&self) -> bool {
matches!(self, Self::Ready | Self::Loading(_))
}
}
#[non_exhaustive]
pub enum LoadEvent {
Ready {
id: BufferId,
name: String,
},
Failed {
id: BufferId,
name: String,
error: String,
},
}
#[derive(Debug)]
pub enum LoadEventKind<'a> {
Ready {
id: BufferId,
name: &'a str,
},
Failed {
id: BufferId,
name: &'a str,
error: &'a str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseKindKind {
Viewport,
Top,
Bottom,
}
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,
kind: ParseKind,
}
enum Msg {
SetLanguage(BufferId, Option<Arc<Grammar>>),
Forget(BufferId),
SetTheme(Arc<dyn Theme + Send + Sync>),
Parse(ParseRequest),
Quit,
}
const PARSE_QUEUE_CAP: usize = 8;
struct Pending {
parse_queue: std::collections::VecDeque<ParseRequest>,
controls: std::collections::VecDeque<Msg>,
}
impl Pending {
fn new() -> Self {
Self {
parse_queue: std::collections::VecDeque::new(),
controls: std::collections::VecDeque::new(),
}
}
fn has_work(&self) -> bool {
!self.parse_queue.is_empty() || !self.controls.is_empty()
}
fn push_parse(&mut self, req: ParseRequest) {
for slot in self.parse_queue.iter_mut() {
if slot.buffer_id == req.buffer_id && slot.kind == req.kind {
*slot = req;
return;
}
}
if self.parse_queue.len() >= PARSE_QUEUE_CAP {
self.parse_queue.pop_front();
}
self.parse_queue.push_back(req);
}
}
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>, directory: Arc<LanguageDirectory>) -> 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, directory))
.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, grammar: Option<Arc<Grammar>>) {
self.enqueue_control(Msg::SetLanguage(id, grammar));
}
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.push_parse(req);
cvar.notify_one();
}
#[allow(dead_code)]
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 try_recv_all(&self) -> Vec<RenderOutput> {
let mut results = Vec::new();
while let Ok(out) = self.rx.try_recv() {
if let Some(existing) = results
.iter_mut()
.find(|r: &&mut RenderOutput| r.buffer_id == out.buffer_id && r.kind == out.kind)
{
*existing = out;
} else {
results.push(out);
}
}
results
}
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
}
pub fn wait_then_recv_all(&self, timeout: std::time::Duration) -> Vec<RenderOutput> {
let mut results: Vec<RenderOutput> = Vec::new();
if let Ok(first) = self.rx.recv_timeout(timeout) {
results.push(first);
}
while let Ok(out) = self.rx.try_recv() {
if let Some(existing) = results
.iter_mut()
.find(|r: &&mut RenderOutput| r.buffer_id == out.buffer_id && r.kind == out.kind)
{
*existing = out;
} else {
results.push(out);
}
}
results
}
}
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>,
directory: Arc<LanguageDirectory>,
) {
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_queue
.pop_front()
.expect("has_work() implies parse_queue non-empty"),
)
}
};
match msg {
Msg::Quit => return,
Msg::SetLanguage(id, None) => {
buffers.remove(&id);
}
Msg::SetLanguage(id, Some(grammar)) => {
let lang = grammar.name().to_string();
match Highlighter::new(grammar) {
Ok(h) => {
buffers.insert(
id,
WorkerBufferState {
highlighter: h,
last_parsed_dirty_gen: None,
},
);
}
Err(e) => {
tracing::error!(
buffer_id = id,
language = %lang,
error = %e,
"failed to attach syntax highlighter"
);
buffers.remove(&id);
}
}
}
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 =
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_with_injections(
bytes,
req.viewport_byte_range.clone(),
|name| directory.by_name(name),
);
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,
kind: req.kind,
});
}
}
}
}
pub fn build_by_row(
flat_spans: &[hjkl_bonsai::HighlightSpan],
bytes: &[u8],
row_starts: &[usize],
row_count: usize,
theme: &dyn Theme,
) -> Vec<Vec<(usize, usize, StyleSpec)>> {
let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); row_count];
for span in flat_spans {
let style = match theme.style(span.capture()) {
Some(s) => s,
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<DiagSign> {
let errors = h.parse_errors_range(bytes, viewport_byte_range);
let mut signs: Vec<DiagSign> = Vec::new();
let mut last_row: Option<usize> = None;
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(DiagSign::new(r, 'E', 100));
}
signs
}
#[derive(Default)]
struct BufferClient {
has_language: bool,
current_lang: Option<Arc<Grammar>>,
cache: Option<RenderCache>,
pending_edits: Vec<InputEdit>,
pending_reset: bool,
last_submitted_dirty_gen: Option<u64>,
}
struct PendingLoad {
id: BufferId,
name: String,
handle: LoadHandle,
}
pub struct SyntaxLayer {
pub directory: Arc<LanguageDirectory>,
theme: Arc<dyn Theme + Send + Sync>,
worker: SyntaxWorker,
clients: HashMap<BufferId, BufferClient>,
pending_loads: Vec<PendingLoad>,
preview_highlighters: Mutex<HashMap<String, Highlighter>>,
pub last_perf: PerfBreakdown,
}
impl SyntaxLayer {
pub fn new(theme: Arc<dyn Theme + Send + Sync>, directory: Arc<LanguageDirectory>) -> Self {
let worker = SyntaxWorker::spawn(Arc::clone(&theme), Arc::clone(&directory));
Self {
directory,
theme,
worker,
clients: HashMap::new(),
pending_loads: Vec::new(),
preview_highlighters: Mutex::new(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) -> SetLanguageOutcome {
match self.directory.request_for_path(path) {
GrammarRequest::Cached(grammar) => {
self.worker.set_language(id, Some(grammar.clone()));
let c = self.client_mut(id);
c.current_lang = Some(grammar);
c.has_language = true;
SetLanguageOutcome::Ready
}
GrammarRequest::Loading { name, handle } => {
self.worker.set_language(id, None);
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
self.pending_loads.push(PendingLoad {
id,
name: name.clone(),
handle,
});
SetLanguageOutcome::Loading(name)
}
GrammarRequest::Unknown => {
self.worker.set_language(id, None);
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
SetLanguageOutcome::Unknown
}
_ => {
self.worker.set_language(id, None);
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
SetLanguageOutcome::Unknown
}
}
}
pub fn poll_pending_loads(&mut self) -> Vec<LoadEvent> {
let mut events = Vec::new();
let mut i = 0;
while i < self.pending_loads.len() {
match self.pending_loads[i].handle.try_recv() {
None => {
i += 1;
}
Some(Ok(lib_path)) => {
let name = self.pending_loads[i].name.clone();
let bid = self.pending_loads[i].id;
self.pending_loads.swap_remove(i);
match self.directory.complete_load(&name, lib_path) {
Ok(grammar) => {
self.worker.set_language(bid, Some(grammar.clone()));
let c = self.client_mut(bid);
c.current_lang = Some(grammar);
c.has_language = true;
events.push(LoadEvent::Ready { id: bid, name });
}
Err(e) => {
events.push(LoadEvent::Failed {
id: bid,
name,
error: format!("{e:#}"),
});
}
}
}
Some(Err(err)) => {
let name = self.pending_loads[i].name.clone();
let bid = self.pending_loads[i].id;
self.pending_loads.swap_remove(i);
events.push(LoadEvent::Failed {
id: bid,
name,
error: err.to_string(),
});
}
}
}
events
}
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 grammar = self.clients.get(&id).and_then(|c| c.current_lang.clone())?;
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 grammar_name = grammar.name().to_string();
let mut cache = self.preview_highlighters.lock().ok()?;
let h = match cache.entry(grammar_name) {
std::collections::hash_map::Entry::Occupied(o) => {
let h = o.into_mut();
h.reset();
h
}
std::collections::hash_map::Entry::Vacant(v) => match Highlighter::new(grammar) {
Ok(h) => v.insert(h),
Err(_) => return None,
},
};
let mut flat_spans =
h.highlight_with_injections(bytes, |name| self.directory.by_name(name));
drop(cache);
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, StyleSpec)>> = 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(),
kind: ParseKind::Viewport,
})
}
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,
kind: ParseKind,
) -> 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,
kind,
});
Some(source_build_us)
}
#[allow(dead_code)]
pub fn take_result(&mut self) -> Option<RenderOutput> {
let out = self.worker.try_recv_latest()?;
self.last_perf = out.perf;
Some(out)
}
pub fn take_all_results(&mut self) -> Vec<RenderOutput> {
let results = self.worker.try_recv_all();
if let Some(last) = results.last() {
self.last_perf = last.perf;
}
results
}
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_all_results(&mut self, timeout: std::time::Duration) -> Vec<RenderOutput> {
let results = self.worker.wait_then_recv_all(timeout);
if let Some(last) = results.last() {
self.last_perf = last.perf;
}
results
}
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)
}
#[doc(hidden)]
pub fn has_client(&self, id: BufferId) -> bool {
self.clients.contains_key(&id)
}
#[doc(hidden)]
pub fn client_pending_reset(&self, id: BufferId) -> bool {
self.clients
.get(&id)
.map(|c| c.pending_reset)
.unwrap_or(false)
}
pub fn dispatch_load_event(
event: &LoadEvent,
mut handler: impl FnMut(LoadEventKind<'_>),
) -> bool {
#[allow(unreachable_patterns)]
match event {
LoadEvent::Ready { id, name } => {
handler(LoadEventKind::Ready { id: *id, name });
true
}
LoadEvent::Failed { id, name, error } => {
handler(LoadEventKind::Failed {
id: *id,
name,
error,
});
true
}
_ => false,
}
}
pub fn dispatch_parse_kind(kind: ParseKind, mut handler: impl FnMut(ParseKindKind)) -> bool {
#[allow(unreachable_patterns)]
match kind {
ParseKind::Viewport => {
handler(ParseKindKind::Viewport);
true
}
ParseKind::Top => {
handler(ParseKindKind::Top);
true
}
ParseKind::Bottom => {
handler(ParseKindKind::Bottom);
true
}
_ => {
handler(ParseKindKind::Viewport);
false
}
}
}
}
pub fn layer_with_theme(
theme: Arc<DotFallbackTheme>,
directory: Arc<LanguageDirectory>,
) -> SyntaxLayer {
SyntaxLayer::new(theme, directory)
}
#[cfg(test)]
pub fn default_layer() -> SyntaxLayer {
let directory = Arc::new(LanguageDirectory::new().expect("language directory"));
SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()), directory)
}
#[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, ParseKind::Viewport)?;
layer.wait_for_result(Duration::from_secs(5))
}
const TID: BufferId = 0;
#[test]
fn parse_kind_ordering_is_distinct() {
assert_ne!(ParseKind::Viewport, ParseKind::Top);
assert_ne!(ParseKind::Viewport, ParseKind::Bottom);
assert_ne!(ParseKind::Top, ParseKind::Bottom);
}
#[test]
fn diag_sign_new_roundtrip() {
let s = DiagSign::new(7, 'W', 50);
assert_eq!(s.row, 7);
assert_eq!(s.ch, 'W');
assert_eq!(s.priority, 50);
}
#[test]
fn diag_sign_default_is_sensible() {
let s = DiagSign::default();
assert_eq!(s.row, 0);
assert_eq!(s.ch, 'E');
assert_eq!(s.priority, 0);
}
#[test]
fn perf_breakdown_default_zeros() {
let p = PerfBreakdown::new();
assert_eq!(p.source_build_us, 0);
assert_eq!(p.parse_us, 0);
assert_eq!(p.highlight_us, 0);
assert_eq!(p.by_row_us, 0);
assert_eq!(p.diag_us, 0);
}
#[test]
fn set_language_outcome_is_known() {
assert!(SetLanguageOutcome::Ready.is_known());
assert!(SetLanguageOutcome::Loading("rust".to_string()).is_known());
assert!(!SetLanguageOutcome::Unknown.is_known());
}
#[test]
fn render_output_new_roundtrip() {
let out = RenderOutput::new(
99,
vec![vec![]],
vec![DiagSign::new(0, 'E', 100)],
(7, 0, 30),
PerfBreakdown::new(),
ParseKind::Bottom,
);
assert_eq!(out.buffer_id, 99);
assert_eq!(out.kind, ParseKind::Bottom);
assert_eq!(out.key, (7, 0, 30));
assert_eq!(out.signs.len(), 1);
}
#[test]
fn render_output_partial_eq_same() {
let a = RenderOutput::new(
0,
vec![vec![(0, 5, StyleSpec::default())]],
vec![],
(1, 0, 10),
PerfBreakdown::default(),
ParseKind::Viewport,
);
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn render_output_partial_eq_different_kind() {
let a = RenderOutput::new(
0,
vec![],
vec![],
(0, 0, 10),
PerfBreakdown::default(),
ParseKind::Viewport,
);
let b = RenderOutput::new(
0,
vec![],
vec![],
(0, 0, 10),
PerfBreakdown::default(),
ParseKind::Top,
);
assert_ne!(a, b);
}
#[test]
fn build_by_row_empty_spans_gives_empty_rows() {
let by_row = build_by_row(
&[],
b"hello\nworld\n",
&[0, 6, 12],
2,
&DotFallbackTheme::dark(),
);
assert_eq!(by_row.len(), 2);
assert!(by_row[0].is_empty());
assert!(by_row[1].is_empty());
}
#[test]
fn pending_push_parse_replaces_same_buffer_kind() {
let mut p = Pending::new();
let make_req = |kind: ParseKind, dirty_gen: u64| ParseRequest {
buffer_id: 0,
source: Arc::new(String::new()),
row_starts: Arc::new(vec![]),
edits: vec![],
viewport_byte_range: 0..0,
viewport_top: 0,
viewport_height: 10,
row_count: 0,
dirty_gen,
reset: false,
kind,
};
p.push_parse(make_req(ParseKind::Viewport, 1));
p.push_parse(make_req(ParseKind::Viewport, 2));
assert_eq!(p.parse_queue.len(), 1);
assert_eq!(p.parse_queue[0].dirty_gen, 2);
}
#[test]
fn pending_push_parse_keeps_different_kinds() {
let mut p = Pending::new();
let make_req = |kind: ParseKind| ParseRequest {
buffer_id: 0,
source: Arc::new(String::new()),
row_starts: Arc::new(vec![]),
edits: vec![],
viewport_byte_range: 0..0,
viewport_top: 0,
viewport_height: 10,
row_count: 0,
dirty_gen: 1,
reset: false,
kind,
};
p.push_parse(make_req(ParseKind::Viewport));
p.push_parse(make_req(ParseKind::Top));
p.push_parse(make_req(ParseKind::Bottom));
assert_eq!(p.parse_queue.len(), 3);
}
#[test]
fn pending_push_parse_evicts_oldest_at_cap() {
let mut p = Pending::new();
for i in 0..(PARSE_QUEUE_CAP + 2) {
p.push_parse(ParseRequest {
buffer_id: i as BufferId,
source: Arc::new(String::new()),
row_starts: Arc::new(vec![]),
edits: vec![],
viewport_byte_range: 0..0,
viewport_top: 0,
viewport_height: 10,
row_count: 0,
dirty_gen: i as u64,
reset: false,
kind: ParseKind::Viewport,
});
}
assert!(p.parse_queue.len() <= PARSE_QUEUE_CAP);
}
#[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"))
.is_known()
);
assert!(
layer
.submit_render(TID, &buf, 0, 10, ParseKind::Viewport)
.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 worker_handles_quit_cleanly() {
let layer = default_layer();
drop(layer);
}
#[test]
fn set_language_for_path_returns_unknown_for_unrecognized_extension() {
let mut layer = default_layer();
let outcome = layer.set_language_for_path(TID, Path::new("a.zzznope_not_real"));
assert!(!outcome.is_known());
assert!(matches!(outcome, SetLanguageOutcome::Unknown));
}
#[test]
fn poll_pending_loads_drains_ready_handles() {
let mut layer = default_layer();
let events = layer.poll_pending_loads();
assert!(
events.is_empty(),
"expected no events with no pending loads"
);
}
#[test]
fn forget_removes_client_state() {
let mut layer = default_layer();
layer.set_language_for_path(TID, Path::new("a.zzz_unknown"));
layer.forget(TID);
assert!(!layer.clients.contains_key(&TID));
}
#[test]
fn take_all_results_empty_when_nothing_submitted() {
let mut layer = default_layer();
let results = layer.take_all_results();
assert!(results.is_empty());
}
#[test]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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"))
.is_known()
);
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]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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"))
.is_known()
);
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"
);
}
}
#[test]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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]
#[ignore = "network + compiler: needs tree-sitter-rust grammar"]
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).is_known());
let t0 = Instant::now();
layer.submit_render(TID, &buf, 0, 50, ParseKind::Viewport);
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, ParseKind::Viewport);
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
);
}
}