Rework URL highlighting

This completely reworks URL highlighting to fix two issues which were
caused by the original approach.

The primary issues that were not straight-forward to resolve with the
previous implementation were about handling the URL highlighted content
moving while the highlight is active.

This lead to issues with highlighting with scrolling and when the
display offset was not 0.

The new approach sticks closely to prior art done for the selection,
where the selection is tracked on the grid and updated whenever the
buffer is rotated.

The truncation of URLs was incorrectly assuming input to be just a
single codepoint wide to truncate the end of URLs with unmatching
closing parenthesis. This is now handled properly using Rust's built-in
Unicode support.

This fixes #2231.
This fixes #2225.
This commit is contained in:
Christian Duerr 2019-03-30 09:23:48 +00:00 committed by GitHub
parent 28636923e0
commit 91aa683bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 216 deletions

View File

@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Parsing issues with URLs starting in the first or ending in the last column - Parsing issues with URLs starting in the first or ending in the last column
- URLs stopping at double-width characters - URLs stopping at double-width characters
- Fix `start_maximized` option on X11 - Fix `start_maximized` option on X11
- Error when parsing URLs ending with Unicode outside of the ascii range
## Version 0.2.9 ## Version 0.2.9

View File

@ -432,6 +432,7 @@ impl<N: Notify> Processor<N> {
processor.ctx.terminal.dirty = true; processor.ctx.terminal.dirty = true;
processor.ctx.terminal.next_is_urgent = Some(false); processor.ctx.terminal.next_is_urgent = Some(false);
} else { } else {
processor.ctx.terminal.reset_url_highlight();
processor.ctx.terminal.dirty = true; processor.ctx.terminal.dirty = true;
*hide_mouse = false; *hide_mouse = false;
} }

View File

