blob: f972099d9e30c0653816ab7d27c25ad1c9da1eeb [file] [log] [blame]
use std::cmp::min;
use std::mem;
use crossfont::Metrics;
use glutin::event::{ElementState, ModifiersState};
use urlocator::{UrlLocation, UrlLocator};
use alacritty_terminal::index::{Column, Point};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::{RenderableCell, RenderableCellContent, SizeInfo};
use crate::config::Config;
use crate::event::Mouse;
use crate::renderer::rects::{RenderLine, RenderRect};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Url {
lines: Vec<RenderLine>,
end_offset: u16,
num_cols: Column,
}
impl Url {
pub fn rects(&self, metrics: &Metrics, size: &SizeInfo) -> Vec<RenderRect> {
let end = self.end();
self.lines
.iter()
.filter(|line| line.start <= end)
.map(|line| {
let mut rect_line = *line;
rect_line.end = min(line.end, end);
rect_line.rects(Flags::UNDERLINE, metrics, size)
})
.flatten()
.collect()
}
pub fn start(&self) -> Point {
self.lines[0].start
}
pub fn end(&self) -> Point {
self.lines[self.lines.len() - 1].end.sub(self.num_cols, self.end_offset as usize)
}
}
pub struct Urls {
locator: UrlLocator,
urls: Vec<Url>,
scheme_buffer: Vec<RenderableCell>,
last_point: Option<Point>,
state: UrlLocation,
}
impl Default for Urls {
fn default() -> Self {
Self {
locator: UrlLocator::new(),
scheme_buffer: Vec::new(),
urls: Vec::new(),
state: UrlLocation::Reset,
last_point: None,
}
}
}
impl Urls {
pub fn new() -> Self {
Self::default()
}
// Update tracked URLs.
pub fn update(&mut self, num_cols: Column, cell: RenderableCell) {
// Convert cell to character.
let c = match cell.inner {
RenderableCellContent::Chars(chars) => chars[0],
RenderableCellContent::Cursor(_) => return,
};
let point: Point = cell.into();
let end = point;
// Reset URL when empty cells have been skipped.
if point != Point::default() && Some(point.sub(num_cols, 1)) != self.last_point {
self.reset();
}
self.last_point = Some(end);
// Extend current state if a wide char spacer is encountered.
if cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) {
if let UrlLocation::Url(_, mut end_offset) = self.state {
if end_offset != 0 {
end_offset += 1;
}
self.extend_url(point, end, cell.fg, end_offset);
}
return;
}
// Advance parser.
let last_state = mem::replace(&mut self.state, self.locator.advance(c));
match (self.state, last_state) {
(UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => {
// Create empty URL.
self.urls.push(Url { lines: Vec::new(), end_offset, num_cols });
// Push schemes into URL.
for scheme_cell in self.scheme_buffer.split_off(0) {
let point = scheme_cell.into();
self.extend_url(point, point, scheme_cell.fg, end_offset);
}
// Push the new cell into URL.
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => {
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Scheme, _) => self.scheme_buffer.push(cell),
(UrlLocation::Reset, _) => self.reset(),
_ => (),
}
// Reset at un-wrapped linebreak.
if cell.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) {
self.reset();
}
}
/// Extend the last URL.
fn extend_url(&mut self, start: Point, end: Point, color: Rgb, end_offset: u16) {
let url = self.urls.last_mut().unwrap();
// If color changed, we need to insert a new line.
if url.lines.last().map(|last| last.color) == Some(color) {
url.lines.last_mut().unwrap().end = end;
} else {
url.lines.push(RenderLine { color, start, end });
}
// Update excluded cells at the end of the URL.
url.end_offset = end_offset;
}
/// Find URL below the mouse cursor.
pub fn highlighted(
&self,
config: &Config,
mouse: &Mouse,
mods: ModifiersState,
mouse_mode: bool,
selection: bool,
) -> Option<Url> {
// Require additional shift in mouse mode.
let mut required_mods = config.ui_config.mouse.url.mods();
if mouse_mode {
required_mods |= ModifiersState::SHIFT;
}
// Make sure all prerequisites for highlighting are met.
if selection
|| !mouse.inside_grid
|| config.ui_config.mouse.url.launcher.is_none()
|| required_mods != mods
|| mouse.left_button_state == ElementState::Pressed
{
return None;
}
self.find_at(Point::new(mouse.line, mouse.column))
}
/// Find URL at location.
pub fn find_at(&self, point: Point) -> Option<Url> {
for url in &self.urls {
if (url.start()..=url.end()).contains(&point) {
return Some(url.clone());
}
}
None
}
fn reset(&mut self) {
self.locator = UrlLocator::new();
self.state = UrlLocation::Reset;
self.scheme_buffer.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use alacritty_terminal::index::{Column, Line};
use alacritty_terminal::term::cell::MAX_ZEROWIDTH_CHARS;
fn text_to_cells(text: &str) -> Vec<RenderableCell> {
text.chars()
.enumerate()
.map(|(i, c)| RenderableCell {
inner: RenderableCellContent::Chars([c; MAX_ZEROWIDTH_CHARS + 1]),
line: Line(0),
column: Column(i),
fg: Default::default(),
bg: Default::default(),
bg_alpha: 0.,
flags: Flags::empty(),
})
.collect()
}
#[test]
fn multi_color_url() {
let mut input = text_to_cells("test https://example.org ing");
let num_cols = input.len();
input[10].fg = Rgb { r: 0xff, g: 0x00, b: 0xff };
let mut urls = Urls::new();
for cell in input {
urls.update(Column(num_cols), cell);
}
let url = urls.urls.first().unwrap();
assert_eq!(url.start().col, Column(5));
assert_eq!(url.end().col, Column(23));
}
#[test]
fn multiple_urls() {
let input = text_to_cells("test git:a git:b git:c ing");
let num_cols = input.len();
let mut urls = Urls::new();
for cell in input {
urls.update(Column(num_cols), cell);
}
assert_eq!(urls.urls.len(), 3);
assert_eq!(urls.urls[0].start().col, Column(5));
assert_eq!(urls.urls[0].end().col, Column(9));
assert_eq!(urls.urls[1].start().col, Column(11));
assert_eq!(urls.urls[1].end().col, Column(15));
assert_eq!(urls.urls[2].start().col, Column(17));
assert_eq!(urls.urls[2].end().col, Column(21));
}
}