Fix excessive allocations in URL parser

Fixes #3002.
diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs
index 43503f6..5c10bcf 100644
--- a/alacritty/src/url.rs
+++ b/alacritty/src/url.rs
@@ -9,6 +9,7 @@
 use alacritty_terminal::index::Point;
 use alacritty_terminal::renderer::rects::{RenderLine, RenderRect};
 use alacritty_terminal::term::cell::Flags;
+use alacritty_terminal::term::color::Rgb;
 use alacritty_terminal::term::{RenderableCell, RenderableCellContent, SizeInfo};
 
 use crate::config::{Config, RelaxedEq};
@@ -48,6 +49,7 @@
 pub struct Urls {
     locator: UrlLocator,
     urls: Vec<Url>,
+    scheme_buffer: Vec<RenderableCell>,
     last_point: Option<Point>,
     state: UrlLocation,
 }
@@ -56,6 +58,7 @@
     fn default() -> Self {
         Self {
             locator: UrlLocator::new(),
+            scheme_buffer: Vec::new(),
             urls: Vec::new(),
             state: UrlLocation::Reset,
             last_point: None,
@@ -82,63 +85,41 @@
         };
 
         let point: Point = cell.into();
+        let mut end = point;
 
         // Reset URL when empty cells have been skipped
         if point != Point::default() && Some(point.sub(num_cols, 1)) != self.last_point {
             self.reset();
         }
-        self.last_point = Some(point);
+
+        // Extend by one cell for double-width characters
+        if cell.flags.contains(Flags::WIDE_CHAR) {
+            end.col += 1;
+        }
+
+        self.last_point = Some(end);
 
         // Advance parser
         let last_state = mem::replace(&mut self.state, self.locator.advance(c));
         match (self.state, last_state) {
-            (UrlLocation::Url(_length, end_offset), _) => {
-                let mut end = point;
+            (UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => {
+                // Create empty URL
+                self.urls.push(Url { lines: Vec::new(), end_offset, num_cols });
 
-                // Extend by one cell for double-width characters
-                if cell.flags.contains(Flags::WIDE_CHAR) {
-                    end.col += 1;
-
-                    self.last_point = Some(end);
+                // Push schemes into URL
+                for scheme_cell in self.scheme_buffer.split_off(0) {
+                    let point = scheme_cell.into();
+                    self.extend_url(point, point, scheme_cell.fg, end_offset);
                 }
 
-                if let Some(url) = self.urls.last_mut() {
-                    let last_index = url.lines.len() - 1;
-                    let last_line = &mut url.lines[last_index];
-
-                    if last_line.color == cell.fg {
-                        // Update existing line
-                        last_line.end = end;
-                    } else {
-                        // Create new line with different color
-                        url.lines.push(RenderLine { start: point, end, color: cell.fg });
-                    }
-
-                    // Update offset
-                    url.end_offset = end_offset;
-                }
+                // Push the new cell into URL
+                self.extend_url(point, end, cell.fg, end_offset);
             },
-            (UrlLocation::Reset, UrlLocation::Scheme) => {
-                self.urls.pop();
+            (UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => {
+                self.extend_url(point, end, cell.fg, end_offset);
             },
-            (UrlLocation::Scheme, UrlLocation::Reset) => {
-                self.urls.push(Url {
-                    lines: vec![RenderLine { start: point, end: point, color: cell.fg }],
-                    end_offset: 0,
-                    num_cols,
-                });
-            },
-            (UrlLocation::Scheme, _) => {
-                if let Some(url) = self.urls.last_mut() {
-                    if let Some(last_line) = url.lines.last_mut() {
-                        if last_line.color == cell.fg {
-                            last_line.end = point;
-                        } else {
-                            url.lines.push(RenderLine { start: point, end: point, color: cell.fg });
-                        }
-                    }
-                }
-            },
+            (UrlLocation::Scheme, _) => self.scheme_buffer.push(cell),
+            (UrlLocation::Reset, _) => self.reset(),
             _ => (),
         }
 
@@ -148,6 +129,21 @@
         }
     }
 
+    // Extend the last URL
+    fn extend_url(&mut self, start: Point, end: Point, color: Rgb, end_offset: u16) {
+        let url = self.urls.last_mut().unwrap();
+
+        // If color changed, we need to insert a new line
+        if url.lines.last().map(|last| last.color) == Some(color) {
+            url.lines.last_mut().unwrap().end = end;
+        } else {
+            url.lines.push(RenderLine { color, start, end });
+        }
+
+        // Update excluded cells at the end of the URL
+        url.end_offset = end_offset;
+    }
+
     pub fn highlighted(
         &self,
         config: &Config,
@@ -177,12 +173,72 @@
     }
 
     fn reset(&mut self) {
-        // Remove temporarily stored scheme URLs
-        if let UrlLocation::Scheme = self.state {
-            self.urls.pop();
-        }
-
         self.locator = UrlLocator::new();
         self.state = UrlLocation::Reset;
+        self.scheme_buffer.clear();
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    use alacritty_terminal::index::{Column, Line};
+    use alacritty_terminal::term::cell::MAX_ZEROWIDTH_CHARS;
+
+    fn text_to_cells(text: &str) -> Vec<RenderableCell> {
+        text.chars()
+            .enumerate()
+            .map(|(i, c)| RenderableCell {
+                inner: RenderableCellContent::Chars([c; MAX_ZEROWIDTH_CHARS + 1]),
+                line: Line(0),
+                column: Column(i),
+                fg: Default::default(),
+                bg: Default::default(),
+                bg_alpha: 0.,
+                flags: Flags::empty(),
+            })
+            .collect()
+    }
+
+    #[test]
+    fn multi_color_url() {
+        let mut input = text_to_cells("test https://example.org ing");
+        let num_cols = input.len();
+
+        input[10].fg = Rgb { r: 0xff, g: 0x00, b: 0xff };
+
+        let mut urls = Urls::new();
+
+        for cell in input {
+            urls.update(num_cols, cell);
+        }
+
+        let url = urls.urls.first().unwrap();
+        assert_eq!(url.start().col, Column(5));
+        assert_eq!(url.end().col, Column(23));
+    }
+
+    #[test]
+    fn multiple_urls() {
+        let input = text_to_cells("test git:a git:b git:c ing");
+        let num_cols = input.len();
+
+        let mut urls = Urls::new();
+
+        for cell in input {
+            urls.update(num_cols, cell);
+        }
+
+        assert_eq!(urls.urls.len(), 3);
+
+        assert_eq!(urls.urls[0].start().col, Column(5));
+        assert_eq!(urls.urls[0].end().col, Column(9));
+
+        assert_eq!(urls.urls[1].start().col, Column(11));
+        assert_eq!(urls.urls[1].end().col, Column(15));
+
+        assert_eq!(urls.urls[2].start().col, Column(17));
+        assert_eq!(urls.urls[2].end().col, Column(21));
     }
 }