use std::collections::HashMap;
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
use hjkl_bonsai::runtime::{Grammar, LoadHandle};
use hjkl_bonsai::{
CommentMarkerPass, DotFallbackTheme, HEX_BG_KEY, HEX_COLOR_CAPTURE, HEX_FG_KEY, HexColorPass,
Highlighter, InputEdit, MetaValue, Point, RAINBOW_BRACKET_CAPTURE, RAINBOW_DEPTH_KEY, Theme,
rainbow_spans_rope,
};
use hjkl_engine::Query;
use hjkl_lang::{GrammarRequest, LanguageDirectory};
pub use hjkl_theme::{Color, Modifiers, StyleSpec};
pub use hjkl_buffer::BufferId;
#[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,
}
impl RenderOutput {
pub fn new(
buffer_id: BufferId,
spans: Vec<Vec<(usize, usize, StyleSpec)>>,
signs: Vec<DiagSign>,
key: (u64, usize, usize),
perf: PerfBreakdown,
) -> Self {
Self {
buffer_id,
spans,
signs,
key,
perf,
}
}
}
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)
}
}
#[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,
},
}
struct PendingLoad {
id: BufferId,
name: String,
handle: LoadHandle,
}
struct BufferClient {
has_language: bool,
current_lang: Option<Arc<Grammar>>,
highlighter: Option<Highlighter>,
cache_dirty_gen: Option<u64>,
cache_rows: Range<usize>,
cache_spans: Vec<Vec<(usize, usize, StyleSpec)>>,
cache_row_starts: Option<(u64, Arc<Vec<usize>>)>,
parsed_dirty_gen: Option<u64>,
cache_signs: Option<(u64, usize, usize, Vec<DiagSign>)>,
}
impl Default for BufferClient {
fn default() -> Self {
Self {
has_language: false,
current_lang: None,
highlighter: None,
cache_dirty_gen: None,
cache_rows: 0..0,
cache_spans: Vec::new(),
cache_row_starts: None,
parsed_dirty_gen: None,
cache_signs: None,
}
}
}
impl BufferClient {
fn invalidate_cache(&mut self) {
self.cache_dirty_gen = None;
self.cache_rows = 0..0;
self.cache_spans.clear();
self.cache_row_starts = None;
self.parsed_dirty_gen = None;
self.cache_signs = None;
}
}
pub struct SyntaxLayer {
pub directory: Arc<LanguageDirectory>,
theme: Arc<dyn Theme + Send + Sync>,
clients: HashMap<BufferId, BufferClient>,
pending_loads: Vec<PendingLoad>,
colorizer: bool,
colorizer_filetypes: Vec<String>,
rainbow_brackets: bool,
}
impl SyntaxLayer {
pub fn new(theme: Arc<dyn Theme + Send + Sync>, directory: Arc<LanguageDirectory>) -> Self {
Self {
directory,
theme,
clients: HashMap::new(),
pending_loads: Vec::new(),
colorizer: true,
colorizer_filetypes: vec![
"css".to_string(),
"scss".to_string(),
"sass".to_string(),
"less".to_string(),
"html".to_string(),
"vue".to_string(),
"svelte".to_string(),
"tailwindcss".to_string(),
"toml".to_string(),
"lua".to_string(),
"vim".to_string(),
],
rainbow_brackets: true,
}
}
pub fn set_rainbow_brackets(&mut self, enabled: bool) {
if self.rainbow_brackets == enabled {
return;
}
self.rainbow_brackets = enabled;
for client in self.clients.values_mut() {
client.invalidate_cache();
}
}
pub fn set_colorizer(&mut self, enabled: bool, filetypes: Vec<String>) {
if self.colorizer == enabled && self.colorizer_filetypes == filetypes {
return;
}
self.colorizer = enabled;
self.colorizer_filetypes = filetypes;
for client in self.clients.values_mut() {
client.invalidate_cache();
}
}
pub fn directory(&self) -> &Arc<LanguageDirectory> {
&self.directory
}
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.attach_grammar(id, grammar.clone());
let c = self.client_mut(id);
c.current_lang = Some(grammar);
c.has_language = true;
SetLanguageOutcome::Ready
}
GrammarRequest::Loading { name, handle } => {
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
c.highlighter = None;
c.invalidate_cache();
self.pending_loads.push(PendingLoad {
id,
name: name.clone(),
handle,
});
SetLanguageOutcome::Loading(name)
}
GrammarRequest::Unknown | _ => {
let c = self.client_mut(id);
c.current_lang = None;
c.has_language = false;
c.highlighter = None;
c.invalidate_cache();
SetLanguageOutcome::Unknown
}
}
}
fn attach_grammar(&mut self, id: BufferId, grammar: Arc<Grammar>) {
let c = self.clients.entry(id).or_default();
c.invalidate_cache();
match Highlighter::new(grammar) {
Ok(h) => {
c.highlighter = Some(h);
}
Err(e) => {
tracing::error!(buffer_id = id, error = %e, "failed to attach highlighter");
c.highlighter = None;
}
}
}
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.attach_grammar(bid, 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);
}
pub fn set_theme(&mut self, theme: Arc<dyn Theme + Send + Sync>) {
self.theme = theme;
for c in self.clients.values_mut() {
c.invalidate_cache();
}
}
pub fn apply_edits(&mut self, id: BufferId, edits: &[hjkl_engine::ContentEdit]) {
let c = match self.clients.get_mut(&id) {
Some(c) if c.has_language => c,
_ => return,
};
let h = match c.highlighter.as_mut() {
Some(h) => h,
None => return,
};
for e in edits {
h.edit(&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,
},
});
}
c.parsed_dirty_gen = None;
c.cache_row_starts = None;
c.cache_signs = None;
}
pub fn reset(&mut self, id: BufferId) {
if let Some(c) = self.clients.get_mut(&id) {
if let Some(h) = c.highlighter.as_mut() {
h.reset();
}
c.invalidate_cache();
}
}
pub fn render_viewport(
&mut self,
id: BufferId,
buffer: &impl Query,
viewport_top: usize,
viewport_height: usize,
) -> Option<RenderOutput> {
let client = self.clients.get_mut(&id)?;
if !client.has_language {
return None;
}
let dg = buffer.dirty_gen();
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 = (vp_top + viewport_height).min(row_count);
if vp_end <= vp_top {
return None;
}
if client.cache_dirty_gen != Some(dg) {
client.invalidate_cache();
}
let rope = buffer.rope();
let row_starts: Arc<Vec<usize>> = if client
.cache_row_starts
.as_ref()
.is_some_and(|(g, _)| *g == dg)
{
Arc::clone(&client.cache_row_starts.as_ref().unwrap().1)
} else {
let mut rs: Vec<usize> = Vec::with_capacity(row_count + 1);
rs.push(0);
let mut chunk_pos = 0usize;
for chunk in rope.chunks() {
for nl in memchr::memchr_iter(b'\n', chunk.as_bytes()) {
rs.push(chunk_pos + nl + 1);
}
chunk_pos += chunk.len();
}
let arc = Arc::new(rs);
client.cache_row_starts = Some((dg, Arc::clone(&arc)));
arc
};
let needs_reparse = client.parsed_dirty_gen != Some(dg);
{
let highlighter = client.highlighter.as_mut()?;
if highlighter.tree().is_none() {
highlighter.parse_initial_rope(&rope);
if highlighter.tree().is_some() {
client.parsed_dirty_gen = Some(dg);
}
} else if needs_reparse {
let ok = highlighter.parse_incremental_rope(&rope);
if ok && highlighter.tree().is_some() {
client.parsed_dirty_gen = Some(dg);
}
}
}
let colorizer_enabled = {
let c = self.clients.get(&id)?;
let lang_name = c.current_lang.as_ref().map(|g| g.name()).unwrap_or("");
self.colorizer
&& (self.colorizer_filetypes.is_empty()
|| self.colorizer_filetypes.iter().any(|ft| ft == lang_name))
};
let rainbow_brackets_enabled = self.rainbow_brackets;
let client = self.clients.get_mut(&id)?;
let highlighter = client.highlighter.as_mut()?;
highlighter.tree()?;
let theme = self.theme.as_ref();
let directory = Arc::clone(&self.directory);
if client.cache_rows.is_empty() {
client.cache_spans = walk_rows(
highlighter,
&rope,
&row_starts,
row_count,
vp_top,
vp_end,
theme,
&directory,
colorizer_enabled,
rainbow_brackets_enabled,
);
client.cache_rows = vp_top..vp_end;
client.cache_dirty_gen = Some(dg);
} else {
let cache_covers_overlap =
vp_top < client.cache_rows.end && vp_end > client.cache_rows.start;
if !cache_covers_overlap {
client.cache_spans = walk_rows(
highlighter,
&rope,
&row_starts,
row_count,
vp_top,
vp_end,
theme,
&directory,
colorizer_enabled,
rainbow_brackets_enabled,
);
client.cache_rows = vp_top..vp_end;
} else {
if vp_top < client.cache_rows.start {
let new_rows = walk_rows(
highlighter,
&rope,
&row_starts,
row_count,
vp_top,
client.cache_rows.start,
theme,
&directory,
colorizer_enabled,
rainbow_brackets_enabled,
);
let mut combined = new_rows;
combined.append(&mut client.cache_spans);
client.cache_spans = combined;
client.cache_rows.start = vp_top;
}
if vp_end > client.cache_rows.end {
let new_rows = walk_rows(
highlighter,
&rope,
&row_starts,
row_count,
client.cache_rows.end,
vp_end,
theme,
&directory,
colorizer_enabled,
rainbow_brackets_enabled,
);
client.cache_spans.extend(new_rows);
client.cache_rows.end = vp_end;
}
}
client.cache_dirty_gen = Some(dg);
}
let offset = vp_top - client.cache_rows.start;
let len = vp_end - vp_top;
let spans: Vec<Vec<(usize, usize, StyleSpec)>> =
client.cache_spans[offset..offset + len].to_vec();
let signs = if client
.cache_signs
.as_ref()
.is_some_and(|(g, t, e, _)| *g == dg && *t == vp_top && *e == vp_end)
{
client.cache_signs.as_ref().unwrap().3.clone()
} else {
let s = collect_diag_signs_range(highlighter, &rope, &row_starts, vp_top, vp_end);
client.cache_signs = Some((dg, vp_top, vp_end, s.clone()));
s
};
Some(RenderOutput {
buffer_id: id,
spans,
signs,
key: (dg, vp_top, viewport_height),
perf: PerfBreakdown::default(),
})
}
pub fn name_for_path(&self, path: &Path) -> Option<String> {
self.directory.name_for_path(path)
}
#[doc(hidden)]
pub fn has_client(&self, id: BufferId) -> bool {
self.clients.contains_key(&id)
}
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,
}
}
}
const RAINBOW_PALETTE: [Color; 7] = [
Color::rgb(255, 100, 100), Color::rgb(255, 175, 80), Color::rgb(255, 230, 80), Color::rgb(100, 220, 100), Color::rgb(80, 210, 220), Color::rgb(100, 140, 255), Color::rgb(190, 120, 255), ];
#[allow(clippy::too_many_arguments)]
fn walk_rows(
highlighter: &mut Highlighter,
rope: &ropey::Rope,
row_starts: &[usize],
row_count: usize,
seg_start: usize,
seg_end: usize,
theme: &dyn Theme,
directory: &Arc<LanguageDirectory>,
colorizer: bool,
rainbow_brackets: bool,
) -> Vec<Vec<(usize, usize, StyleSpec)>> {
let rope_len = rope.len_bytes();
let byte_start = row_starts.get(seg_start).copied().unwrap_or(rope_len);
let byte_end = row_starts
.get(seg_end)
.copied()
.unwrap_or(rope_len)
.min(rope_len)
.max(byte_start);
let mut flat_spans =
highlighter.highlight_range_with_injections_rope(rope, byte_start..byte_end, |name| {
directory.by_name(name)
});
let marker_pass = CommentMarkerPass::new();
marker_pass.apply_rope(&mut flat_spans, rope);
if colorizer {
let hex_color_pass = HexColorPass::new();
hex_color_pass.apply_range_rope(&mut flat_spans, rope, byte_start..byte_end);
}
if rainbow_brackets
&& let (Some(tree), Some(grammar)) = (highlighter.tree(), highlighter.grammar())
{
let rb_spans = rainbow_spans_rope(tree, grammar, rope, byte_start..byte_end);
flat_spans.extend(rb_spans);
}
let _ = row_count; build_by_row_range(&flat_spans, rope_len, row_starts, seg_start..seg_end, theme)
}
fn build_by_row_range(
flat_spans: &[hjkl_bonsai::HighlightSpan],
source_len: usize,
row_starts: &[usize],
row_range: Range<usize>,
theme: &dyn Theme,
) -> Vec<Vec<(usize, usize, StyleSpec)>> {
let seg_start = row_range.start;
let seg_end = row_range.end.min(row_starts.len());
if seg_end <= seg_start {
return Vec::new();
}
let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); seg_end - seg_start];
for span in flat_spans {
let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
let bg = match span.metadata.get(HEX_BG_KEY) {
Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
_ => None,
};
let fg = match span.metadata.get(HEX_FG_KEY) {
Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
_ => None,
};
bg.map(|bg| StyleSpec {
fg,
bg: Some(bg),
modifiers: hjkl_theme::Modifiers::default(),
})
} else if span.capture() == RAINBOW_BRACKET_CAPTURE {
let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
Some(MetaValue::Int(d)) => *d as usize,
_ => 0,
};
let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
Some(StyleSpec {
fg: Some(fg),
bg: None,
modifiers: hjkl_theme::Modifiers::default(),
})
} else {
None
};
let style: StyleSpec = if let Some(s) = hex_style {
s
} else {
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.max(seg_start);
while row < seg_end {
let row_byte_start = row_starts[row];
let row_byte_end = row_starts
.get(row + 1)
.map(|&s| s.saturating_sub(1))
.unwrap_or(source_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 - seg_start].push((local_start, local_end, style));
}
row += 1;
}
}
by_row
}
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 hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
let bg = match span.metadata.get(HEX_BG_KEY) {
Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
_ => None,
};
let fg = match span.metadata.get(HEX_FG_KEY) {
Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
_ => None,
};
bg.map(|bg| StyleSpec {
fg,
bg: Some(bg),
modifiers: hjkl_theme::Modifiers::default(),
})
} else if span.capture() == RAINBOW_BRACKET_CAPTURE {
let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
Some(MetaValue::Int(d)) => *d as usize,
_ => 0,
};
let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
Some(StyleSpec {
fg: Some(fg),
bg: None,
modifiers: hjkl_theme::Modifiers::default(),
})
} else {
None
};
let style: StyleSpec = if let Some(s) = hex_style {
s
} else {
match theme.style(span.capture()) {
Some(s) => *s,
None => continue,
}
};
let style = &style;
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_range(
h: &mut Highlighter,
rope: &ropey::Rope,
row_starts: &[usize],
vp_top: usize,
vp_end: usize,
) -> Vec<DiagSign> {
let rope_len = rope.len_bytes();
let byte_start = row_starts.get(vp_top).copied().unwrap_or(rope_len);
let byte_end = row_starts.get(vp_end).copied().unwrap_or(rope_len);
let window: String = if byte_start < byte_end && byte_end <= rope_len {
rope.byte_slice(byte_start..byte_end).to_string()
} else {
String::new()
};
let errors = h.parse_errors_range(window.as_bytes(), 0..(byte_end - byte_start));
let mut signs: Vec<DiagSign> = Vec::new();
let mut last_row: Option<usize> = None;
for err in &errors {
let abs_start = err.byte_range.start + byte_start;
let r = row_starts
.partition_point(|&rs| rs <= abs_start)
.saturating_sub(1);
if last_row == Some(r) {
continue;
}
last_row = Some(r);
signs.push(DiagSign::new(r, 'E', 100));
}
signs
}
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;
const TID: BufferId = 0;
#[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(),
);
assert_eq!(out.buffer_id, 99);
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(),
);
let b = a.clone();
assert_eq!(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 build_by_row_hex_color_uses_metadata_colors() {
let bytes = b"--accent: #bb9af7;";
let mut metadata = std::collections::HashMap::new();
metadata.insert(
HEX_BG_KEY.to_string(),
MetaValue::Str("#bb9af7".to_string()),
);
metadata.insert(
HEX_FG_KEY.to_string(),
MetaValue::Str("#ffffff".to_string()),
);
let span = hjkl_bonsai::HighlightSpan {
byte_range: 10..17,
capture: HEX_COLOR_CAPTURE.to_string(),
metadata,
};
let by_row = build_by_row(&[span], bytes, &[0], 1, &DotFallbackTheme::dark());
assert_eq!(by_row.len(), 1);
assert_eq!(by_row[0].len(), 1);
let (_, _, style) = by_row[0][0];
let bg = style.bg.expect("hex color must set background");
assert_eq!((bg.r, bg.g, bg.b), (0xbb, 0x9a, 0xf7));
let fg = style.fg.expect("hex color must set foreground");
assert_eq!((fg.r, fg.g, fg.b), (0xff, 0xff, 0xff));
}
#[test]
fn build_by_row_hex_color_without_metadata_skips() {
let span = hjkl_bonsai::HighlightSpan {
byte_range: 0..3,
capture: HEX_COLOR_CAPTURE.to_string(),
metadata: std::collections::HashMap::new(),
};
let by_row = build_by_row(&[span], b"foo", &[0], 1, &DotFallbackTheme::dark());
assert_eq!(by_row.len(), 1);
assert!(by_row[0].is_empty());
}
#[test]
fn render_viewport_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.render_viewport(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);
}
#[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]
#[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 = layer
.render_viewport(TID, &buf, 0, 10)
.expect("render output");
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 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 = layer.render_viewport(TID, &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 _ = layer.render_viewport(TID, &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 = layer.render_viewport(TID, &post, 0, 10).unwrap();
let mut cold_layer = default_layer();
cold_layer.set_language_for_path(TID, Path::new("a.rs"));
let cold = cold_layer.render_viewport(TID, &post, 0, 10).unwrap();
assert_eq!(inc.spans, cold.spans);
}
#[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 _ = layer.render_viewport(TID, &buf, 0, 10).unwrap();
assert!(layer.clients.contains_key(&TID));
layer.forget(TID);
assert!(!layer.clients.contains_key(&TID));
}
}