Repository: alacritty/alacritty (language: rust)

Issue:
underline url moves as more lines are added to terminal output
For bug reports, the following information can help speed up the process. Please
describe the bug that you have found and what you would expect to happen
instead.

### System

OS: Linux/BSD/macOS/Windows
Version: `alacritty --version` output
Linux/BSD: X11/Wayland, DE or Compositor and WM

i3, `Linux p14s 6.6.10-1-MANJARO #1 SMP PREEMPT_DYNAMIC Fri Jan  5 17:38:36 UTC 2024 x86_64 GNU/Linux`

### Logs

Crashes: STDERR, STDOUT
Font/Terminal size: `alacritty -vv` `alacritty 0.14.0-dev (f1853a03)`
Keyboard and bindings: `alacritty --print-events`



1) run this in your term:
```
while `sleep 0.5` ; do ; echo "`date` https://www.github.com" ; done
```
2) move your cursor on top of one of the urls
3) scroll upwards a bit
=> as more lines are added, the highlight will "move" to keep the same distance relative to the freshest line, so its no longer the actual lined underneath the cursor :laughing: 



Discussion / hints:

https://github.com/alacritty/alacritty/assets/476013/c9221789-96c3-46e7-8629-7a3ebd66b031



Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 659a098e61a..2026893c700 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its
 - Fullwidth semantic escape characters
 - Windows app icon now displays properly in old alt+tab on Windows
 - Alacritty not being properly activated with startup notify
+- Invalid URL highlights after terminal scrolling
 
 ## 0.13.2
 
diff --git a/alacritty/src/display/damage.rs b/alacritty/src/display/damage.rs
index 450643b75b1..8efe0133ba8 100644
--- a/alacritty/src/display/damage.rs
+++ b/alacritty/src/display/damage.rs
@@ -189,6 +189,15 @@ impl FrameDamage {
             self.lines.push(LineDamageBounds::undamaged(line, num_cols));
         }
     }
+
+    /// Check if a range is damaged.
+    #[inline]
+    pub fn intersects(&self, start: Point<usize>, end: Point<usize>) -> bool {
+        self.full
+            || self.lines[start.line].left <= start.column
+            || self.lines[end.line].right >= end.column
+            || (start.line + 1..end.line).any(|line| self.lines[line].is_damaged())
+    }
 }
 
 /// Convert viewport `y` coordinate to [`Rect`] damage coordinate.
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
index 7809c8246de..515532889d4 100644
--- a/alacritty/src/display/mod.rs
+++ b/alacritty/src/display/mod.rs
@@ -29,8 +29,7 @@ use alacritty_terminal::index::{Column, Direction, Line, Point};
 use alacritty_terminal::selection::Selection;
 use alacritty_terminal::term::cell::Flags;
 use alacritty_terminal::term::{
-    self, point_to_viewport, LineDamageBounds, Term, TermDamage, TermMode, MIN_COLUMNS,
-    MIN_SCREEN_LINES,
+    self, LineDamageBounds, Term, TermDamage, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES,
 };
 use alacritty_terminal::vte::ansi::{CursorShape, NamedColor};
 
@@ -746,39 +745,37 @@ impl Display {
         let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None };
 
         // Add damage from the terminal.