@ -15,9 +15,9 @@
//! A specialized 2d grid implementation optimized for use in a terminal. //! A specialized 2d grid implementation optimized for use in a terminal.
use std::cmp::{min, max, Ordering}; use std::cmp::{min, max, Ordering};
use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull}; use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull, RangeInclusive};
use crate::index::{self, Point, Line, Column, IndexRange, PointIterator}; use crate::index::{self, Point, Line, Column, IndexRange};
use crate::selection::Selection; use crate::selection::Selection;
mod row; mod row;
@ -60,7 +60,8 @@ impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> {
self.lines.eq(&other.lines) && self.lines.eq(&other.lines) &&
self.display_offset.eq(&other.display_offset) && self.display_offset.eq(&other.display_offset) &&
self.scroll_limit.eq(&other.scroll_limit) && self.scroll_limit.eq(&other.scroll_limit) &&
self.selection.eq(&other.selection) self.selection.eq(&other.selection) &&
self.url_highlight.eq(&other.url_highlight)
} }
} }
@ -103,6 +104,10 @@ pub struct Grid<T> {
#[serde(default)] #[serde(default)]
max_scroll_limit: usize, max_scroll_limit: usize,
/// Range for URL hover highlights
#[serde(default)]
pub url_highlight: Option<RangeInclusive<index::Linear>>,
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -132,6 +137,7 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
scroll_limit: 0, scroll_limit: 0,
selection: None, selection: None,
max_scroll_limit: scrollback, max_scroll_limit: scrollback,
url_highlight: None,
} }
} }
@ -387,6 +393,7 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
let prev = self.lines; let prev = self.lines;
self.selection = None; self.selection = None;
self.url_highlight = None;
self.raw.rotate(*prev as isize - *target as isize); self.raw.rotate(*prev as isize - *target as isize);
self.raw.shrink_visible_lines(target); self.raw.shrink_visible_lines(target);
self.lines = target; self.lines = target;
@ -422,6 +429,7 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
if let Some(ref mut selection) = self.selection { if let Some(ref mut selection) = self.selection {
selection.rotate(-(*positions as isize)); selection.rotate(-(*positions as isize));
} }
self.url_highlight = None;
self.decrease_scroll_limit(*positions); self.decrease_scroll_limit(*positions);
@ -473,6 +481,7 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
if let Some(ref mut selection) = self.selection { if let Some(ref mut selection) = self.selection {
selection.rotate(*positions as isize); selection.rotate(*positions as isize);
} }
self.url_highlight = None;
// // This next loop swaps "fixed" lines outside of a scroll region // // This next loop swaps "fixed" lines outside of a scroll region
// // back into place after the rotation. The work is done in buffer- // // back into place after the rotation. The work is done in buffer-
@ -517,6 +526,7 @@ impl<T: GridCell + Copy + Clone> Grid<T> {
self.display_offset = 0; self.display_offset = 0;
self.selection = None; self.selection = None;
self.url_highlight = None;
} }
} }
@ -573,7 +583,7 @@ impl<T> Grid<T> {
pub fn iter_from(&self, point: Point<usize>) -> GridIterator<'_, T> { pub fn iter_from(&self, point: Point<usize>) -> GridIterator<'_, T> {
GridIterator { GridIterator {
grid: self, grid: self,
point_iter: point.iter(self.num_cols() - 1, self.len() - 1), cur: point,
} }
} }
@ -589,27 +599,50 @@ impl<T> Grid<T> {
} }
pub struct GridIterator<'a, T> { pub struct GridIterator<'a, T> {
point_iter: PointIterator<usize>, /// Immutable grid reference
grid: &'a Grid<T>, grid: &'a Grid<T>,
}
impl<'a, T> GridIterator<'a, T> { /// Current position of the iterator within the grid.
pub fn cur(&self) -> Point<usize> { pub cur: Point<usize>,
self.point_iter.cur
}
} }
impl<'a, T> Iterator for GridIterator<'a, T> { impl<'a, T> Iterator for GridIterator<'a, T> {
type Item = &'a T; type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.point_iter.next().map(|p| &self.grid[p.line][p.col]) let last_col = self.grid.num_cols() - Column(1);
match self.cur {
Point { line, col } if line == 0 && col == last_col => None,
Point { col, .. } if
(col == last_col) => {
self.cur.line -= 1;
self.cur.col = Column(0);
Some(&self.grid[self.cur.line][self.cur.col])
},
_ => {
self.cur.col += Column(1);
Some(&self.grid[self.cur.line][self.cur.col])
}
}
} }
} }
impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
fn prev(&mut self) -> Option<Self::Item> { fn prev(&mut self) -> Option<Self::Item> {
self.point_iter.prev().map(|p| &self.grid[p.line][p.col]) let num_cols = self.grid.num_cols();
match self.cur {
Point { line, col: Column(0) } if line == self.grid.len() - 1 => None,
Point { col: Column(0), .. } => {
self.cur.line += 1;
self.cur.col = num_cols - Column(1);
Some(&self.grid[self.cur.line][self.cur.col])
},
_ => {
self.cur.col -= Column(1);
Some(&self.grid[self.cur.line][self.cur.col])
}
}
} }
} }

View File

