diff --git a/CHANGELOG.md b/CHANGELOG.md index 396025d..06862ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Override default bindings with subset terminal mode match - On Linux, respect fontconfig's `embeddedbitmap` configuration option - Selecting trailing tab with semantic expansion +- URL parser incorrectly handling Markdown URLs and angled brackets ## 0.3.3 diff --git a/Cargo.lock b/Cargo.lock index 522f5f9..d787300 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,7 @@ dependencies = [ "notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rfind_url 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -153,7 +154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "backtrace" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", @@ -610,7 +611,7 @@ name = "failure" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -874,7 +875,7 @@ dependencies = [ [[package]] name = "http_req" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1779,6 +1780,11 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rfind_url" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rle-decode-fast" version = "1.0.1" @@ -2448,7 +2454,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "android_glue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "cocoa 0.18.4 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2472,7 +2478,7 @@ version = "0.1.0" dependencies = [ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "embed-resource 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "http_req 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "http_req 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "named_pipe 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2577,7 +2583,7 @@ dependencies = [ "checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "22130e92352b948e7e82a49cdb0aa94f2211761117f29e052dd397c1ac33542b" -"checksum backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)" = "88fb679bc9af8fa639198790a77f52d345fe13656c08b43afa9424c206b731c6" +"checksum backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)" = "b5164d292487f037ece34ec0de2fcede2faa162f085dd96d2385ab81b12765ba" "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum bindgen 0.33.2 (registry+https://github.com/rust-lang/crates.io-index)" = "603ed8d8392ace9581e834e26bd09799bf1e989a79bd1aedbb893e72962bdc6e" @@ -2656,7 +2662,7 @@ dependencies = [ "checksum glutin_gles2_sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "89996c30857ae1b4de4b5189abf1ea822a20a9fe9e1c93e5e7b862ff0bdd5cdf" "checksum glutin_glx_sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1290a5ca5e46fcfa7f66f949cc9d9194b2cb6f2ed61892c8c2b82343631dba57" "checksum glutin_wgl_sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f801bbc91efc22dd1c4818a47814fc72bf74d024510451b119381579bfa39021" -"checksum http_req 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "42448f4b1a710db4889978e8c8145f6af60d729be549b1b71e036ecc96f9f26f" +"checksum http_req 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7a3235907ba93aeeb84419957956ab7055f1cc4aacfabd4cd1f32f49addab3ec" "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum image 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)" = "99198e595d012efccf12abf4abc08da2d97be0b0355a2b08d101347527476ba4" @@ -2758,6 +2764,7 @@ dependencies = [ "checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" "checksum regex-syntax 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5485bf1523a9ed51c4964273f22f63f24e31632adb5dad134f488f86a3875c" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum rfind_url 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d090798d14d8cc79d732ab0fc3c77ef3cd62c71d98e02b4f8c7076ad1c484973" "checksum rle-decode-fast 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" "checksum rustc_tools_util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b725dadae9fabc488df69a287f5a99c5eaf5d10853842a8a3dfac52476f544ee" diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index 3b9f991..eb72e7d 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -32,6 +32,7 @@ terminfo = "0.6.1" url = "1.7.1" crossbeam-channel = "0.3.8" copypasta = { path = "../copypasta" } +rfind_url = "0.4.0" [target.'cfg(unix)'.dependencies] nix = "0.14.1" diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 1925a6f..e2cda17 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -120,7 +120,7 @@ pub enum Scroll { } #[derive(Copy, Clone)] -pub enum ViewportPosition { +enum ViewportPosition { Visible(Line), Above, Below, @@ -141,11 +141,25 @@ impl Grid { } } - pub fn visible_to_buffer(&self, point: Point) -> Point { - Point { line: self.visible_line_to_buffer(point.line), col: point.col } + pub fn buffer_to_visible(&self, point: impl Into>) -> Point { + let mut point = point.into(); + + match self.buffer_line_to_visible(point.line) { + ViewportPosition::Visible(line) => point.line = line.0, + ViewportPosition::Above => { + point.col = Column(0); + point.line = 0; + }, + ViewportPosition::Below => { + point.col = self.num_cols(); + point.line = self.num_lines().0 - 1; + }, + } + + point } - pub fn buffer_line_to_visible(&self, line: usize) -> ViewportPosition { + fn buffer_line_to_visible(&self, line: usize) -> ViewportPosition { let offset = line.saturating_sub(self.display_offset); if line < self.display_offset { ViewportPosition::Below @@ -156,7 +170,11 @@ impl Grid { } } - pub fn visible_line_to_buffer(&self, line: Line) -> usize { + pub fn visible_to_buffer(&self, point: Point) -> Point { + Point { line: self.visible_line_to_buffer(point.line), col: point.col } + } + + fn visible_line_to_buffer(&self, line: Line) -> usize { self.line_to_offset(line) + self.display_offset } @@ -596,7 +614,17 @@ pub struct GridIterator<'a, T> { grid: &'a Grid, /// Current position of the iterator within the grid. - pub cur: Point, + cur: Point, +} + +impl<'a, T> GridIterator<'a, T> { + pub fn point(&self) -> Point { + self.cur + } + + pub fn cell(&self) -> &'a T { + &self.grid[self.cur.line][self.cur.col] + } } impl<'a, T> Iterator for GridIterator<'a, T> { diff --git a/alacritty_terminal/src/grid/tests.rs b/alacritty_terminal/src/grid/tests.rs index a352e74..d28e783 100644 --- a/alacritty_terminal/src/grid/tests.rs +++ b/alacritty_terminal/src/grid/tests.rs @@ -109,8 +109,8 @@ fn test_iter() { assert_eq!(None, iter.prev()); assert_eq!(Some(&1), iter.next()); - assert_eq!(Column(1), iter.cur.col); - assert_eq!(4, iter.cur.line); + assert_eq!(Column(1), iter.point().col); + assert_eq!(4, iter.point().line); assert_eq!(Some(&2), iter.next()); assert_eq!(Some(&3), iter.next()); @@ -118,12 +118,15 @@ fn test_iter() { // test linewrapping assert_eq!(Some(&5), iter.next()); - assert_eq!(Column(0), iter.cur.col); - assert_eq!(3, iter.cur.line); + assert_eq!(Column(0), iter.point().col); + assert_eq!(3, iter.point().line); assert_eq!(Some(&4), iter.prev()); - assert_eq!(Column(4), iter.cur.col); - assert_eq!(4, iter.cur.line); + assert_eq!(Column(4), iter.point().col); + assert_eq!(4, iter.point().line); + + // Make sure iter.cell() returns the current iterator position + assert_eq!(&4, iter.cell()); // test that iter ends at end of grid let mut final_iter = grid.iter_from(Point { line: 0, col: Column(4) }); diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs index f6ea4ad..7d1a8e9 100644 --- a/alacritty_terminal/src/index.rs +++ b/alacritty_terminal/src/index.rs @@ -59,6 +59,12 @@ impl From> for Point { } } +impl From> for Point { + fn from(point: Point) -> Self { + Point::new(Line(point.line), point.col) + } +} + impl From> for Point { fn from(point: Point) -> Self { Point::new(point.line as usize, point.col) diff --git a/alacritty_terminal/src/input.rs b/alacritty_terminal/src/input.rs index 9443a1a..8eceef1 100644 --- a/alacritty_terminal/src/input.rs +++ b/alacritty_terminal/src/input.rs @@ -18,27 +18,25 @@ //! In order to figure that out, state about which modifier keys are pressed //! needs to be tracked. Additionally, we need a bit of a state machine to //! determine what to do when a non-modifier key is pressed. +use crate::url::Url; use std::borrow::Cow; use std::mem; -use std::ops::RangeInclusive; use std::time::Instant; use glutin::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta, TouchPhase, }; -use unicode_width::UnicodeWidthStr; use crate::ansi::{ClearMode, Handler}; use crate::clipboard::ClipboardType; use crate::config::{self, Key}; use crate::event::{ClickState, Mouse}; use crate::grid::Scroll; -use crate::index::{Column, Line, Linear, Point, Side}; +use crate::index::{Column, Line, Point, Side}; use crate::message_bar::{self, Message}; use crate::term::mode::TermMode; -use crate::term::{Search, SizeInfo, Term}; -use crate::url::Url; +use crate::term::{SizeInfo, Term}; use crate::util::start_daemon; pub const FONT_SIZE_STEP: f32 = 0.5; @@ -392,15 +390,18 @@ enum MousePosition { impl<'a, A: ActionContext + 'a> Processor<'a, A> { fn mouse_position(&mut self, point: Point) -> MousePosition { + let buffer_point = self.ctx.terminal().visible_to_buffer(point); + + // Check message bar before URL to ignore URLs in the message bar if let Some(message) = self.message_at_point(Some(point)) { if self.message_close_at_point(point, message) { MousePosition::MessageBarButton } else { MousePosition::MessageBar } - // Check for url should be after check for message bar, since we're not looking into - // message bar content. - } else if let Some(url) = self.ctx.terminal().url_search(point.into()) { + } else if let Some(url) = + self.ctx.terminal().urls().drain(..).find(|url| url.contains(buffer_point)) + { MousePosition::Url(url) } else { MousePosition::Terminal @@ -443,7 +444,7 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { && (!self.ctx.terminal().mode().intersects(mouse_mode) || modifiers.shift) && self.mouse_config.url.launcher.is_some() { - let url_bounds = self.url_bounds_at_point(url, point); + let url_bounds = url.linear_bounds(self.ctx.terminal()); self.ctx.terminal_mut().set_url_highlight(url_bounds); self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand); self.ctx.terminal_mut().dirty = true; @@ -485,47 +486,6 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { } } - fn url_bounds_at_point(&self, url: Url, point: Point) -> RangeInclusive { - let Url { origin, text } = url; - let cols = self.ctx.size_info().cols().0; - - // Calculate the URL's start position - let lines_before = (origin + cols - point.col.0 - 1) / cols; - let (start_col, start_line) = if lines_before > point.line.0 { - (0, 0) - } else { - let start_col = (cols + point.col.0 - origin % cols) % cols; - let start_line = point.line.0 - lines_before; - (start_col, start_line) - }; - - let start = Point::new(start_line, Column(start_col)); - - // Calculate the URL's highlight end position - let len = text.width(); - let url_end_col_denormilized = point.col.0 + len - origin; - - // This means that url ends at the last cell of the line - let end_col = if url_end_col_denormilized % cols == 0 { - cols - 1 - } else { - url_end_col_denormilized % cols - 1 - }; - - let end_line = if end_col == cols - 1 { - point.line.0 + (url_end_col_denormilized) / cols - 1 - } else { - point.line.0 + (url_end_col_denormilized) / cols - }; - - let end = Point::new(end_line, Column(end_col)); - - let start = Linear::from_point(Column(cols), start); - let end = Linear::from_point(Column(cols), end); - - RangeInclusive::new(start, end) - } - fn get_mouse_side(&self) -> Side { let size_info = self.ctx.size_info(); let x = self.ctx.mouse().x; @@ -705,7 +665,9 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { return None; } - let text = self.ctx.terminal().url_search(point.into())?.text; + let point = self.ctx.terminal().visible_to_buffer(point); + let url = self.ctx.terminal().urls().drain(..).find(|url| url.contains(point))?; + let text = self.ctx.terminal().url_to_string(&url); let launcher = self.mouse_config.url.launcher.as_ref()?; let mut args = launcher.args().to_vec(); diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 691a4fe..0cc2cd6 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -20,17 +20,17 @@ use std::{io, mem, ptr}; use font::{self, Size}; use glutin::MouseCursor; +use rfind_url::{Parser, ParserState}; use unicode_width::UnicodeWidthChar; use crate::ansi::{ - self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, + self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo, }; use crate::clipboard::{Clipboard, ClipboardType}; use crate::config::{Config, VisualBellAnimation}; use crate::cursor::CursorKey; use crate::grid::{ BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, - ViewportPosition, }; use crate::index::{self, Column, Contains, IndexRange, Line, Linear, Point}; use crate::input::FONT_SIZE_STEP; @@ -38,7 +38,7 @@ use crate::message_bar::MessageBuffer; use crate::selection::{self, Selection, SelectionRange, Span}; use crate::term::cell::{Cell, Flags, LineLength}; use crate::term::color::Rgb; -use crate::url::{Url, UrlParser}; +use crate::url::Url; #[cfg(windows)] use crate::tty; @@ -58,8 +58,6 @@ pub trait Search { fn semantic_search_left(&self, _: Point) -> Point; /// Find the nearest semantic boundary _to the point_ of provided point. fn semantic_search_right(&self, _: Point) -> Point; - /// Find the nearest URL boundary in both directions. - fn url_search(&self, _: Point) -> Option; /// Find the nearest matching bracket. fn bracket_search(&self, _: Point) -> Option>; } @@ -77,11 +75,11 @@ impl Search for Term { break; } - if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { + if iter.point().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { break; // cut off if on new line or hit escape char } - point = iter.cur; + point = iter.point(); } point @@ -99,7 +97,7 @@ impl Search for Term { break; } - point = iter.cur; + point = iter.point(); if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { break; // cut off if on new line or hit escape char @@ -109,40 +107,6 @@ impl Search for Term { point } - fn url_search(&self, mut point: Point) -> Option { - let last_col = self.grid.num_cols() - 1; - - // Switch first line from top to bottom - point.line = self.grid.num_lines().0 - point.line - 1; - - // Remove viewport scroll offset - point.line += self.grid.display_offset(); - - // Create forwards and backwards iterators - let mut iterf = self.grid.iter_from(point); - point.col += 1; - let mut iterb = self.grid.iter_from(point); - - // Find URLs - let mut url_parser = UrlParser::new(); - while let Some(cell) = iterb.prev() { - if (iterb.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) - || url_parser.advance_left(cell) - { - break; - } - } - - while let Some(cell) = iterf.next() { - if url_parser.advance_right(cell) - || (iterf.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) - { - break; - } - } - url_parser.url() - } - fn bracket_search(&self, point: Point) -> Option> { let start_char = self.grid[point.line][point.col].c; @@ -175,7 +139,7 @@ impl Search for Term { // Check if the bracket matches if c == end_char && skip_pairs == 0 { - return Some(iter.cur); + return Some(iter.point()); } else if c == start_char { skip_pairs += 1; } else if c == end_char { @@ -235,44 +199,27 @@ impl<'a> RenderableCellsIter<'a> { let cursor_offset = grid.line_to_offset(term.cursor.point.line); let inner = grid.display_iter(); - let selection_range = selection.and_then(|span| { - // Get on-screen lines of the selection's locations - let start_line = grid.buffer_line_to_visible(span.start.line); - let end_line = grid.buffer_line_to_visible(span.end.line); - - // Limit block selection columns to within start/end points - let (limit_start, limit_end) = - if span.is_block { (span.start.col, span.end.col) } else { (Column(0), Column(0)) }; - - // Get start/end locations based on what part of selection is on screen - let locations = match (start_line, end_line) { - (ViewportPosition::Visible(start_line), ViewportPosition::Visible(end_line)) => { - Some((start_line, span.start.col, end_line, span.end.col)) - }, - (ViewportPosition::Visible(start_line), ViewportPosition::Above) => { - Some((start_line, span.start.col, Line(0), limit_end)) - }, - (ViewportPosition::Below, ViewportPosition::Visible(end_line)) => { - Some((grid.num_lines(), limit_start, end_line, span.end.col)) - }, - (ViewportPosition::Below, ViewportPosition::Above) => { - Some((grid.num_lines(), limit_start, Line(0), limit_end)) - }, - _ => None, + let selection_range = selection.map(|span| { + let (limit_start, limit_end) = if span.is_block { + (span.end.col, span.start.col) + } else { + (Column(0), term.cols() - 1) }; - locations.map(|(start_line, start_col, end_line, end_col)| { - // start and end *lines* are swapped as we switch from buffer to - // Line coordinates. - let mut end = Point { line: start_line, col: start_col }; - let mut start = Point { line: end_line, col: end_col }; + // Get on-screen lines of the selection's locations + let mut start = term.buffer_to_visible(span.start); + let mut end = term.buffer_to_visible(span.end); - if start > end { - ::std::mem::swap(&mut start, &mut end); - } + // Start and end lines are swapped as we switch from buffer to line coordinates + if start > end { + mem::swap(&mut start, &mut end); + } - SelectionRange::new(start, end, span.is_block) - }) + // Trim start/end with partially visible block selection + start.col = max(limit_start, start.col); + end.col = min(limit_end, end.col); + + SelectionRange::new(start.into(), end.into(), span.is_block) }); // Load cursor glyph @@ -1108,10 +1055,14 @@ impl Term { Some(res) } - pub(crate) fn visible_to_buffer(&self, point: Point) -> Point { + pub fn visible_to_buffer(&self, point: Point) -> Point { self.grid.visible_to_buffer(point) } + pub fn buffer_to_visible(&self, point: impl Into>) -> Point { + self.grid.buffer_to_visible(point) + } + /// Convert the given pixel values to a grid coordinate /// /// The mouse coordinates are expected to be relative to the top left. The @@ -1368,9 +1319,79 @@ impl Term { pub fn clipboard(&mut self) -> &mut Clipboard { &mut self.clipboard } + + pub fn urls(&self) -> Vec { + let display_offset = self.grid.display_offset(); + let num_cols = self.grid.num_cols().0; + let last_col = Column(num_cols - 1); + let last_line = self.grid.num_lines() - 1; + + let grid_end_point = Point::new(0, last_col); + let mut iter = self.grid.iter_from(grid_end_point); + + let mut parser = Parser::new(); + let mut extra_url_len = 0; + let mut urls = Vec::new(); + + let mut c = Some(iter.cell()); + while let Some(cell) = c { + let point = iter.point(); + c = iter.prev(); + + // Skip double-width cell but extend URL length + if cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) { + extra_url_len += 1; + continue; + } + + // Interrupt URLs on line break + if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { + extra_url_len = 0; + parser.reset(); + } + + match parser.advance(cell.c) { + ParserState::Url(length) => { + urls.push(Url::new(point, length + extra_url_len, num_cols)) + }, + ParserState::NoUrl => { + extra_url_len = 0; + + // Stop searching for URLs once the viewport has been left without active URL + if point.line > last_line.0 + display_offset { + break; + } + }, + _ => (), + } + } + + urls + } + + pub fn url_to_string(&self, url: &Url) -> String { + let mut url_text = String::new(); + + let mut iter = self.grid.iter_from(url.start); + + let mut c = Some(iter.cell()); + while let Some(cell) = c { + if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) { + url_text.push(cell.c); + } + + if iter.point() == url.end { + break; + } + + c = iter.next(); + } + + url_text + } } -impl ansi::TermInfo for Term { +impl TermInfo for Term { #[inline] fn lines(&self) -> Line { self.grid.num_lines() diff --git a/alacritty_terminal/src/url.rs b/alacritty_terminal/src/url.rs index c3c7026..f1b7934 100644 --- a/alacritty_terminal/src/url.rs +++ b/alacritty_terminal/src/url.rs @@ -1,318 +1,41 @@ -// Copyright 2016 Joe Wilm, The Alacritty Project Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +use crate::ansi::TermInfo; +use crate::index::{Column, Linear, Point}; +use crate::term::Term; +use std::ops::RangeInclusive; -use unicode_width::UnicodeWidthChar; - -use crate::term::cell::{Cell, Flags}; - -// See https://tools.ietf.org/html/rfc3987#page-13 -const URL_SEPARATOR_CHARS: [char; 10] = ['<', '>', '"', ' ', '{', '}', '|', '\\', '^', '`']; -const URL_DENY_END_CHARS: [char; 7] = ['.', ',', ';', ':', '?', '!', '(']; -const URL_SCHEMES: [&str; 8] = - ["http://", "https://", "mailto:", "news:", "file://", "git://", "ssh://", "ftp://"]; - -/// URL text and origin of the original click position. -#[derive(Debug, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)] pub struct Url { - pub text: String, - pub origin: usize, + pub start: Point, + pub end: Point, } -/// Parser for streaming inside-out detection of URLs. -pub struct UrlParser { - state: String, - origin: usize, -} +impl Url { + pub fn new(start: Point, length: usize, num_cols: usize) -> Self { + let unwrapped_end_col = start.col.0 + length - 1; + let end_col = unwrapped_end_col % num_cols; + let end_line = start.line - unwrapped_end_col / num_cols; -impl UrlParser { - pub fn new() -> Self { - UrlParser { state: String::new(), origin: 0 } + Url { end: Point::new(end_line, Column(end_col)), start } } - /// Advance the parser one character to the left. - pub fn advance_left(&mut self, cell: &Cell) -> bool { - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - self.origin += 1; - return false; - } - - if self.advance(cell.c, 0) { - true - } else { - self.origin += 1; - false - } + pub fn contains(&self, point: impl Into>) -> bool { + let point = point.into(); + point.line <= self.start.line + && point.line >= self.end.line + && (point.line != self.start.line || point.col >= self.start.col) + && (point.line != self.end.line || point.col <= self.end.col) } - /// Advance the parser one character to the right. - pub fn advance_right(&mut self, cell: &Cell) -> bool { - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - return false; - } + pub fn linear_bounds(&self, terminal: &Term) -> RangeInclusive { + let mut start = self.start; + let mut end = self.end; - self.advance(cell.c, self.state.len()) - } + start = terminal.buffer_to_visible(start); + end = terminal.buffer_to_visible(end); - /// Returns the URL if the parser has found any. - pub fn url(mut self) -> Option { - // Remove non-alphabetical characters before the scheme - // https://tools.ietf.org/html/rfc3986#section-3.1 - if let Some(index) = self.state.find("://") { - let iter = - self.state.char_indices().rev().skip_while(|(byte_index, _)| *byte_index >= index); - for (byte_index, c) in iter { - match c { - 'a'..='z' | 'A'..='Z' => (), - _ => { - self.origin = - self.origin.saturating_sub(byte_index + c.width().unwrap_or(1)); - self.state = self.state.split_off(byte_index + c.len_utf8()); - break; - }, - } - } - } + let start = Linear::from_point(terminal.cols(), start); + let end = Linear::from_point(terminal.cols(), end); - // Remove non-matching parenthesis and brackets - let mut open_parens_count: isize = 0; - let mut open_bracks_count: isize = 0; - for (i, c) in self.state.char_indices() { - match c { - '(' => open_parens_count += 1, - ')' if open_parens_count > 0 => open_parens_count -= 1, - '[' => open_bracks_count += 1, - ']' if open_bracks_count > 0 => open_bracks_count -= 1, - ')' | ']' => { - self.state.truncate(i); - break; - }, - _ => (), - } - } - - // Track number of quotes - let mut num_quotes = self.state.chars().filter(|&c| c == '\'').count(); - - // Remove all characters which aren't allowed at the end of a URL - while !self.state.is_empty() - && (URL_DENY_END_CHARS.contains(&self.state.chars().last().unwrap()) - || (num_quotes % 2 != 0 && self.state.ends_with('\'')) - || self.state.ends_with("''") - || self.state.ends_with("()")) - { - if self.state.pop().unwrap() == '\'' { - num_quotes -= 1; - } - } - - // Check if string is valid url - if self.origin > 0 && url::Url::parse(&self.state).is_ok() { - for scheme in &URL_SCHEMES { - if self.state.starts_with(scheme) { - return Some(Url { origin: self.origin - 1, text: self.state }); - } - } - } - - None - } - - fn advance(&mut self, c: char, pos: usize) -> bool { - if URL_SEPARATOR_CHARS.contains(&c) - || (c >= '\u{00}' && c <= '\u{1F}') - || (c >= '\u{7F}' && c <= '\u{9F}') - { - true - } else { - self.state.insert(pos, c); - false - } - } -} - -#[cfg(test)] -mod tests { - use std::mem; - - use unicode_width::UnicodeWidthChar; - - use crate::clipboard::Clipboard; - use crate::grid::Grid; - use crate::index::{Column, Line, Point}; - use crate::message_bar::MessageBuffer; - use crate::term::cell::{Cell, Flags}; - use crate::term::{Search, SizeInfo, Term}; - - fn url_create_term(input: &str) -> Term { - let size = SizeInfo { - width: 21.0, - height: 51.0, - cell_width: 3.0, - cell_height: 3.0, - padding_x: 0.0, - padding_y: 0.0, - dpr: 1.0, - }; - - let width = input.chars().map(|c| if c.width() == Some(2) { 2 } else { 1 }).sum(); - let mut term = - Term::new(&Default::default(), size, MessageBuffer::new(), Clipboard::new_nop()); - let mut grid: Grid = Grid::new(Line(1), Column(width), 0, Cell::default()); - - let mut i = 0; - for c in input.chars() { - grid[Line(0)][Column(i)].c = c; - - if c.width() == Some(2) { - grid[Line(0)][Column(i)].flags.insert(Flags::WIDE_CHAR); - grid[Line(0)][Column(i + 1)].flags.insert(Flags::WIDE_CHAR_SPACER); - grid[Line(0)][Column(i + 1)].c = ' '; - i += 1; - } - - i += 1; - } - - mem::swap(term.grid_mut(), &mut grid); - - term - } - - fn url_test(input: &str, expected: &str) { - let term = url_create_term(input); - let url = term.url_search(Point::new(0, Column(15))); - assert_eq!(url.map(|u| u.text), Some(expected.into())); - } - - #[test] - fn url_skip_invalid() { - let term = url_create_term("no url here"); - let url = term.url_search(Point::new(0, Column(4))); - assert_eq!(url, None); - - let term = url_create_term(" https://example.org"); - let url = term.url_search(Point::new(0, Column(0))); - assert_eq!(url, None); - } - - #[test] - fn url_origin() { - let term = url_create_term(" test https://example.org "); - let url = term.url_search(Point::new(0, Column(10))); - assert_eq!(url.map(|u| u.origin), Some(4)); - - let term = url_create_term("https://example.org"); - let url = term.url_search(Point::new(0, Column(0))); - assert_eq!(url.map(|u| u.origin), Some(0)); - - let term = url_create_term("https://全.org"); - let url = term.url_search(Point::new(0, Column(10))); - assert_eq!(url.map(|u| u.origin), Some(10)); - - let term = url_create_term("https://全.org"); - let url = term.url_search(Point::new(0, Column(8))); - assert_eq!(url.map(|u| u.origin), Some(8)); - - let term = url_create_term("https://全.org"); - let url = term.url_search(Point::new(0, Column(9))); - assert_eq!(url.map(|u| u.origin), Some(9)); - - let term = url_create_term("test@https://example.org"); - let url = term.url_search(Point::new(0, Column(9))); - assert_eq!(url.map(|u| u.origin), Some(4)); - - let term = url_create_term("test全https://example.org"); - let url = term.url_search(Point::new(0, Column(9))); - assert_eq!(url.map(|u| u.origin), Some(3)); - } - - #[test] - fn url_matching_chars() { - url_test("(https://example.org/test(ing))", "https://example.org/test(ing)"); - url_test("https://example.org/test(ing)", "https://example.org/test(ing)"); - url_test("((https://example.org))", "https://example.org"); - url_test(")https://example.org(", "https://example.org"); - url_test("https://example.org)", "https://example.org"); - url_test("https://example.org(", "https://example.org"); - url_test("(https://one.org/)(https://two.org/)", "https://one.org/"); - - url_test("https://[2001:db8:a0b:12f0::1]:80", "https://[2001:db8:a0b:12f0::1]:80"); - url_test("([(https://example.org/test(ing))])", "https://example.org/test(ing)"); - url_test("https://example.org/]()", "https://example.org/"); - url_test("[https://example.org]", "https://example.org"); - - url_test("'https://example.org/test'ing'''", "https://example.org/test'ing'"); - url_test("https://example.org/test'ing'", "https://example.org/test'ing'"); - url_test("'https://example.org'", "https://example.org"); - url_test("'https://example.org", "https://example.org"); - url_test("https://example.org'", "https://example.org"); - - url_test("(https://example.org/test全)", "https://example.org/test全"); - } - - #[test] - fn url_detect_end() { - url_test("https://example.org/test\u{00}ing", "https://example.org/test"); - url_test("https://example.org/test\u{1F}ing", "https://example.org/test"); - url_test("https://example.org/test\u{7F}ing", "https://example.org/test"); - url_test("https://example.org/test\u{9F}ing", "https://example.org/test"); - url_test("https://example.org/test\ting", "https://example.org/test"); - url_test("https://example.org/test ing", "https://example.org/test"); - } - - #[test] - fn url_remove_end_chars() { - url_test("https://example.org/test?ing", "https://example.org/test?ing"); - url_test("https://example.org.,;:)'!/?", "https://example.org"); - url_test("https://example.org'.", "https://example.org"); - url_test("https://example.org/test/?;:", "https://example.org/test/"); - } - - #[test] - fn url_remove_start_chars() { - url_test("complicated:https://example.org", "https://example.org"); - url_test("test.https://example.org", "https://example.org"); - url_test(",https://example.org", "https://example.org"); - url_test("\u{2502}https://example.org", "https://example.org"); - } - - #[test] - fn url_unicode() { - url_test("https://xn--example-2b07f.org", "https://xn--example-2b07f.org"); - url_test("https://example.org/\u{2008A}", "https://example.org/\u{2008A}"); - url_test("https://example.org/\u{f17c}", "https://example.org/\u{f17c}"); - url_test("https://üñîçøðé.com/ä", "https://üñîçøðé.com/ä"); - } - - #[test] - fn url_schemes() { - url_test("mailto://example.org", "mailto://example.org"); - url_test("https://example.org", "https://example.org"); - url_test("http://example.org", "http://example.org"); - url_test("news://example.org", "news://example.org"); - url_test("file://example.org", "file://example.org"); - url_test("git://example.org", "git://example.org"); - url_test("ssh://example.org", "ssh://example.org"); - url_test("ftp://example.org", "ftp://example.org"); - - assert_eq!(url_create_term("mailto.example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("https:example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("http:example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("news.example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("file:example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("git:example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("ssh:example.org").url_search(Point::default()), None); - assert_eq!(url_create_term("ftp:example.org").url_search(Point::default()), None); + RangeInclusive::new(start, end) } }