use std::{collections::HashMap, ops::Range, sync::Once};
use duat_core::{
Ns,
buffer::{Buffer, Moment},
context::{self, Handle},
data::Pass,
form::{self, Form, FormId},
hook::{self, BufferOpened, BufferUpdated, OnMouseEvent},
text::{Inlay, Text, TextParts, TextRange, TwoPoints},
txt,
ui::{Coord, PushSpecs, Side, Widget},
};
pub struct Gutter {
text: Text,
entries: HashMap<Ns, Vec<GutterEntry>>,
opts: GutterOpts,
mouse_coord: Option<Coord>,
}
fn initial_setup() {
form::set_weak("gutter.hint", Form::mimic("default.info"));
form::set_weak("gutter.warning", Form::mimic("default.warning"));
form::set_weak("gutter.error", Form::mimic("default.error"));
form::set_weak("buffer.hint", Form::new().underline_grey().underlined());
form::set_weak(
"buffer.warning",
Form::new().underline_yellow().underlined(),
);
form::set_weak("buffer.error", Form::new().underline_red().underlined());
let ns = Ns::new();
let msg_ns = Ns::new();
hook::add::<BufferOpened>(move |pa, buffer| _ = buffer.read(pa).moment_for(ns));
hook::add::<BufferUpdated>(move |pa, buffer| {
let Some((gutter, _)) = buffer.get_related::<Gutter>(pa).first().cloned() else {
return;
};
let printed_line_ranges = buffer.printed_line_ranges(pa);
let (gtr, buf) = pa.write_many((&gutter, buffer));
gtr.apply_changes(buf.moment_for(ns));
let (gt, buf, area) = pa.write_many((&gutter, buffer, buffer.area()));
buf.text_parts().tags.remove(msg_ns, ..);
let opts = buf.print_opts();
let mouse_point = gt
.mouse_coord
.filter(|&coord| coord >= area.top_left() && coord < area.bottom_right())
.and_then(|coord| {
Some(
area.points_at_coord(buf.text(), coord, opts)?
.as_within()?
.real,
)
});
let entries = gt
.entries
.iter()
.flat_map(|(_, entries)| entries)
.filter(|entry| {
let is_onscreen = printed_line_ranges
.iter()
.any(|range| range.contains(&entry.range.end));
let display = match entry.kind {
EntryKind::Hint => gt.opts.hint.display,
EntryKind::Warning => gt.opts.warning.display,
EntryKind::Error => gt.opts.error.display,
EntryKind::_Custom(..) => todo!(),
};
let do_show = match display {
GutterDisplay::OwnLines(always) => {
always
|| mouse_point.is_some_and(|point| entry.range.contains(&point.byte()))
}
GutterDisplay::Inline(_) => todo!(),
GutterDisplay::Spawn(_) => todo!(),
GutterDisplay::SpawnCorner(..) => todo!(),
};
do_show && is_onscreen
});
for entry in entries {
let Some(line) = buf.text()[entry.range.clone()].lines().last() else {
continue;
};
let range = line.range();
let lnum = range.start.line();
let Some(columns) =
area.columns_at(buf.text(), TwoPoints::new_after_ghost(range.start), opts)
else {
continue;
};
let mut parts = buf.text_parts();
let inlay = Inlay::new(txt!("{}{entry.msg}\n", " ".repeat(columns.wrapped)));
let line_end = parts.strs.line(lnum).byte_range().end;
parts.tags.insert(msg_ns, line_end, inlay)
}
})
.lateness(100_000_000);
hook::add::<BufferUpdated>(|pa, buffer| {
let Some((gutter, _)) = buffer.get_related::<Gutter>(pa).first().cloned() else {
return;
};
gutter.write(pa).text = Gutter::form_text(gutter.read(pa), pa, buffer);
})
.lateness(usize::MAX);
hook::add::<OnMouseEvent<Buffer>>(move |pa, event| {
let Some((gutter, _)) = event.handle.get_related::<Gutter>(pa).first().cloned() else {
return;
};
gutter.write(pa).mouse_coord = Some(event.coord);
})
.lateness(usize::MAX);
hook::add::<OnMouseEvent>(move |pa, _| {
for gutter in context::windows().handles_of::<Gutter>(pa) {
let gt = gutter.write(pa);
if gt.mouse_coord.take().is_some() {
let (buffer, _) = gutter.get_related::<Buffer>(pa).first().cloned().unwrap();
buffer.request_update();
}
}
})
.lateness(usize::MAX);
}
impl Gutter {
pub fn builder() -> GutterOpts {
static ONCE: Once = Once::new();
ONCE.call_once(initial_setup);
GutterOpts {
hint: GutterSymbolOpts {
symbol: 'i',
display: GutterDisplay::OwnLines(false),
},
warning: GutterSymbolOpts {
symbol: '!',
display: GutterDisplay::OwnLines(false),
},
error: GutterSymbolOpts {
symbol: '*',
display: GutterDisplay::OwnLines(true),
},
renderer: Some(Box::new(default_renderer)),
}
}
fn form_text(&self, pa: &Pass, buffer: &Handle) -> Text {
let printed_line_numbers = buffer.printed_line_numbers(pa);
let text = buffer.text(pa);
let mut builder = Text::builder();
for (idx, line) in printed_line_numbers.iter().enumerate() {
if idx > 0 && (line.is_wrapped || line.is_ghost) {
builder.push(" \n");
continue;
};
let mut kind = None;
let range = text.line(line.number).byte_range();
for (_, entries) in self.entries.iter() {
let (Ok(idx) | Err(idx)) =
entries.binary_search_by(|entry| entry.range.start.cmp(&range.start));
let mut iter = entries[idx..].iter();
while let Some(entry) = iter.next()
&& entry.range.start < range.end
{
kind = kind.max(Some(entry.kind))
}
}
if let Some(kind) = kind {
let (symbol, symbol_form) = match kind {
EntryKind::Hint => (self.opts.hint.symbol, form::id_of!("gutter.hint")),
EntryKind::Warning => {
(self.opts.warning.symbol, form::id_of!("gutter.warning"))
}
EntryKind::Error => (self.opts.error.symbol, form::id_of!("gutter.error")),
EntryKind::_Custom(symbol, symbol_form, _) => (symbol, symbol_form),
};
builder.push(symbol_form);
builder.push(symbol);
builder.push(FormId::default());
builder.push("\n");
} else {
builder.push(" \n");
}
}
builder.build()
}
fn apply_changes(&mut self, moment: Moment) {
let sh = |value: &mut usize, shift: i32| {
*value = value.saturating_add_signed(shift as isize);
};
for (_, entries) in self.entries.iter_mut() {
let mut shift = 0;
let mut iter = entries.iter_mut().enumerate();
let mut to_remove = Vec::new();
for change in moment.iter() {
let mut is_contained = |i: usize, range: Range<usize>| {
let change_range = change.taken_range();
let change_range = change_range.start.byte()..change_range.end.byte();
if change_range.contains(&range.start) || change_range.contains(&range.end) {
to_remove.push(i);
true
} else {
false
}
};
if let Some((_, entry)) = iter.find_map(|(i, entry)| {
sh(&mut entry.range.start, shift);
sh(&mut entry.range.end, shift);
(!is_contained(i, entry.range.clone())
&& entry.range.end > change.start().byte())
.then_some((i, entry))
}) {
let start_shift =
change.shift()[0] * (entry.range.start > change.start().byte()) as i32;
sh(&mut entry.range.start, start_shift);
sh(&mut entry.range.end, change.shift()[0]);
}
shift += change.shift()[0];
}
for idx in to_remove.into_iter().rev() {
entries.remove(idx);
}
}
}
}
impl Widget for Gutter {
fn text(&self) -> &Text {
&self.text
}
fn text_mut(&mut self) -> duat_core::text::TextMut<'_> {
self.text.as_mut()
}
}
pub struct GutterOpts {
pub hint: GutterSymbolOpts,
pub warning: GutterSymbolOpts,
pub error: GutterSymbolOpts,
renderer: Option<Box<Renderer>>,
}
impl GutterOpts {
pub fn push_on(self, pa: &mut Pass, handle: &Handle) -> Handle<Gutter> {
let text = Text::from(" \n".repeat(handle.text(pa).end_point().line()));
handle.push_outer_widget(
pa,
Gutter {
text,
entries: HashMap::new(),
opts: self,
mouse_coord: None,
},
PushSpecs {
side: Side::Left,
width: Some(1.0),
..PushSpecs::default()
},
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GutterSymbolOpts {
symbol: char,
display: GutterDisplay,
}
pub struct GutterEntries {
list: Vec<GutterEntry>,
_display: GutterDisplay,
}
pub struct GutterEntry {
range: Range<usize>,
msg: Text,
kind: EntryKind,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EntryKind {
Hint,
Warning,
Error,
_Custom(char, FormId, FormId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(unused)]
pub enum GutterDisplay {
Inline(OnlyOnHover),
Spawn(OnlyOnHover),
SpawnCorner(OnlyOnHover, Corner, OnWindow),
OwnLines(OnlyOnHover),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(unused)]
pub enum Corner {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
pub struct GutterEntryBuilder<'p> {
ns: Ns,
pa: &'p mut Pass,
buffer: &'p Handle,
gutter: Handle<Gutter>,
entries: GutterEntries,
}
impl<'g> GutterEntryBuilder<'g> {
pub fn add_related_hint(mut self, range: impl TextRange, msg: Text) -> Self {
let text = self.buffer.text(self.pa);
let range = range.to_range(text.len());
self.entries
.list
.push(GutterEntry { range, msg, kind: EntryKind::Hint });
self
}
pub fn add_related_warning(mut self, range: impl TextRange, msg: Text) -> Self {
let text = self.buffer.text(self.pa);
let range = range.to_range(text.len());
self.entries
.list
.push(GutterEntry { range, msg, kind: EntryKind::Warning });
self
}
pub fn add_related_error(mut self, range: impl TextRange, msg: Text) -> Self {
let text = self.buffer.text(self.pa);
let range = range.to_range(text.len());
self.entries
.list
.push(GutterEntry { range, msg, kind: EntryKind::Error });
self
}
}
impl<'g> Drop for GutterEntryBuilder<'g> {
fn drop(&mut self) {
let (buf, gtr) = self.pa.write_many((self.buffer, &self.gutter));
let mut renderer = gtr.opts.renderer.take().unwrap();
renderer(&self.entries, self.ns, buf.text_mut().parts());
gtr.opts.renderer = Some(renderer);
let entries = gtr.entries.entry(self.ns).or_default();
for entry in std::mem::take(&mut self.entries.list) {
let (Ok(idx) | Err(idx)) =
entries.binary_search_by(|e| e.range.start.cmp(&entry.range.start));
entries.insert(idx, entry);
}
}
}
#[allow(private_bounds)]
trait Sealed {}
#[allow(private_bounds)]
pub trait GutterBuffer: Sealed {
fn remove_gutter_entries(&self, pa: &mut Pass, ns: Ns);
fn add_hint<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g>;
fn add_warning<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g>;
fn add_error<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g>;
}
impl Sealed for Handle {}
impl GutterBuffer for Handle {
#[track_caller]
fn remove_gutter_entries(&self, pa: &mut Pass, ns: Ns) {
let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
panic!("Tried to remove Gutter entries on Buffer with no Gutter");
};
gutter.write(pa).entries.remove(&ns);
self.text_mut(pa).remove_tags(ns, ..);
}
#[track_caller]
fn add_hint<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g> {
let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
panic!("Tried to add a Gutter entry on Buffer with no Gutter");
};
let text = self.text(pa);
let range = range.to_range(text.len());
let display = gutter.read(pa).opts.hint.display;
GutterEntryBuilder {
ns,
pa,
buffer: self,
gutter,
entries: GutterEntries {
list: vec![GutterEntry { range, msg, kind: EntryKind::Hint }],
_display: display,
},
}
}
#[track_caller]
fn add_warning<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g> {
let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
panic!("Tried to add a Gutter entry on Buffer with no Gutter");
};
let text = self.text(pa);
let range = range.to_range(text.len());
let display = gutter.read(pa).opts.hint.display;
GutterEntryBuilder {
ns,
pa,
buffer: self,
gutter,
entries: GutterEntries {
list: vec![GutterEntry { range, msg, kind: EntryKind::Warning }],
_display: display,
},
}
}
#[track_caller]
fn add_error<'g>(
&'g self,
pa: &'g mut Pass,
ns: Ns,
range: impl TextRange,
msg: Text,
) -> GutterEntryBuilder<'g> {
let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
panic!("Tried to add a Gutter entry on Buffer with no Gutter");
};
let text = self.text(pa);
let range = range.to_range(text.len());
let display = gutter.read(pa).opts.hint.display;
GutterEntryBuilder {
ns,
pa,
buffer: self,
gutter,
entries: GutterEntries {
list: vec![GutterEntry { range, msg, kind: EntryKind::Error }],
_display: display,
},
}
}
}
pub fn default_renderer(entries: &GutterEntries, ns: Ns, mut parts: TextParts<'_>) {
for entry in &entries.list {
let form_tag = match entry.kind {
EntryKind::Hint => form::id_of!("buffer.hint").to_tag(190),
EntryKind::Warning => form::id_of!("buffer.warning").to_tag(191),
EntryKind::Error => form::id_of!("buffer.error").to_tag(192),
EntryKind::_Custom(.., text_form) => text_form.to_tag(193),
};
parts.tags.insert(ns, entry.range.clone(), form_tag);
}
}
type Renderer = dyn FnMut(&GutterEntries, Ns, TextParts<'_>) + 'static + Send;
type OnlyOnHover = bool;
type OnWindow = bool;