-        if self.collect_damage() {
-            match terminal.damage() {
-                TermDamage::Full => self.damage_tracker.frame().mark_fully_damaged(),
-                TermDamage::Partial(damaged_lines) => {
-                    for damage in damaged_lines {
-                        self.damage_tracker.frame().damage_line(damage);
-                    }
-                },
-            }
-            terminal.reset_damage();
+        match terminal.damage() {
+            TermDamage::Full => self.damage_tracker.frame().mark_fully_damaged(),
+            TermDamage::Partial(damaged_lines) => {
+                for damage in damaged_lines {
+                    self.damage_tracker.frame().damage_line(damage);
+                }
+            },
         }
+        terminal.reset_damage();
 
         // Drop terminal as early as possible to free lock.
         drop(terminal);
 
-        // Add damage from alacritty's UI elements overlapping terminal.
-        if self.collect_damage() {
-            let requires_full_damage = self.visual_bell.intensity() != 0.
-                || self.hint_state.active()
-                || search_state.regex().is_some();
-
-            if requires_full_damage {
-                self.damage_tracker.frame().mark_fully_damaged();
-                self.damage_tracker.next_frame().mark_fully_damaged();
-            }
+        // Invalidate highlighted hints if grid has changed.
+        self.validate_hints(display_offset);
 
-            let vi_cursor_viewport_point =
-                vi_cursor_point.and_then(|cursor| point_to_viewport(display_offset, cursor));
+        // Add damage from alacritty's UI elements overlapping terminal.
 
-            self.damage_tracker.damage_vi_cursor(vi_cursor_viewport_point);
-            self.damage_tracker.damage_selection(selection_range, display_offset);
+        let requires_full_damage = self.visual_bell.intensity() != 0.
+            || self.hint_state.active()
+            || search_state.regex().is_some();
+        if requires_full_damage {
+            self.damage_tracker.frame().mark_fully_damaged();
+            self.damage_tracker.next_frame().mark_fully_damaged();
         }
 
+        let vi_cursor_viewport_point =
+            vi_cursor_point.and_then(|cursor| term::point_to_viewport(display_offset, cursor));
+        self.damage_tracker.damage_vi_cursor(vi_cursor_viewport_point);
+        self.damage_tracker.damage_selection(selection_range, display_offset);
+
         // Make sure this window's OpenGL context is active.
         self.make_current();
 
@@ -802,36 +799,27 @@ impl Display {
             let vi_highlighted_hint = &self.vi_highlighted_hint;
             let damage_tracker = &mut self.damage_tracker;
 
-            self.renderer.draw_cells(
-                &size_info,
-                glyph_cache,
-                grid_cells.into_iter().map(|mut cell| {
-                    // Underline hints hovered by mouse or vi mode cursor.
+            let cells = grid_cells.into_iter().map(|mut cell| {
+                // Underline hints hovered by mouse or vi mode cursor.
+                if has_highlighted_hint {
                     let point = term::viewport_to_point(display_offset, cell.point);
-
-                    if has_highlighted_hint {
-                        let hyperlink =
-                            cell.extra.as_ref().and_then(|extra| extra.hyperlink.as_ref());
-                        if highlighted_hint
-                            .as_ref()
-                            .map_or(false, |hint| hint.should_highlight(point, hyperlink))
-                            || vi_highlighted_hint
-                                .as_ref()
-                                .map_or(false, |hint| hint.should_highlight(point, hyperlink))
-                        {
-                            cell.flags.insert(Flags::UNDERLINE);
-                            // Damage hints for the current and next frames.
-                            damage_tracker.frame().damage_point(cell.point);
-                            damage_tracker.next_frame().damage_point(cell.point);
-                        }
+                    let hyperlink = cell.extra.as_ref().and_then(|extra| extra.hyperlink.as_ref());
+
+                    let should_highlight = |hint: &Option<HintMatch>| {
+                        hint.as_ref().map_or(false, |hint| hint.should_highlight(point, hyperlink))
+                    };
+                    if should_highlight(highlighted_hint) || should_highlight(vi_highlighted_hint) {
+                        damage_tracker.frame().damage_point(cell.point);
+                        cell.flags.insert(Flags::UNDERLINE);
                     }
+                }
 
-                    // Update underline/strikeout.
-                    lines.update(&cell);
+                // Update underline/strikeout.
+                lines.update(&cell);
 
-                    cell
-                }),
-            );
+                cell
+            });
+            self.renderer.draw_cells(&size_info, glyph_cache, cells);
         }
 
         let mut rects = lines.rects(&metrics, &size_info);
@@ -1025,10 +1013,17 @@ impl Display {
         let mut dirty = vi_highlighted_hint != self.vi_highlighted_hint;
         self.vi_highlighted_hint = vi_highlighted_hint;
 
+        // Force full redraw if the vi mode highlight was cleared.
+        if dirty && self.vi_highlighted_hint.is_none() {
+            self.damage_tracker.frame().mark_fully_damaged();
+        }
+
         // Abort if mouse highlighting conditions are not met.
         if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) {
-            dirty |= self.highlighted_hint.is_some();
-            self.highlighted_hint = None;
+            if self.highlighted_hint.take().is_some() {
+                self.damage_tracker.frame().mark_fully_damaged();
+                dirty = true;
+            }
             return dirty;
         }
 
@@ -1052,9 +1047,15 @@ impl Display {
             }
         }
 
-        dirty |= self.highlighted_hint != highlighted_hint;
+        let mouse_highlight_dirty = self.highlighted_hint != highlighted_hint;
+        dirty |= mouse_highlight_dirty;
         self.highlighted_hint = highlighted_hint;
 
+        // Force full redraw if the mouse cursor highlight was cleared.
+        if mouse_highlight_dirty && self.highlighted_hint.is_none() {
+            self.damage_tracker.frame().mark_fully_damaged();
+        }
+
         dirty
     }
 
@@ -1113,7 +1114,7 @@ impl Display {
         );
 
         // Damage preedit inside the terminal viewport.
-        if self.collect_damage() && point.line < self.size_info.screen_lines() {
+        if point.line < self.size_info.screen_lines() {
             let damage = LineDamageBounds::new(start.line, 0, num_cols);
             self.damage_tracker.frame().damage_line(damage);
             self.damage_tracker.next_frame().damage_line(damage);
@@ -1224,13 +1225,11 @@ impl Display {
         let bg = config.colors.footer_bar_background();
         for (uri, point) in uris.into_iter().zip(uri_lines) {
             // Damage the uri preview.
-            if self.collect_damage() {
-                let damage = LineDamageBounds::new(point.line, point.column.0, num_cols);
-                self.damage_tracker.frame().damage_line(damage);
+            let damage = LineDamageBounds::new(point.line, point.column.0, num_cols);
+            self.damage_tracker.frame().damage_line(damage);
 
-                // Damage the uri preview for the next frame as well.
-                self.damage_tracker.next_frame().damage_line(damage);
-            }
+            // Damage the uri preview for the next frame as well.
+            self.damage_tracker.next_frame().damage_line(damage);
 
             self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache);
         }
@@ -1270,12 +1269,10 @@ impl Display {
         let fg = config.colors.primary.background;
         let bg = config.colors.normal.red;
 
-        if self.collect_damage() {
-            let damage = LineDamageBounds::new(point.line, point.column.0, timing.len());
-            self.damage_tracker.frame().damage_line(damage);
-            // Damage the render timer for the next frame.
-            self.damage_tracker.next_frame().damage_line(damage);
-        }
+        // Damage render timer for current and next frame.
+        let damage = LineDamageBounds::new(point.line, point.column.0, timing.len());
+        self.damage_tracker.frame().damage_line(damage);
+        self.damage_tracker.next_frame().damage_line(damage);
 
         let glyph_cache = &mut self.glyph_cache;
         self.renderer.draw_string(point, fg, bg, timing.chars(), &self.size_info, glyph_cache);
@@ -1295,12 +1292,10 @@ impl Display {
         let column = Column(self.size_info.columns().saturating_sub(text.len()));
         let point = Point::new(0, column);
 
-        if self.collect_damage() {
-            let damage = LineDamageBounds::new(point.line, point.column.0, columns - 1);
-            self.damage_tracker.frame().damage_line(damage);
-            // Damage it on the next frame in case it goes away.
-            self.damage_tracker.next_frame().damage_line(damage);
-        }
+        // Damage the line indicator for current and next frame.
+        let damage = LineDamageBounds::new(point.line, point.column.0, columns - 1);
+        self.damage_tracker.frame().damage_line(damage);
+        self.damage_tracker.next_frame().damage_line(damage);
 
         let colors = &config.colors;
         let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background);
@@ -1313,12 +1308,6 @@ impl Display {
         }
     }
 
-    /// Returns `true` if damage information should be collected, `false` otherwise.
-    #[inline]
-    fn collect_damage(&self) -> bool {
-        matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) || self.damage_tracker.debug
-    }
-
     /// Highlight damaged rects.
     ///
     /// This function is for debug purposes only.
@@ -1334,6 +1323,34 @@ impl Display {
         }
     }
 
+    /// Check whether a hint highlight needs to be cleared.
+    fn validate_hints(&mut self, display_offset: usize) {
+        let frame = self.damage_tracker.frame();
+        for (hint, reset_mouse) in
+            [(&mut self.highlighted_hint, true), (&mut self.vi_highlighted_hint, false)]
+        {
+            let (start, end) = match hint {
+                Some(hint) => (*hint.bounds().start(), *hint.bounds().end()),
+                None => return,
+            };
+
+            // Convert hint bounds to viewport coordinates.
+            let start = term::point_to_viewport(display_offset, start).unwrap_or_default();
+            let end = term::point_to_viewport(display_offset, end).unwrap_or_else(|| {
+                Point::new(self.size_info.screen_lines() - 1, self.size_info.last_column())
+            });
+
+            // Clear invalidated hints.
+            if frame.intersects(start, end) {
+                if reset_mouse {
+                    self.window.set_mouse_cursor(CursorIcon::Default);
+                }
+                frame.mark_fully_damaged();
+                *hint = None;
+            }
+        }
+    }
+
     /// Request a new frame for a window on Wayland.
     fn request_frame(&mut self, scheduler: &mut Scheduler) {
         // Mark that we've used a frame.
diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs
index 59769883c5e..278bea3bc17 100644
--- a/alacritty_terminal/src/grid/mod.rs
+++ b/alacritty_terminal/src/grid/mod.rs
@@ -137,7 +137,7 @@ pub struct Grid<T> {
     max_scroll_limit: usize,
 }
 
-impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
+impl<T: GridCell + Default + PartialEq> Grid<T> {
     pub fn new(lines: usize, columns: usize, max_scroll_limit: usize) -> Grid<T> {
         Grid {
             raw: Storage::with_capacity(lines, columns),
@@ -356,7 +356,7 @@ impl<T> Grid<T> {
     /// Reset a visible region within the grid.
     pub fn reset_region<D, R: RangeBounds<Line>>(&mut self, bounds: R)
     where
-        T: ResetDiscriminant<D> + GridCell + Clone + Default,
+        T: ResetDiscriminant<D> + GridCell + Default,
         D: PartialEq,
     {
         let start = match bounds.start_bound() {
@@ -392,7 +392,7 @@ impl<T> Grid<T> {
     #[inline]
     pub fn initialize_all(&mut self)
     where
-        T: GridCell + Clone + Default,
+        T: GridCell + Default,
     {
         // Remove all cached lines to clear them of any content.
         self.truncate();
diff --git a/alacritty_terminal/src/grid/resize.rs b/alacritty_terminal/src/grid/resize.rs
index 92ee55d77db..751abae1b82 100644
--- a/alacritty_terminal/src/grid/resize.rs
+++ b/alacritty_terminal/src/grid/resize.rs
@@ -9,7 +9,7 @@ use crate::term::cell::{Flags, ResetDiscriminant};
 use crate::grid::row::Row;
 use crate::grid::{Dimensions, Grid, GridCell};
 
-impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
+impl<T: GridCell + Default + PartialEq> Grid<T> {
     /// Resize the grid's width and/or height.
     pub fn resize<D>(&mut self, reflow: bool, lines: usize, columns: usize)
     where
diff --git a/alacritty_terminal/src/grid/row.rs b/alacritty_terminal/src/grid/row.rs
index 4e22e50bb7b..d4a6e6d30ae 100644
--- a/alacritty_terminal/src/grid/row.rs
+++ b/alacritty_terminal/src/grid/row.rs
@@ -30,7 +30,7 @@ impl<T: PartialEq> PartialEq for Row<T> {
     }
 }
 
-impl<T: Clone + Default> Row<T> {
+impl<T: Default> Row<T> {
     /// Create a new terminal row.
     ///
     /// Ideally the `template` should be `Copy` in all performance sensitive scenarios.
diff --git a/alacritty_terminal/src/grid/storage.rs b/alacritty_terminal/src/grid/storage.rs
index 0a2be43b71a..abf5710347e 100644
--- a/alacritty_terminal/src/grid/storage.rs
+++ b/alacritty_terminal/src/grid/storage.rs
@@ -66,7 +66,7 @@ impl<T> Storage<T> {
     #[inline]
     pub fn with_capacity(visible_lines: usize, columns: usize) -> Storage<T>
     where
-        T: Clone + Default,
+        T: Default,
     {
         // Initialize visible lines; the scrollback buffer is initialized dynamically.
         let mut inner = Vec::with_capacity(visible_lines);
@@ -79,7 +79,7 @@ impl<T> Storage<T> {
     #[inline]
     pub fn grow_visible_lines(&mut self, next: usize)
     where
-        T: Clone + Default,
+        T: Default,
     {
         // Number of lines the buffer needs to grow.
         let additional_lines = next - self.visible_lines;
@@ -125,7 +125,7 @@ impl<T> Storage<T> {
     #[inline]
     pub fn initialize(&mut self, additional_rows: usize, columns: usize)
     where
-        T: Clone + Default,
+        T: Default,
     {
         if self.len + additional_rows > self.inner.len() {
             self.rezero();
diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs
index 81dc1e3acf7..73077644091 100644
--- a/alacritty_terminal/src/term/cell.rs
+++ b/alacritty_terminal/src/term/cell.rs
@@ -124,9 +124,7 @@ impl ResetDiscriminant<Color> for Cell {
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub struct CellExtra {
     zerowidth: Vec<char>,
-
     underline_color: Option<Color>,
-
     hyperlink: Option<Hyperlink>,
 }
 


Test patch: