1use std::{collections::HashMap, ops::Range, sync::Once};
12
13use duat_core::{
14 Ns,
15 buffer::{Buffer, Moment},
16 context::{self, Handle},
17 data::Pass,
18 form::{self, Form, FormId},
19 hook::{self, BufferOpened, BufferUpdated, OnMouseEvent},
20 text::{Inlay, Text, TextParts, TextRange, TwoPoints},
21 txt,
22 ui::{Coord, PushSpecs, Side, Widget},
23};
24
25pub struct Gutter {
33 text: Text,
34 entries: HashMap<Ns, Vec<GutterEntry>>,
35 opts: GutterOpts,
36 mouse_coord: Option<Coord>,
37}
38
39fn initial_setup() {
40 form::set_weak("gutter.hint", Form::mimic("default.info"));
41 form::set_weak("gutter.warning", Form::mimic("default.warning"));
42 form::set_weak("gutter.error", Form::mimic("default.error"));
43 form::set_weak("buffer.hint", Form::new().underline_grey().underlined());
44 form::set_weak(
45 "buffer.warning",
46 Form::new().underline_yellow().underlined(),
47 );
48 form::set_weak("buffer.error", Form::new().underline_red().underlined());
49
50 let ns = Ns::new();
51 let msg_ns = Ns::new();
52
53 hook::add::<BufferOpened>(move |pa, buffer| _ = buffer.read(pa).moment_for(ns));
54 hook::add::<BufferUpdated>(move |pa, buffer| {
55 let Some((gutter, _)) = buffer.get_related::<Gutter>(pa).first().cloned() else {
56 return;
57 };
58
59 let printed_line_ranges = buffer.printed_line_ranges(pa);
60
61 let (gtr, buf) = pa.write_many((&gutter, buffer));
62 gtr.apply_changes(buf.moment_for(ns));
63
64 let (gt, buf, area) = pa.write_many((&gutter, buffer, buffer.area()));
65 buf.text_parts().tags.remove(msg_ns, ..);
66 let opts = buf.print_opts();
67
68 let mouse_point = gt
69 .mouse_coord
70 .filter(|&coord| coord >= area.top_left() && coord < area.bottom_right())
71 .and_then(|coord| {
72 Some(
73 area.points_at_coord(buf.text(), coord, opts)?
74 .as_within()?
75 .real,
76 )
77 });
78
79 let entries = gt
80 .entries
81 .iter()
82 .flat_map(|(_, entries)| entries)
83 .filter(|entry| {
84 let is_onscreen = printed_line_ranges
85 .iter()
86 .any(|range| range.contains(&entry.range.end));
87
88 let display = match entry.kind {
89 EntryKind::Hint => gt.opts.hint.display,
90 EntryKind::Warning => gt.opts.warning.display,
91 EntryKind::Error => gt.opts.error.display,
92 EntryKind::_Custom(..) => todo!(),
93 };
94
95 let do_show = match display {
96 GutterDisplay::OwnLines(always) => {
97 always
98 || mouse_point.is_some_and(|point| entry.range.contains(&point.byte()))
99 }
100 GutterDisplay::Inline(_) => todo!(),
101 GutterDisplay::Spawn(_) => todo!(),
102 GutterDisplay::SpawnCorner(..) => todo!(),
103 };
104
105 do_show && is_onscreen
106 });
107
108 for entry in entries {
109 let Some(line) = buf.text()[entry.range.clone()].lines().last() else {
110 continue;
111 };
112
113 let range = line.range();
114 let lnum = range.start.line();
115 let Some(columns) =
116 area.columns_at(buf.text(), TwoPoints::new_after_ghost(range.start), opts)
117 else {
118 continue;
119 };
120
121 let mut parts = buf.text_parts();
122
123 let inlay = Inlay::new(txt!("{}{entry.msg}\n", " ".repeat(columns.wrapped)));
124 let line_end = parts.strs.line(lnum).byte_range().end;
125 parts.tags.insert(msg_ns, line_end, inlay)
126 }
127 })
128 .lateness(100_000_000);
129
130 hook::add::<BufferUpdated>(|pa, buffer| {
131 let Some((gutter, _)) = buffer.get_related::<Gutter>(pa).first().cloned() else {
132 return;
133 };
134
135 gutter.write(pa).text = Gutter::form_text(gutter.read(pa), pa, buffer);
136 })
137 .lateness(usize::MAX);
138
139 hook::add::<OnMouseEvent<Buffer>>(move |pa, event| {
140 let Some((gutter, _)) = event.handle.get_related::<Gutter>(pa).first().cloned() else {
141 return;
142 };
143
144 gutter.write(pa).mouse_coord = Some(event.coord);
145 })
146 .lateness(usize::MAX);
147
148 hook::add::<OnMouseEvent>(move |pa, _| {
149 for gutter in context::windows().handles_of::<Gutter>(pa) {
150 let gt = gutter.write(pa);
151 if gt.mouse_coord.take().is_some() {
152 let (buffer, _) = gutter.get_related::<Buffer>(pa).first().cloned().unwrap();
153 buffer.request_update();
154 }
155 }
156 })
157 .lateness(usize::MAX);
158}
159
160impl Gutter {
161 pub fn builder() -> GutterOpts {
163 static ONCE: Once = Once::new();
164 ONCE.call_once(initial_setup);
165
166 GutterOpts {
167 hint: GutterSymbolOpts {
168 symbol: 'i',
169 display: GutterDisplay::OwnLines(false),
170 },
171 warning: GutterSymbolOpts {
172 symbol: '!',
173 display: GutterDisplay::OwnLines(false),
174 },
175 error: GutterSymbolOpts {
176 symbol: '*',
177 display: GutterDisplay::OwnLines(true),
178 },
179 renderer: Some(Box::new(default_renderer)),
180 }
181 }
182
183 fn form_text(&self, pa: &Pass, buffer: &Handle) -> Text {
184 let printed_line_numbers = buffer.printed_line_numbers(pa);
185 let text = buffer.text(pa);
186
187 let mut builder = Text::builder();
188
189 for (idx, line) in printed_line_numbers.iter().enumerate() {
190 if idx > 0 && (line.is_wrapped || line.is_ghost) {
191 builder.push(" \n");
192 continue;
193 };
194
195 let mut kind = None;
196 let range = text.line(line.number).byte_range();
197
198 for (_, entries) in self.entries.iter() {
199 let (Ok(idx) | Err(idx)) =
200 entries.binary_search_by(|entry| entry.range.start.cmp(&range.start));
201
202 let mut iter = entries[idx..].iter();
203 while let Some(entry) = iter.next()
204 && entry.range.start < range.end
205 {
206 kind = kind.max(Some(entry.kind))
207 }
208 }
209
210 if let Some(kind) = kind {
211 let (symbol, symbol_form) = match kind {
212 EntryKind::Hint => (self.opts.hint.symbol, form::id_of!("gutter.hint")),
213 EntryKind::Warning => {
214 (self.opts.warning.symbol, form::id_of!("gutter.warning"))
215 }
216 EntryKind::Error => (self.opts.error.symbol, form::id_of!("gutter.error")),
217 EntryKind::_Custom(symbol, symbol_form, _) => (symbol, symbol_form),
218 };
219
220 builder.push(symbol_form);
221 builder.push(symbol);
222 builder.push(FormId::default());
223 builder.push("\n");
224 } else {
225 builder.push(" \n");
226 }
227 }
228
229 builder.build()
230 }
231
232 fn apply_changes(&mut self, moment: Moment) {
233 let sh = |value: &mut usize, shift: i32| {
234 *value = value.saturating_add_signed(shift as isize);
235 };
236
237 for (_, entries) in self.entries.iter_mut() {
238 let mut shift = 0;
239 let mut iter = entries.iter_mut().enumerate();
240 let mut to_remove = Vec::new();
241
242 for change in moment.iter() {
243 let mut is_contained = |i: usize, range: Range<usize>| {
244 let change_range = change.taken_range();
245 let change_range = change_range.start.byte()..change_range.end.byte();
246 if change_range.contains(&range.start) || change_range.contains(&range.end) {
247 to_remove.push(i);
248 true
249 } else {
250 false
251 }
252 };
253
254 if let Some((_, entry)) = iter.find_map(|(i, entry)| {
255 sh(&mut entry.range.start, shift);
256 sh(&mut entry.range.end, shift);
257
258 (!is_contained(i, entry.range.clone())
259 && entry.range.end > change.start().byte())
260 .then_some((i, entry))
261 }) {
262 let start_shift =
263 change.shift()[0] * (entry.range.start > change.start().byte()) as i32;
264
265 sh(&mut entry.range.start, start_shift);
266 sh(&mut entry.range.end, change.shift()[0]);
267 }
268 shift += change.shift()[0];
269 }
270
271 for idx in to_remove.into_iter().rev() {
272 entries.remove(idx);
273 }
274 }
275 }
276}
277
278impl Widget for Gutter {
279 fn text(&self) -> &Text {
280 &self.text
281 }
282
283 fn text_mut(&mut self) -> duat_core::text::TextMut<'_> {
284 self.text.as_mut()
285 }
286}
287
288pub struct GutterOpts {
293 pub hint: GutterSymbolOpts,
305 pub warning: GutterSymbolOpts,
318 pub error: GutterSymbolOpts,
331 renderer: Option<Box<Renderer>>,
332}
333
334impl GutterOpts {
335 pub fn push_on(self, pa: &mut Pass, handle: &Handle) -> Handle<Gutter> {
343 let text = Text::from(" \n".repeat(handle.text(pa).end_point().line()));
344
345 handle.push_outer_widget(
346 pa,
347 Gutter {
348 text,
349 entries: HashMap::new(),
350 opts: self,
351 mouse_coord: None,
352 },
353 PushSpecs {
354 side: Side::Left,
355 width: Some(1.0),
356 ..PushSpecs::default()
357 },
358 )
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct GutterSymbolOpts {
364 symbol: char,
365 display: GutterDisplay,
366}
367
368pub struct GutterEntries {
370 list: Vec<GutterEntry>,
372 _display: GutterDisplay,
374}
375
376pub struct GutterEntry {
381 range: Range<usize>,
382 msg: Text,
383 kind: EntryKind,
384}
385
386#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
387enum EntryKind {
388 Hint,
389 Warning,
390 Error,
391 _Custom(char, FormId, FormId),
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397#[allow(unused)]
398pub enum GutterDisplay {
399 Inline(OnlyOnHover),
405 Spawn(OnlyOnHover),
411 SpawnCorner(OnlyOnHover, Corner, OnWindow),
423 OwnLines(OnlyOnHover),
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432#[allow(unused)]
433pub enum Corner {
434 TopLeft,
435 TopRight,
436 BottomRight,
437 BottomLeft,
438}
439
440pub struct GutterEntryBuilder<'p> {
449 ns: Ns,
450 pa: &'p mut Pass,
451 buffer: &'p Handle,
452 gutter: Handle<Gutter>,
453 entries: GutterEntries,
454}
455
456impl<'g> GutterEntryBuilder<'g> {
457 pub fn add_related_hint(mut self, range: impl TextRange, msg: Text) -> Self {
462 let text = self.buffer.text(self.pa);
463 let range = range.to_range(text.len());
464
465 self.entries
466 .list
467 .push(GutterEntry { range, msg, kind: EntryKind::Hint });
468
469 self
470 }
471
472 pub fn add_related_warning(mut self, range: impl TextRange, msg: Text) -> Self {
474 let text = self.buffer.text(self.pa);
475 let range = range.to_range(text.len());
476
477 self.entries
478 .list
479 .push(GutterEntry { range, msg, kind: EntryKind::Warning });
480
481 self
482 }
483
484 pub fn add_related_error(mut self, range: impl TextRange, msg: Text) -> Self {
490 let text = self.buffer.text(self.pa);
491 let range = range.to_range(text.len());
492
493 self.entries
494 .list
495 .push(GutterEntry { range, msg, kind: EntryKind::Error });
496
497 self
498 }
499}
500
501impl<'g> Drop for GutterEntryBuilder<'g> {
502 fn drop(&mut self) {
503 let (buf, gtr) = self.pa.write_many((self.buffer, &self.gutter));
504
505 let mut renderer = gtr.opts.renderer.take().unwrap();
506 renderer(&self.entries, self.ns, buf.text_mut().parts());
507 gtr.opts.renderer = Some(renderer);
508
509 let entries = gtr.entries.entry(self.ns).or_default();
510
511 for entry in std::mem::take(&mut self.entries.list) {
512 let (Ok(idx) | Err(idx)) =
513 entries.binary_search_by(|e| e.range.start.cmp(&entry.range.start));
514 entries.insert(idx, entry);
515 }
516 }
517}
518
519#[allow(private_bounds)]
520trait Sealed {}
521#[allow(private_bounds)]
525pub trait GutterBuffer: Sealed {
526 fn remove_gutter_entries(&self, pa: &mut Pass, ns: Ns);
528
529 fn add_hint<'g>(
535 &'g self,
536 pa: &'g mut Pass,
537 ns: Ns,
538 range: impl TextRange,
539 msg: Text,
540 ) -> GutterEntryBuilder<'g>;
541
542 fn add_warning<'g>(
548 &'g self,
549 pa: &'g mut Pass,
550 ns: Ns,
551 range: impl TextRange,
552 msg: Text,
553 ) -> GutterEntryBuilder<'g>;
554
555 fn add_error<'g>(
560 &'g self,
561 pa: &'g mut Pass,
562 ns: Ns,
563 range: impl TextRange,
564 msg: Text,
565 ) -> GutterEntryBuilder<'g>;
566}
567
568impl Sealed for Handle {}
569impl GutterBuffer for Handle {
570 #[track_caller]
571 fn remove_gutter_entries(&self, pa: &mut Pass, ns: Ns) {
572 let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
573 panic!("Tried to remove Gutter entries on Buffer with no Gutter");
574 };
575
576 gutter.write(pa).entries.remove(&ns);
577 self.text_mut(pa).remove_tags(ns, ..);
578 }
579
580 #[track_caller]
581 fn add_hint<'g>(
582 &'g self,
583 pa: &'g mut Pass,
584 ns: Ns,
585 range: impl TextRange,
586 msg: Text,
587 ) -> GutterEntryBuilder<'g> {
588 let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
589 panic!("Tried to add a Gutter entry on Buffer with no Gutter");
590 };
591
592 let text = self.text(pa);
593 let range = range.to_range(text.len());
594 let display = gutter.read(pa).opts.hint.display;
595
596 GutterEntryBuilder {
597 ns,
598 pa,
599 buffer: self,
600 gutter,
601 entries: GutterEntries {
602 list: vec![GutterEntry { range, msg, kind: EntryKind::Hint }],
603 _display: display,
604 },
605 }
606 }
607
608 #[track_caller]
609 fn add_warning<'g>(
610 &'g self,
611 pa: &'g mut Pass,
612 ns: Ns,
613 range: impl TextRange,
614 msg: Text,
615 ) -> GutterEntryBuilder<'g> {
616 let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
617 panic!("Tried to add a Gutter entry on Buffer with no Gutter");
618 };
619
620 let text = self.text(pa);
621 let range = range.to_range(text.len());
622 let display = gutter.read(pa).opts.hint.display;
623
624 GutterEntryBuilder {
625 ns,
626 pa,
627 buffer: self,
628 gutter,
629 entries: GutterEntries {
630 list: vec![GutterEntry { range, msg, kind: EntryKind::Warning }],
631 _display: display,
632 },
633 }
634 }
635
636 #[track_caller]
637 fn add_error<'g>(
638 &'g self,
639 pa: &'g mut Pass,
640 ns: Ns,
641 range: impl TextRange,
642 msg: Text,
643 ) -> GutterEntryBuilder<'g> {
644 let Some((gutter, _)) = self.get_related::<Gutter>(pa).first().cloned() else {
645 panic!("Tried to add a Gutter entry on Buffer with no Gutter");
646 };
647
648 let text = self.text(pa);
649 let range = range.to_range(text.len());
650 let display = gutter.read(pa).opts.hint.display;
651
652 GutterEntryBuilder {
653 ns,
654 pa,
655 buffer: self,
656 gutter,
657 entries: GutterEntries {
658 list: vec![GutterEntry { range, msg, kind: EntryKind::Error }],
659 _display: display,
660 },
661 }
662 }
663}
664
665pub fn default_renderer(entries: &GutterEntries, ns: Ns, mut parts: TextParts<'_>) {
670 for entry in &entries.list {
671 let form_tag = match entry.kind {
672 EntryKind::Hint => form::id_of!("buffer.hint").to_tag(190),
673 EntryKind::Warning => form::id_of!("buffer.warning").to_tag(191),
674 EntryKind::Error => form::id_of!("buffer.error").to_tag(192),
675 EntryKind::_Custom(.., text_form) => text_form.to_tag(193),
676 };
677
678 parts.tags.insert(ns, entry.range.clone(), form_tag);
679 }
680}
681
682type Renderer = dyn FnMut(&GutterEntries, Ns, TextParts<'_>) + 'static + Send;
683type OnlyOnHover = bool;
684type OnWindow = bool;