Skip to main content

duat_base/widgets/
gutter.rs

1//! A gutter to add side information relating to the [`Buffer`]
2//!
3//! This struct is meant to be used by plugins like `duat-lsp`, which
4//! can show diagnostics about a `Buffer`. This [`Widget`] will then
5//! be used to show that there are errors in the `Buffer`.
6//!
7//! Additionally, this module contains functions that are used to add
8//! errors to a `Buffer`.
9//!
10//! [`Buffer`]: duat_core::buffer::Buffer
11use 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
25/// A struct to hold diagnostic hints about a [`Buffer`].
26///
27/// It sits on the sides of the `Buffer`, and tells you when there are
28/// things to note about specific lines. These may be hints, warnings,
29/// errors, or custom annotations.
30///
31/// [`Buffer`]: duat_core::buffer::Buffer
32pub 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    /// A builder for a `Gutter`.
162    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
288/// Options for the [`Gutter`].
289///
290/// You can change the character of hints, warnings and errors, and
291/// you can also set how they should be displayed by default.
292pub struct GutterOpts {
293    /// Hints are information that doesn't necessarily indicate that
294    /// something's wrong, but may be related to an actual issue.
295    ///
296    /// By default, they are shown as `'i'` on the [`Gutter`], and the
297    /// hint's [`Text`] is only shown when hovering over it.
298    ///
299    /// On the `Gutter`, it makes use of the `gutter.hint` [`Form`],
300    /// while on the [`Buffer`], it makes use of the `buffer.hint`
301    /// `Form`.
302    ///
303    /// [`Buffer`]: duat_core::buffer::Buffer
304    pub hint: GutterSymbolOpts,
305    /// Warnings are problems with your code that don't necessarily
306    /// prevent it from working or compiling, but otherwise represent
307    /// inadequacies or things that could be improved upon.
308    ///
309    /// By default, they are shown as `'!'` on the [`Gutter`], and the
310    /// hint's [`Text`] is only shown when hovering over it.
311    ///
312    /// On the `Gutter`, it makes use of the `gutter.warning`
313    /// [`Form`], while on the [`Buffer`], it makes use of the
314    /// `buffer.warning` `Form`.
315    ///
316    /// [`Buffer`]: duat_core::buffer::Buffer
317    pub warning: GutterSymbolOpts,
318    /// Errors are fundamental issues with your code. Either the
319    /// compiler couldn't figure out what you meant, or the code is
320    /// invalid for some reason.
321    ///
322    /// By default, they are shown as `'*'` on the [`Gutter`], and the
323    /// hint's [`Text`] is shown as [`Inlay`] text on separate lines.
324    ///
325    /// On the `Gutter`, it makes use of the `gutter.error`
326    /// [`Form`], while on the [`Buffer`], it makes use of the
327    /// `buffer.error` `Form`.
328    ///
329    /// [`Buffer`]: duat_core::buffer::Buffer
330    pub error: GutterSymbolOpts,
331    renderer: Option<Box<Renderer>>,
332}
333
334impl GutterOpts {
335    /// Places a [`Gutter`] around a [`Buffer`].
336    ///
337    /// The [`Widget`] will be pushed on the "outside". That is, if
338    /// there are other widgets pushed on the buffer, this one will be
339    /// placed around them.
340    ///
341    /// [`Buffer`]: duat_core::buffer::Buffer
342    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
368/// Related entries on the [`Gutter`].
369pub struct GutterEntries {
370    /// The entries that are related.
371    list: Vec<GutterEntry>,
372    /// How to display the entry's message.
373    _display: GutterDisplay,
374}
375
376/// An entry in the [`Gutter`].
377///
378/// This contains a range in the [`Text`] and a message, in the form
379/// of a `Text`.
380pub 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/// How to display the accompanying [`Text`] message to a [`Gutter`]
395/// entry.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397#[allow(unused)]
398pub enum GutterDisplay {
399    /// The [`Text`] will be shown at the end of the line, potentially
400    /// running off out of screen.
401    ///
402    /// If [`GutterEntryBuilder::only_on_hover`] is not called, this
403    /// display method will default to always be shown.
404    Inline(OnlyOnHover),
405    /// The [`Text`] will be shown as a spawned widget near the
406    /// entry's range.
407    ///
408    /// If [`GutterEntryBuilder::only_on_hover`] is not called, this
409    /// display method will default to show up only on hover.
410    Spawn(OnlyOnHover),
411    /// The [`Text`] will be show as a spawned widget on one of the
412    /// corners.
413    ///
414    /// If [`OnWindow`] is set to true, this will spawn it on the
415    /// corners of the window. Otherwise, it will be spawned on the
416    /// corners of the [`Buffer`]
417    ///
418    /// If [`GutterEntryBuilder::only_on_hover`] is not called, this
419    /// display method will default to show up only on hover.
420    ///
421    /// [`Buffer`]: duat_core::buffer::Buffer
422    SpawnCorner(OnlyOnHover, Corner, OnWindow),
423    /// The [`Text`] will be shown as [`Inlay`] lines under the
424    /// entry's range.
425    ///
426    /// If [`GutterEntryBuilder::only_on_hover`] is not called, this
427    /// display method will default to always be shown.
428    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
440/// A builder for a [`Gutter`] entry.
441///
442/// This lets you add more related messages to this entry, which will
443/// make their display cohesive. You may, for example, have an error
444/// that happens in a specific line, because of a decision you made on
445/// another line (e.g. borrowing errors on Rust), which should be
446/// interlinked with this error, in order to show more cohesive
447/// diagnostics.
448pub 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    /// Add a hint that is related to this entry.
458    ///
459    /// This could be something like the first borrow, which prevented
460    /// a future borrow from making sense (in Rust).
461    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    /// Add a warning that is related to this entry.
473    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    /// Add an error that is related to this entry.
485    ///
486    /// This could be more errors on the same range, since it's
487    /// possible that multiple things went wrong, or more context
488    /// would be helpful.
489    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/// Trait for adding gutter entries to a [`Buffer`].
522///
523/// [`Buffer`]: duat_core::buffer::Buffer
524#[allow(private_bounds)]
525pub trait GutterBuffer: Sealed {
526    /// Remove all [`Gutter`] entries from a given [`Ns`].
527    fn remove_gutter_entries(&self, pa: &mut Pass, ns: Ns);
528
529    /// Add a hint to the [`Gutter`] and the [`Buffer`].
530    ///
531    /// This could just be useful information, like the fact that
532    /// something won't be included in compilation because of a `cfg`
533    /// attribute.
534    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    /// Add a warning to the [`Gutter`] and the [`Buffer`].
543    ///
544    /// This could be improvements that you could do to your code, or
545    /// ways in which it is innadequate that don't necessarily hinder
546    /// it from working properly.
547    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    /// Add an error to the [`Gutter`] and the [`Buffer`].
556    ///
557    /// These are fundamental issues in your code, and either prevent
558    /// compilation, or prevent it from working properly.
559    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
665/// The default [`Gutter`] renderer.
666///
667/// You can use this if you want to render things differently in some
668/// situations, but not all.
669pub 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;