use std::{
collections::HashMap,
ops::Range,
sync::{LazyLock, Mutex},
};
use duat_core::{
Ns,
buffer::{Buffer, BufferOpts, Moment, PerBuffer},
form::{self, FormId},
hook::{self, BufferClosed, BufferOpened, BufferPrinted, BufferUpdated},
text::{Mask, Overlay, RegexHaystack, Strs, Tags, txt},
utils::Memoized,
};
struct BufferOptsParser {
opts: BufferOpts,
}
static PARSERS: PerBuffer<BufferOptsParser> = PerBuffer::new();
pub fn enable_parser() {
hook::add::<BufferOpened>(move |pa, handle| {
let opts_parser = BufferOptsParser { opts: handle.read(pa).opts };
PARSERS.register(pa, handle, opts_parser);
});
hook::add::<BufferClosed>(|pa, handle| _ = PARSERS.unregister(pa, handle));
let [nl_ns, space_ns] = [Ns::new(), Ns::new()];
let cur_line_ns = Ns::new();
let indent_ns = Ns::new();
let replacement_ns = Ns::new();
hook::add::<BufferUpdated>(move |pa, buffer| {
let printed_line_ranges = buffer.printed_line_ranges(pa);
let (parser, buf) = PARSERS.write(pa, buffer).unwrap();
let opts_changed = buf.opts != parser.opts;
parser.opts = buf.opts;
let moment = buf.moment_for(replacement_ns);
let nss = [nl_ns, space_ns];
replace_chars(buf, &moment, &printed_line_ranges, nss, opts_changed);
show_indents(buf, &moment, &printed_line_ranges, indent_ns, opts_changed);
if parser.opts.highlight_current_line {
hightlight_current_line(buf, cur_line_ns);
}
});
hook::add::<BufferPrinted>(move |pa, buffer| buffer.text_mut(pa).remove_tags(cur_line_ns, ..));
form::enable_mask("indent");
}
fn hightlight_current_line(buf: &mut Buffer, ns: Ns) {
let mut parts = buf.text_parts();
let caret = parts.selections.main().caret();
let line_range = parts.strs.line(caret.line()).byte_range();
parts.tags.insert(ns, line_range, Mask("current_line"));
}
fn replace_chars(
buf: &mut Buffer,
moment: &Moment,
ranges: &[Range<usize>],
nss: [Ns; 2],
opts_changed: bool,
) {
static OVERLAYS: LazyLock<Mutex<HashMap<(char, FormId), Overlay>>> =
LazyLock::new(Mutex::default);
let mut overlays = OVERLAYS.lock().unwrap();
macro_rules! overlay {
($char:expr, $form:literal) => {{
let form = form::id_of!($form);
overlays
.entry(($char, form))
.or_insert_with(|| Overlay::new(txt!("{}{}", form.to_tag(90), $char)))
.clone()
}};
}
let ranges_to_update = buf.ranges_to_update_for(nss[0]);
let opts = buf.opts;
let mut parts = buf.text_parts();
if opts_changed {
ranges_to_update.add_ranges([..]);
} else {
ranges_to_update.add_ranges(moment.iter().map(|change| change.line_range(parts.strs)));
}
let lines_to_update = ranges_to_update.select_from(ranges.iter().cloned());
if lines_to_update.is_empty() {
return;
}
let [nl_ns, space_ns] = nss;
let space_overlay = opts.space_char.map(|char| overlay!(char, "replace.space"));
let nl_overlay = (opts.newline != ' ').then(|| overlay!(opts.newline, "replace.newline"));
let nl_overlay_empty = opts
.newline_on_empty
.map(|char| overlay!(char, "replace.newline.empty"));
let nl_overlay_trailing = opts
.newline_trailing
.map(|char| overlay!(char, "replace.newline.trailing"));
for range in lines_to_update.iter().cloned() {
parts.tags.remove(space_ns, range.start..range.end);
parts.tags.remove_excl(nl_ns, range.start..range.end);
let line = &parts.strs[range.clone()];
let line_start = line.byte_range().start;
let mut space_start = None;
for (byte, char) in line.char_indices() {
let byte = line_start + byte;
match char {
'\n' => {
let overlay = space_start
.and_then(|_| nl_overlay_trailing.clone())
.or_else(|| {
nl_overlay_empty
.as_ref()
.filter(|_| byte == line_start)
.cloned()
})
.or_else(|| nl_overlay.clone());
if let Some(overlay) = overlay {
parts.tags.insert(nl_ns, byte, overlay);
}
}
' ' => _ = space_start.get_or_insert(byte),
_ => {
if let Some(start) = space_start.take()
&& start != line_start
&& let Some(char) = opts.space_char
&& char != ' '
&& let Some(overlay) = &space_overlay
{
for byte in start..byte {
parts.tags.insert(space_ns, byte, overlay.clone());
}
}
}
}
}
}
ranges_to_update.update_on(lines_to_update);
}
fn show_indents(
buf: &mut Buffer,
moment: &Moment,
ranges: &[Range<usize>],
ns: Ns,
opts_changed: bool,
) {
let ranges_to_update = buf.ranges_to_update_for(ns);
let opts = buf.opts;
let mut parts = buf.text_parts();
if opts_changed {
ranges_to_update.add_ranges([..]);
} else {
ranges_to_update.add_ranges(moment.iter().map(|change| change.line_range(parts.strs)));
}
let lines_to_update = ranges_to_update.select_from(ranges.iter().cloned());
if lines_to_update.is_empty() {
return;
}
let popts = opts.to_print_opts();
let sequences = lines_to_update
.iter()
.fold(Vec::<Vec<&Strs>>::new(), |mut seqs, range| {
if let Some(seq) = seqs.last_mut()
&& seq.last().unwrap().byte_range().end == range.start
{
seq.push(&parts.strs[range.clone()]);
} else {
seqs.push(vec![&parts.strs[range.clone()]]);
}
seqs
});
let set_capped = |state: &mut IndentState, line: &Strs, indent: usize| {
let total = state.list.iter().copied().sum();
if indent >= total && line.search(r"^\s*(\}|\)|\]|end)").next().is_some() {
state.capped = true;
}
};
for seq in sequences {
let prev_unindented = {
parts.strs[..seq[0].byte_range().start]
.lines()
.rev()
.find_map(|line| {
(!line.chars().next().unwrap().is_ascii_whitespace())
.then_some(line.byte_range())
})
.unwrap_or(0..0)
};
let next_unindented = {
parts.strs[seq.last().unwrap().byte_range().end..]
.lines()
.find_map(|line| {
(!line.chars().next().unwrap().is_ascii_whitespace())
.then_some(line.byte_range())
})
.unwrap_or(parts.strs.len()..parts.strs.len())
};
let mut state = IndentState::new(opts.indent_str, opts.tabstop);
let mut empty_lines = Vec::new();
for line in parts.strs[prev_unindented.end..next_unindented.end].lines() {
if line.is_empty_line() {
empty_lines.push(line);
} else {
let indent = line.indent(popts);
state.truncate(indent);
set_capped(&mut state, line, indent);
for line in empty_lines.drain(..) {
state.indent_line(line, &mut parts.tags, ns);
}
state.capped = false;
state.increment(indent);
state.indent_line(line, &mut parts.tags, ns)
}
}
let updated_range = prev_unindented.end..next_unindented.end;
ranges_to_update.update_on([updated_range]);
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct IndentState {
list: Vec<usize>,
capped: bool,
indent_str: Option<&'static str>,
tabstop: u8,
}
impl IndentState {
fn new(indent_str: Option<&'static str>, tabstop: u8) -> Self {
Self {
list: Vec::new(),
capped: false,
indent_str,
tabstop,
}
}
fn truncate(&mut self, indent: usize) {
self.list.retain({
let mut sum = 0;
move |len| {
sum += *len;
sum <= indent
}
});
}
fn increment(&mut self, indent: usize) {
let total: usize = self.list.iter().copied().sum();
if indent > self.list.iter().copied().sum() {
self.list.push((indent - total).min(self.tabstop as usize))
}
}
fn indent_line(&self, line: &Strs, tags: &mut Tags, ns: Ns) {
static OVERLAYS: Memoized<IndentState, Overlay> = Memoized::new();
let range = line.byte_range();
tags.remove_excl(ns, range.clone());
if self.list.is_empty() && !self.capped {
return;
}
let Some(indent_str) = self.indent_str.filter(|str| !str.is_empty()) else {
return;
};
let indent_form = form::id_of!("replace").to_tag(90);
let overlay = OVERLAYS.get_or_insert_with(self, || {
let ghost: String = self
.list
.iter()
.copied()
.chain(self.capped.then_some(self.tabstop as usize))
.flat_map(|len| indent_str.chars().chain(std::iter::repeat(' ')).take(len))
.collect();
Overlay::new(txt!("{}{indent_form}{ghost}", Mask("indent")))
});
tags.insert(ns, range.start, overlay);
}
}