@ -112,8 +112,8 @@ fn test_iter() {
assert_eq!(None, iter.prev()); assert_eq!(None, iter.prev());
assert_eq!(Some(&1), iter.next()); assert_eq!(Some(&1), iter.next());
assert_eq!(Column(1), iter.cur().col); assert_eq!(Column(1), iter.cur.col);
assert_eq!(4, iter.cur().line); assert_eq!(4, iter.cur.line);
assert_eq!(Some(&2), iter.next()); assert_eq!(Some(&2), iter.next());
assert_eq!(Some(&3), iter.next()); assert_eq!(Some(&3), iter.next());
@ -121,12 +121,12 @@ fn test_iter() {
// test linewrapping // test linewrapping
assert_eq!(Some(&5), iter.next()); assert_eq!(Some(&5), iter.next());
assert_eq!(Column(0), iter.cur().col); assert_eq!(Column(0), iter.cur.col);
assert_eq!(3, iter.cur().line); assert_eq!(3, iter.cur.line);
assert_eq!(Some(&4), iter.prev()); assert_eq!(Some(&4), iter.prev());
assert_eq!(Column(4), iter.cur().col); assert_eq!(Column(4), iter.cur.col);
assert_eq!(4, iter.cur().line); assert_eq!(4, iter.cur.line);
// test that iter ends at end of grid // test that iter ends at end of grid

View File

@ -19,8 +19,6 @@ use std::cmp::{Ord, Ordering};
use std::fmt; use std::fmt;
use std::ops::{self, Deref, Range, RangeInclusive, Add, Sub, AddAssign, SubAssign}; use std::ops::{self, Deref, Range, RangeInclusive, Add, Sub, AddAssign, SubAssign};
use crate::grid::BidirectionalIterator;
/// The side of a cell /// The side of a cell
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Side { pub enum Side {
@ -72,67 +70,6 @@ impl From<Point> for Point<usize> {
} }
} }
impl<T> Point<T>
where
T: Copy + Default + SubAssign<usize> + PartialEq,
{
pub fn iter(&self, last_col: Column, last_line: T) -> PointIterator<T> {
PointIterator {
cur: *self,
last_col,
last_line,
}
}
}
pub struct PointIterator<T> {
pub cur: Point<T>,
last_col: Column,
last_line: T,
}
impl<T> Iterator for PointIterator<T>
where
T: Copy + Default + SubAssign<usize> + PartialEq,
{
type Item = Point<T>;
fn next(&mut self) -> Option<Self::Item> {
match self.cur {
Point { line, col } if line == Default::default() && col == self.last_col => None,
Point { col, .. } if col == self.last_col => {
self.cur.line -= 1;
self.cur.col = Column(0);
Some(self.cur)
},
_ => {
self.cur.col += Column(1);
Some(self.cur)
}
}
}
}
impl<T> BidirectionalIterator for PointIterator<T>
where
T: Copy + Default + AddAssign<usize> + SubAssign<usize> + PartialEq,
{
fn prev(&mut self) -> Option<Self::Item> {
match self.cur {
Point { line, col: Column(0) } if line == self.last_line => None,
Point { col: Column(0), .. } => {
self.cur.line += 1;
self.cur.col = self.last_col;
Some(self.cur)
},
_ => {
self.cur.col -= Column(1);
Some(self.cur)
}
}
}
}
/// A line /// A line
/// ///
/// Newtype to avoid passing values incorrectly /// Newtype to avoid passing values incorrectly
@ -163,6 +100,16 @@ impl fmt::Display for Column {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)]
pub struct Linear(pub usize); pub struct Linear(pub usize);
impl Linear {
pub fn new(columns: Column, column: Column, line: Line) -> Self {
Linear(line.0 * columns.0 + column.0)
}
pub fn from_point(columns: Column, point: Point<usize>) -> Self {
Linear(point.line * columns.0 + point.col.0)
}
}
impl fmt::Display for Linear { impl fmt::Display for Linear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Linear({})", self.0) write!(f, "Linear({})", self.0)

View File

@ -21,9 +21,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::mem; use std::mem;
use std::time::Instant; use std::time::Instant;
use std::iter::once; use std::ops::RangeInclusive;
use copypasta::{Clipboard, Load, Buffer as ClipboardBuffer}; use copypasta::{Clipboard, Load, Buffer as ClipboardBuffer};
use unicode_width::UnicodeWidthStr;
use glutin::{ use glutin::{
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta, ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta,
TouchPhase, TouchPhase,
@ -32,10 +33,9 @@ use glutin::{
use crate::config::{self, Key}; use crate::config::{self, Key};
use crate::grid::Scroll; use crate::grid::Scroll;
use crate::event::{ClickState, Mouse}; use crate::event::{ClickState, Mouse};
use crate::index::{Line, Column, Side, Point}; use crate::index::{Line, Column, Side, Point, Linear};
use crate::term::{Term, SizeInfo, Search, UrlHoverSaveState}; use crate::term::{Term, SizeInfo, Search};
use crate::term::mode::TermMode; use crate::term::mode::TermMode;
use crate::term::cell::Flags;
use crate::util::fmt::Red; use crate::util::fmt::Red;
use crate::util::start_daemon; use crate::util::start_daemon;
use crate::message_bar::{self, Message}; use crate::message_bar::{self, Message};
@ -448,45 +448,30 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
None None
}; };
if let Some(Url { origin, len, .. }) = url { if let Some(Url { origin, text }) = url {
let mouse_cursor = if self.ctx.terminal().mode().intersects(mouse_mode) {
MouseCursor::Default
} else {
MouseCursor::Text
};
let cols = self.ctx.size_info().cols().0; let cols = self.ctx.size_info().cols().0;
let last_line = self.ctx.size_info().lines().0 - 1;
// Calculate the URL's start position // Calculate the URL's start position
let col = (cols + point.col.0 - origin % cols) % cols; let lines_before = (origin + cols - point.col.0 - 1) / cols;
let line = last_line - point.line.0 + (origin + cols - point.col.0 - 1) / cols; let (start_col, start_line) = if lines_before > point.line.0 {
let start = Point::new(line, Column(col)); (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));
// Update URLs only on change, so they don't all get marked as underlined // Calculate the URL's end position
if self.ctx.terminal().url_highlight_start() == Some(start) { let len = text.width();
return; let end_col = (point.col.0 + len - origin) % cols - 1;
} let end_line = point.line.0 + (point.col.0 + len - origin) / cols;
let end = Point::new(end_line, Column(end_col));
// Since the URL changed without reset, we need to clear the previous underline let start = Linear::from_point(Column(cols), start);
self.ctx.terminal_mut().reset_url_highlight(); let end = Linear::from_point(Column(cols), end);
// Underline all cells and store their current underline state
let mut underlined = Vec::with_capacity(len);
let iter = once(start).chain(start.iter(Column(cols - 1), last_line));
for point in iter.take(len) {
let cell = &mut self.ctx.terminal_mut().grid_mut()[point.line][point.col];
underlined.push(cell.flags.contains(Flags::UNDERLINE));
cell.flags.insert(Flags::UNDERLINE);
}
// Save the higlight state for restoring it again
self.ctx.terminal_mut().set_url_highlight(UrlHoverSaveState {
mouse_cursor,
underlined,
start,
});
self.ctx.terminal_mut().set_url_highlight(RangeInclusive::new(start, end));
self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand); self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand);
self.ctx.terminal_mut().dirty = true; self.ctx.terminal_mut().dirty = true;
} else { } else {

View File

@ -17,7 +17,6 @@ use std::ops::{Range, Index, IndexMut, RangeInclusive};
use std::{ptr, io, mem}; use std::{ptr, io, mem};
use std::cmp::{min, max}; use std::cmp::{min, max};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::iter::once;
use arraydeque::ArrayDeque; use arraydeque::ArrayDeque;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
@ -27,7 +26,7 @@ use font::{self, Size};
use crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; use crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle};
use crate::grid::{ use crate::grid::{
BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll,
ViewportPosition, ViewportPosition
}; };
use crate::index::{self, Point, Column, Line, IndexRange, Contains, Linear}; use crate::index::{self, Point, Column, Line, IndexRange, Contains, Linear};
use crate::selection::{self, Selection, Locations}; use crate::selection::{self, Selection, Locations};
@ -71,11 +70,11 @@ impl Search for Term {
break; break;
} }
if iter.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char break; // cut off if on new line or hit escape char
} }
point = iter.cur(); point = iter.cur;
} }
point point
@ -93,7 +92,7 @@ impl Search for Term {
break; break;
} }
point = iter.cur(); point = iter.cur;
if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char break; // cut off if on new line or hit escape char
@ -120,7 +119,7 @@ impl Search for Term {
// Find URLs // Find URLs
let mut url_parser = UrlParser::new(); let mut url_parser = UrlParser::new();
while let Some(cell) = iterb.prev() { while let Some(cell) = iterb.prev() {
if (iterb.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) if (iterb.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE))
|| url_parser.advance_left(cell) || url_parser.advance_left(cell)
{ {
break; break;
@ -129,7 +128,7 @@ impl Search for Term {
while let Some(cell) = iterf.next() { while let Some(cell) = iterf.next() {
if url_parser.advance_right(cell) if url_parser.advance_right(cell)
|| (iterf.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) || (iterf.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE))
{ {
break; break;
} }
@ -164,6 +163,7 @@ pub struct RenderableCellsIter<'a> {
config: &'a Config, config: &'a Config,
colors: &'a color::List, colors: &'a color::List,
selection: Option<RangeInclusive<index::Linear>>, selection: Option<RangeInclusive<index::Linear>>,
url_highlight: &'a Option<RangeInclusive<index::Linear>>,
cursor_cells: ArrayDeque<[Indexed<Cell>; 3]>, cursor_cells: ArrayDeque<[Indexed<Cell>; 3]>,
} }
@ -173,15 +173,14 @@ impl<'a> RenderableCellsIter<'a> {
/// The cursor and terminal mode are required for properly displaying the /// The cursor and terminal mode are required for properly displaying the
/// cursor. /// cursor.
fn new<'b>( fn new<'b>(
grid: &'b Grid<Cell>, term: &'b Term,
cursor: &'b Point,
colors: &'b color::List,
mode: TermMode,
config: &'b Config, config: &'b Config,
selection: Option<Locations>, selection: Option<Locations>,
cursor_style: CursorStyle, cursor_style: CursorStyle,
) -> RenderableCellsIter<'b> { ) -> RenderableCellsIter<'b> {
let cursor_offset = grid.line_to_offset(cursor.line); let grid = &term.grid;
let cursor_offset = grid.line_to_offset(term.cursor.point.line);
let inner = grid.display_iter(); let inner = grid.display_iter();
let mut selection_range = None; let mut selection_range = None;
@ -224,8 +223,8 @@ impl<'a> RenderableCellsIter<'a> {
} }
let cols = grid.num_cols(); let cols = grid.num_cols();
let start = Linear(start.line.0 * cols.0 + start.col.0); let start = Linear::from_point(cols, start.into());
let end = Linear(end.line.0 * cols.0 + end.col.0); let end = Linear::from_point(cols, end.into());
// Update the selection // Update the selection
selection_range = Some(RangeInclusive::new(start, end)); selection_range = Some(RangeInclusive::new(start, end));
@ -233,14 +232,15 @@ impl<'a> RenderableCellsIter<'a> {
} }
RenderableCellsIter { RenderableCellsIter {
cursor, cursor: &term.cursor.point,
cursor_offset, cursor_offset,
grid, grid,
inner, inner,
mode, mode: term.mode,
selection: selection_range, selection: selection_range,
url_highlight: &grid.url_highlight,
config, config,
colors, colors: &term.colors,
cursor_cells: ArrayDeque::new(), cursor_cells: ArrayDeque::new(),
}.initialize(cursor_style) }.initialize(cursor_style)
} }
@ -435,7 +435,7 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
loop { loop {
// Handle cursor // Handle cursor
let (cell, selected) = if self.cursor_offset == self.inner.offset() && let (cell, selected, highlighted) = if self.cursor_offset == self.inner.offset() &&
self.inner.column() == self.cursor.col self.inner.column() == self.cursor.col
{ {
// Cursor cell // Cursor cell
@ -448,11 +448,11 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
if self.cursor_cells.is_empty() { if self.cursor_cells.is_empty() {
self.inner.next(); self.inner.next();
} }
(cell, false) (cell, false, false)
} else { } else {
let cell = self.inner.next()?; let cell = self.inner.next()?;
let index = Linear(cell.line.0 * self.grid.num_cols().0 + cell.column.0); let index = Linear::new(self.grid.num_cols(), cell.column, cell.line);
let selected = self.selection.as_ref() let selected = self.selection.as_ref()
.map(|range| range.contains_(index)) .map(|range| range.contains_(index))
@ -463,7 +463,12 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
continue; continue;
} }
(cell, selected) // Underline URL highlights
let highlighted = self.url_highlight.as_ref()
.map(|range| range.contains_(index))
.unwrap_or(false);
(cell, selected, highlighted)
}; };
// Lookup RGB values // Lookup RGB values
@ -488,14 +493,19 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
fg_rgb = col; fg_rgb = col;
} }
let mut flags = cell.flags;
if highlighted {
flags.insert(Flags::UNDERLINE);
}
return Some(RenderableCell { return Some(RenderableCell {
line: cell.line, line: cell.line,
column: cell.column, column: cell.column,
flags: cell.flags,
chars: cell.chars(), chars: cell.chars(),
fg: fg_rgb, fg: fg_rgb,
bg: bg_rgb, bg: bg_rgb,
bg_alpha, bg_alpha,
flags,
}) })
} }
} }
@ -824,16 +834,6 @@ pub struct Term {
/// Hint that Alacritty should be closed /// Hint that Alacritty should be closed
should_exit: bool, should_exit: bool,
/// URL highlight save state
url_hover_save: Option<UrlHoverSaveState>,
}
/// Temporary save state for restoring mouse cursor and underline after unhovering a URL.
pub struct UrlHoverSaveState {
pub mouse_cursor: MouseCursor,
pub underlined: Vec<bool>,
pub start: Point<usize>,
} }
/// Terminal size info /// Terminal size info
@ -910,8 +910,10 @@ impl Term {
self.next_title.take() self.next_title.take()
} }
#[inline]
pub fn scroll_display(&mut self, scroll: Scroll) { pub fn scroll_display(&mut self, scroll: Scroll) {
self.grid.scroll_display(scroll); self.grid.scroll_display(scroll);
self.reset_url_highlight();
self.dirty = true; self.dirty = true;
} }
@ -966,7 +968,6 @@ impl Term {
auto_scroll: config.scrolling().auto_scroll, auto_scroll: config.scrolling().auto_scroll,
message_buffer, message_buffer,
should_exit: false, should_exit: false,
url_hover_save: None,
} }
} }
@ -1162,7 +1163,8 @@ impl Term {
&self.grid &self.grid
} }
/// Mutable access to the raw grid data structure /// Mutable access for swapping out the grid during tests
#[cfg(test)]
pub fn grid_mut(&mut self) -> &mut Grid<Cell> { pub fn grid_mut(&mut self) -> &mut Grid<Cell> {
&mut self.grid &mut self.grid
} }
@ -1191,10 +1193,7 @@ impl Term {
}; };
RenderableCellsIter::new( RenderableCellsIter::new(
&self.grid, &self,
&self.cursor.point,
&self.colors,
self.mode,
config, config,
selection, selection,
cursor, cursor,
@ -1212,8 +1211,6 @@ impl Term {
return; return;
} }
self.reset_url_highlight();
let old_cols = self.grid.num_cols(); let old_cols = self.grid.num_cols();
let old_lines = self.grid.num_lines(); let old_lines = self.grid.num_lines();
let mut num_cols = size.cols(); let mut num_cols = size.cols();
@ -1232,6 +1229,7 @@ impl Term {
self.grid.selection = None; self.grid.selection = None;
self.alt_grid.selection = None; self.alt_grid.selection = None;
self.grid.url_highlight = None;
// Should not allow less than 1 col, causes all sorts of checks to be required. // Should not allow less than 1 col, causes all sorts of checks to be required.
if num_cols <= Column(1) { if num_cols <= Column(1) {
@ -1381,37 +1379,22 @@ impl Term {
} }
#[inline] #[inline]
pub fn set_url_highlight(&mut self, hover_save: UrlHoverSaveState) { pub fn set_url_highlight(&mut self, hl: RangeInclusive<index::Linear>) {
self.url_hover_save = Some(hover_save); self.grid.url_highlight = Some(hl);
} }
#[inline] #[inline]
pub fn url_highlight_start(&self) -> Option<Point<usize>> {
self.url_hover_save.as_ref().map(|hs| hs.start)
}
/// Remove the URL highlight and restore the previous mouse cursor and underline state.
pub fn reset_url_highlight(&mut self) { pub fn reset_url_highlight(&mut self) {
let hover_save = match self.url_hover_save.take() { let mouse_mode =
Some(hover_save) => hover_save, TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK;
_ => return, let mouse_cursor = if self.mode().intersects(mouse_mode) {
MouseCursor::Default
} else {
MouseCursor::Text
}; };
self.set_mouse_cursor(mouse_cursor);
// Reset the mouse cursor self.grid.url_highlight = None;
self.set_mouse_cursor(hover_save.mouse_cursor);
let last_line = self.size_info.lines().0 - 1;
let last_col = self.size_info.cols() - 1;
// Reset the underline state after unhovering a URL
let mut iter = once(hover_save.start).chain(hover_save.start.iter(last_col, last_line));
for underlined in &hover_save.underlined {
if let (Some(point), false) = (iter.next(), underlined) {
let cell = &mut self.grid[point.line][point.col];
cell.flags.remove(Flags::UNDERLINE);
}
}
self.dirty = true; self.dirty = true;
} }
} }
@ -1961,8 +1944,9 @@ impl ansi::Handler for Term {
let mut template = self.cursor.template; let mut template = self.cursor.template;
template.flags ^= template.flags; template.flags ^= template.flags;
// Remove active selections // Remove active selections and URL highlights
self.grid.selection = None; self.grid.selection = None;
self.grid.url_highlight = None;
match mode { match mode {
ansi::ClearMode::Below => { ansi::ClearMode::Below => {

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
use url; use url;
use unicode_width::UnicodeWidthChar;
use crate::term::cell::{Cell, Flags}; use crate::term::cell::{Cell, Flags};
@ -28,14 +29,12 @@ const URL_SCHEMES: [&str; 8] = [
pub struct Url { pub struct Url {
pub text: String, pub text: String,
pub origin: usize, pub origin: usize,
pub len: usize,
} }
/// Parser for streaming inside-out detection of URLs. /// Parser for streaming inside-out detection of URLs.
pub struct UrlParser { pub struct UrlParser {
state: String, state: String,
origin: usize, origin: usize,
len: usize,
} }
impl UrlParser { impl UrlParser {
@ -43,7 +42,6 @@ impl UrlParser {
UrlParser { UrlParser {
state: String::new(), state: String::new(),
origin: 0, origin: 0,
len: 0,
} }
} }
@ -51,7 +49,6 @@ impl UrlParser {
pub fn advance_left(&mut self, cell: &Cell) -> bool { pub fn advance_left(&mut self, cell: &Cell) -> bool {
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
self.origin += 1; self.origin += 1;
self.len += 1;
return false; return false;
} }
@ -59,7 +56,6 @@ impl UrlParser {
true true
} else { } else {
self.origin += 1; self.origin += 1;
self.len += 1;
false false
} }
} }
@ -67,16 +63,10 @@ impl UrlParser {
/// Advance the parser one character to the right. /// Advance the parser one character to the right.
pub fn advance_right(&mut self, cell: &Cell) -> bool { pub fn advance_right(&mut self, cell: &Cell) -> bool {
if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
self.len += 1;
return false; return false;
} }
if self.advance(cell.c, self.state.len()) { self.advance(cell.c, self.state.len())
true
} else {
self.len += 1;
false
}
} }
/// Returns the URL if the parser has found any. /// Returns the URL if the parser has found any.
@ -93,7 +83,7 @@ impl UrlParser {
match c { match c {
'a'...'z' | 'A'...'Z' => (), 'a'...'z' | 'A'...'Z' => (),
_ => { _ => {
self.origin = self.origin.saturating_sub(byte_index + 1); self.origin = self.origin.saturating_sub(byte_index + c.width().unwrap_or(1));
self.state = self.state.split_off(byte_index + c.len_utf8()); self.state = self.state.split_off(byte_index + c.len_utf8());
break; break;
} }
@ -104,7 +94,7 @@ impl UrlParser {
// Remove non-matching parenthesis and brackets // Remove non-matching parenthesis and brackets
let mut open_parens_count: isize = 0; let mut open_parens_count: isize = 0;
let mut open_bracks_count: isize = 0; let mut open_bracks_count: isize = 0;
for (i, c) in self.state.chars().enumerate() { for (i, c) in self.state.char_indices() {
match c { match c {
'(' => open_parens_count += 1, '(' => open_parens_count += 1,
')' if open_parens_count > 0 => open_parens_count -= 1, ')' if open_parens_count > 0 => open_parens_count -= 1,
@ -140,7 +130,6 @@ impl UrlParser {
Some(Url { Some(Url {
origin: self.origin - 1, origin: self.origin - 1,
text: self.state, text: self.state,
len: self.len,
}) })
} else { } else {
None None
@ -247,25 +236,14 @@ mod tests {
let term = url_create_term("https://全.org"); let term = url_create_term("https://全.org");
let url = term.url_search(Point::new(0, Column(9))); let url = term.url_search(Point::new(0, Column(9)));
assert_eq!(url.map(|u| u.origin), Some(9)); assert_eq!(url.map(|u| u.origin), Some(9));
}
#[test] let term = url_create_term("test@https://example.org");
fn url_len() {
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.len), Some(19));
let term = url_create_term("https://全.org");
let url = term.url_search(Point::new(0, Column(0)));
assert_eq!(url.map(|u| u.len), Some(14));
let term = url_create_term("https://全.org");
let url = term.url_search(Point::new(0, Column(10)));
assert_eq!(url.map(|u| u.len), Some(14));
let term = url_create_term("https://全.org");
let url = term.url_search(Point::new(0, Column(9))); let url = term.url_search(Point::new(0, Column(9)));
assert_eq!(url.map(|u| u.len), Some(14)); 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] #[test]
@ -288,6 +266,8 @@ mod tests {
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://example.org'", "https://example.org"); url_test("https://example.org'", "https://example.org");
url_test("(https://example.org/test全)", "https://example.org/test全");
} }
#[test] #[test]