Switch to rfind_url for URL detection

This switches to rfind_url for detecting URLs inside the grid. Instead
of expanding at the cursor position, the complete terminal is searched
from the bottom until the visible region is left with no active URL.

Instead of having the field `cur` publicly accessibly on the
`DisplayIterator`, there are the two methods `DisplayIterator::point`
and `DisplayIterator::cell` for accessing the current element of the
iterator now. This allows accessing the current element right after
creating the iterator.

Fixes #2629.
Fixes #2627.
This commit is contained in:
Christian Duerr 2019-08-01 15:37:01 +00:00 committed by GitHub
parent f51c7b067a
commit 9dddf649a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 453 deletions

View File

@ -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

21
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -120,7 +120,7 @@ pub enum Scroll {
}
#[derive(Copy, Clone)]
pub enum ViewportPosition {
enum ViewportPosition {
Visible(Line),
Above,
Below,
@ -141,11 +141,25 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
}
}
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
Point { line: self.visible_line_to_buffer(point.line), col: point.col }
pub fn buffer_to_visible(&self, point: impl Into<Point<usize>>) -> Point<usize> {
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<T: GridCell + Copy + Clone> Grid<T> {
}
}
pub fn visible_line_to_buffer(&self, line: Line) -> usize {
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
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<T>,
/// Current position of the iterator within the grid.
pub cur: Point<usize>,
cur: Point<usize>,
}
impl<'a, T> GridIterator<'a, T> {
pub fn point(&self) -> Point<usize> {
self.cur
}
pub fn cell(&self) -> &'a T {
&self.grid[self.cur.line][self.cur.col]
}
}
impl<'a, T> Iterator for GridIterator<'a, T> {

View File

@ -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) });

View File

@ -59,6 +59,12 @@ impl From<Point<usize>> for Point<isize> {
}
}
impl From<Point<usize>> for Point<Line> {
fn from(point: Point<usize>) -> Self {
Point::new(Line(point.line), point.col)
}
}
impl From<Point<isize>> for Point<usize> {
fn from(point: Point<isize>) -> Self {
Point::new(point.line as usize, point.col)

View File

@ -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<Linear> {
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();

View File

@ -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<usize>) -> Point<usize>;
/// Find the nearest semantic boundary _to the point_ of provided point.
fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>;
/// Find the nearest URL boundary in both directions.
fn url_search(&self, _: Point<usize>) -> Option<Url>;
/// Find the nearest matching bracket.
fn bracket_search(&self, _: Point<usize>) -> Option<Point<usize>>;
}
@ -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<usize>) -> Option<Url> {
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<usize>) -> Option<Point<usize>> {
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<usize> {
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
self.grid.visible_to_buffer(point)
}
pub fn buffer_to_visible(&self, point: impl Into<Point<usize>>) -> Point<usize> {
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<Url> {
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()

View File

@ -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<usize>,
pub end: Point<usize>,
}
/// Parser for streaming inside-out detection of URLs.
pub struct UrlParser {
state: String,
origin: usize,
}
impl Url {
pub fn new(start: Point<usize>, 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<Point<usize>>) -> 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<Linear> {
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<Url> {
// 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<Cell> = 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)
}
}