aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/display')
-rw-r--r--alacritty/src/display/content.rs15
-rw-r--r--alacritty/src/display/hint.rs179
-rw-r--r--alacritty/src/display/mod.rs122
3 files changed, 263 insertions, 53 deletions
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs
index e356f1f3..23a2f8a4 100644
--- a/alacritty/src/display/content.rs
+++ b/alacritty/src/display/content.rs
@@ -18,15 +18,12 @@ use alacritty_terminal::term::{
use crate::config::ui_config::UiConfig;
use crate::display::color::{List, DIM_FACTOR};
use crate::display::hint::HintState;
-use crate::display::Display;
+use crate::display::{self, Display, MAX_SEARCH_LINES};
use crate::event::SearchState;
/// Minimum contrast between a fixed cursor color and the cell's background.
pub const MIN_CURSOR_CONTRAST: f64 = 1.5;
-/// Maximum number of linewraps followed outside of the viewport during search highlighting.
-const MAX_SEARCH_LINES: usize = 100;
-
/// Renderable terminal content.
///
/// This provides the terminal cursor and an iterator over all non-empty cells.
@@ -138,8 +135,8 @@ impl<'a> RenderableContent<'a> {
// Convert cursor point to viewport position.
let cursor_point = self.terminal_cursor.point;
- let line = (cursor_point.line + self.terminal_content.display_offset as i32).0 as usize;
- let point = Point::new(line, cursor_point.column);
+ let display_offset = self.terminal_content.display_offset;
+ let point = display::point_to_viewport(display_offset, cursor_point).unwrap();
Some(RenderableCursor {
shape: self.terminal_cursor.shape,
@@ -258,8 +255,8 @@ impl RenderableCell {
// Convert cell point to viewport position.
let cell_point = cell.point;
- let line = (cell_point.line + content.terminal_content.display_offset as i32).0 as usize;
- let point = Point::new(line, cell_point.column);
+ let display_offset = content.terminal_content.display_offset;
+ let point = display::point_to_viewport(display_offset, cell_point).unwrap();
RenderableCell {
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
@@ -441,7 +438,7 @@ impl<'a> From<&'a HintState> for Hint<'a> {
/// Wrapper for finding visible regex matches.
#[derive(Default, Clone)]
-pub struct RegexMatches(Vec<RangeInclusive<Point>>);
+pub struct RegexMatches(pub Vec<RangeInclusive<Point>>);
impl RegexMatches {
/// Find all visible matches.
diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs
index 2a5e9c65..f9ab90d4 100644
--- a/alacritty/src/display/hint.rs
+++ b/alacritty/src/display/hint.rs
@@ -1,8 +1,16 @@
-use alacritty_terminal::term::search::Match;
+use std::cmp::{max, min};
+
+use glutin::event::ModifiersState;
+
+use alacritty_terminal::grid::BidirectionalIterator;
+use alacritty_terminal::index::{Boundary, Point};
+use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::Term;
use crate::config::ui_config::{Hint, HintAction};
+use crate::config::Config;
use crate::display::content::RegexMatches;
+use crate::display::MAX_SEARCH_LINES;
/// Percentage of characters in the hints alphabet used for the last character.
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
@@ -63,7 +71,20 @@ impl HintState {
};
// Find visible matches.
- self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex));
+ self.matches.0 = hint.regex.with_compiled(|regex| {
+ let mut matches = RegexMatches::new(term, regex);
+
+ // Apply post-processing and search for sub-matches if necessary.
+ if hint.post_processing {
+ matches
+ .drain(..)
+ .map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
+ .flatten()
+ .collect()
+ } else {
+ matches.0
+ }
+ });
// Cancel highlight with no visible matches.
if self.matches.is_empty() {
@@ -144,6 +165,7 @@ impl HintState {
}
/// Hint match which was selected by the user.
+#[derive(Clone)]
pub struct HintMatch {
/// Action for handling the text.
pub action: HintAction,
@@ -217,6 +239,159 @@ impl HintLabels {
}
}
+/// Check if there is a hint highlighted at the specified point.
+pub fn highlighted_at<T>(
+ term: &Term<T>,
+ config: &Config,
+ point: Point,
+ mouse_mods: ModifiersState,
+) -> Option<HintMatch> {
+ config.ui_config.hints.enabled.iter().find_map(|hint| {
+ // Check if all required modifiers are pressed.
+ if hint.mouse.map_or(true, |mouse| !mouse.enabled || !mouse_mods.contains(mouse.mods.0)) {
+ return None;
+ }
+
+ hint.regex.with_compiled(|regex| {
+ // Setup search boundaries.
+ let mut start = term.line_search_left(point);
+ start.line = max(start.line, point.line - MAX_SEARCH_LINES);
+ let mut end = term.line_search_right(point);
+ end.line = min(end.line, point.line + MAX_SEARCH_LINES);
+
+ // Function to verify if the specified point is inside the match.
+ let at_point = |rm: &Match| *rm.start() <= point && *rm.end() >= point;
+
+ // Check if there's any match at the specified point.
+ let regex_match = term.regex_search_right(regex, start, end).filter(at_point)?;
+
+ // Apply post-processing and search for sub-matches if necessary.
+ let regex_match = if hint.post_processing {
+ HintPostProcessor::new(term, regex, regex_match).find(at_point)
+ } else {
+ Some(regex_match)
+ };
+
+ regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
+ })
+ })
+}
+
+/// Iterator over all post-processed matches inside an existing hint match.
+struct HintPostProcessor<'a, T> {
+ /// Regex search DFAs.
+ regex: &'a RegexSearch,
+
+ /// Terminal reference.
+ term: &'a Term<T>,
+
+ /// Next hint match in the iterator.
+ next_match: Option<Match>,
+
+ /// Start point for the next search.
+ start: Point,
+
+ /// End point for the hint match iterator.
+ end: Point,
+}
+
+impl<'a, T> HintPostProcessor<'a, T> {
+ /// Create a new iterator for an unprocessed match.
+ fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self {
+ let end = *regex_match.end();
+ let mut post_processor = Self { next_match: None, start: end, end, term, regex };
+
+ // Post-process the first hint match.
+ let next_match = post_processor.hint_post_processing(&regex_match);
+ post_processor.start = next_match.end().add(term, Boundary::Grid, 1);
+ post_processor.next_match = Some(next_match);
+
+ post_processor
+ }
+
+ /// Apply some hint post processing heuristics.
+ ///
+ /// This will check the end of the hint and make it shorter if certain characters are determined
+ /// to be unlikely to be intentionally part of the hint.
+ ///
+ /// This is most useful for identifying URLs appropriately.
+ fn hint_post_processing(&self, regex_match: &Match) -> Match {
+ let mut iter = self.term.grid().iter_from(*regex_match.start());
+
+ let mut c = iter.cell().c;
+
+ // Truncate uneven number of brackets.
+ let end = *regex_match.end();
+ let mut open_parents = 0;
+ let mut open_brackets = 0;
+ loop {
+ match c {
+ '(' => open_parents += 1,
+ '[' => open_brackets += 1,
+ ')' => {
+ if open_parents == 0 {
+ iter.prev();
+ break;
+ } else {
+ open_parents -= 1;
+ }
+ },
+ ']' => {
+ if open_brackets == 0 {
+ iter.prev();
+ break;
+ } else {
+ open_brackets -= 1;
+ }
+ },
+ _ => (),
+ }
+
+ if iter.point() == end {
+ break;
+ }
+
+ match iter.next() {
+ Some(indexed) => c = indexed.cell.c,
+ None => break,
+ }
+ }
+
+ // Truncate trailing characters which are likely to be delimiters.
+ let start = *regex_match.start();
+ while iter.point() != start {
+ if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') {
+ break;
+ }
+
+ match iter.prev() {
+ Some(indexed) => c = indexed.cell.c,
+ None => break,
+ }
+ }
+
+ start..=iter.point()
+ }
+}
+
+impl<'a, T> Iterator for HintPostProcessor<'a, T> {
+ type Item = Match;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let next_match = self.next_match.take()?;
+
+ if self.start <= self.end {
+ if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) {
+ let regex_match = self.hint_post_processing(&rm);
+ self.start = regex_match.end().add(self.term, Boundary::Grid, 1);
+ self.next_match = Some(regex_match);
+ }
+ }
+
+ Some(next_match)
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
index 78220b59..6e40e35c 100644
--- a/alacritty/src/display/mod.rs
+++ b/alacritty/src/display/mod.rs
@@ -2,6 +2,7 @@
//! GPU drawing.
use std::cmp::min;
+use std::convert::TryFrom;
use std::f64;
use std::fmt::{self, Formatter};
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
@@ -27,6 +28,7 @@ use alacritty_terminal::event::{EventListener, OnResize};
use alacritty_terminal::grid::Dimensions as _;
use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::selection::Selection;
+use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES};
use crate::config::font::Font;
@@ -38,14 +40,13 @@ use crate::display::bell::VisualBell;
use crate::display::color::List;
use crate::display::content::RenderableContent;
use crate::display::cursor::IntoRects;
-use crate::display::hint::HintState;
+use crate::display::hint::{HintMatch, HintState};
use crate::display::meter::Meter;
use crate::display::window::Window;
use crate::event::{Mouse, SearchState};
use crate::message_bar::{MessageBuffer, MessageType};
use crate::renderer::rects::{RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, QuadRenderer};
-use crate::url::{Url, Urls};
pub mod content;
pub mod cursor;
@@ -58,7 +59,13 @@ mod meter;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
mod wayland_theme;
+/// Maximum number of linewraps followed outside of the viewport during search highlighting.
+pub const MAX_SEARCH_LINES: usize = 100;
+
+/// Label for the forward terminal search bar.
const FORWARD_SEARCH_LABEL: &str = "Search: ";
+
+/// Label for the backward terminal search bar.
const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
#[derive(Debug)]
@@ -164,10 +171,12 @@ impl DisplayUpdate {
pub struct Display {
pub size_info: SizeInfo,
pub window: Window,
- pub urls: Urls,
- /// Currently highlighted URL.
- pub highlighted_url: Option<Url>,
+ /// Hint highlighted by the mouse.
+ pub highlighted_hint: Option<HintMatch>,
+
+ /// Hint highlighted by the vi mode cursor.
+ pub vi_highlighted_hint: Option<HintMatch>,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
pub wayland_event_queue: Option<EventQueue>,
@@ -331,8 +340,8 @@ impl Display {
hint_state,
meter: Meter::new(),
size_info,
- urls: Urls::new(),
- highlighted_url: None,
+ highlighted_hint: None,
+ vi_highlighted_hint: None,
#[cfg(not(any(target_os = "macos", windows)))]
is_x11,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
@@ -473,8 +482,6 @@ impl Display {
terminal: MutexGuard<'_, Term<T>>,
message_buffer: &MessageBuffer,
config: &Config,
- mouse: &Mouse,
- mods: ModifiersState,
search_state: &SearchState,
) {
// Collect renderable content before the terminal is dropped.
@@ -492,10 +499,6 @@ impl Display {
let metrics = self.glyph_cache.font_metrics();
let size_info = self.size_info;
- let selection = !terminal.selection.as_ref().map(Selection::is_empty).unwrap_or(true);
- let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE)
- && !terminal.mode().contains(TermMode::VI);
-
let vi_mode = terminal.mode().contains(TermMode::VI);
let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None };
@@ -507,18 +510,24 @@ impl Display {
});
let mut lines = RenderLines::new();
- let mut urls = Urls::new();
// Draw grid.
{
let _sampler = self.meter.sampler();
let glyph_cache = &mut self.glyph_cache;
+ let highlighted_hint = &self.highlighted_hint;
+ let vi_highlighted_hint = &self.vi_highlighted_hint;
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
// Iterate over all non-empty cells in the grid.
- for cell in grid_cells {
- // Update URL underlines.
- urls.update(&size_info, &cell);
+ for mut cell in grid_cells {
+ // Underline hints hovered by mouse or vi mode cursor.
+ let point = viewport_to_point(display_offset, cell.point);
+ if highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
+ || vi_highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
+ {
+ cell.flags.insert(Flags::UNDERLINE);
+ }
// Update underline/strikeout.
lines.update(&cell);
@@ -531,33 +540,9 @@ impl Display {
let mut rects = lines.rects(&metrics, &size_info);
- // Update visible URLs.
- self.urls = urls;
- if let Some(url) = self.urls.highlighted(config, mouse, mods, mouse_mode, selection) {
- rects.append(&mut url.rects(&metrics, &size_info));
-
- self.window.set_mouse_cursor(CursorIcon::Hand);
-
- self.highlighted_url = Some(url);
- } else if self.highlighted_url.is_some() {
- self.highlighted_url = None;
-
- if mouse_mode {
- self.window.set_mouse_cursor(CursorIcon::Default);
- } else {
- self.window.set_mouse_cursor(CursorIcon::Text);
- }
- }
-
if let Some(vi_mode_cursor) = vi_mode_cursor {
- // Highlight URLs at the vi mode cursor position.
- let vi_point = vi_mode_cursor.point;
- let line = (vi_point.line + display_offset).0 as usize;
- if let Some(url) = self.urls.find_at(Point::new(line, vi_point.column)) {
- rects.append(&mut url.rects(&metrics, &size_info));
- }
-
// Indicate vi mode by showing the cursor's position in the top right corner.
+ let vi_point = vi_mode_cursor.point;
let line = (-vi_point.line.0 + size_info.bottommost_line().0) as usize;
self.draw_line_indicator(config, &size_info, total_lines, Some(vi_point), line);
} else if search_state.regex().is_some() {
@@ -671,6 +656,47 @@ impl Display {
self.colors = List::from(&config.ui_config.colors);
}
+ /// Update the mouse/vi mode cursor hint highlighting.
+ pub fn update_highlighted_hints<T>(
+ &mut self,
+ term: &Term<T>,
+ config: &Config,
+ mouse: &Mouse,
+ modifiers: ModifiersState,
+ ) {
+ // Update vi mode cursor hint.
+ if term.mode().contains(TermMode::VI) {
+ let mods = ModifiersState::all();
+ let point = term.vi_mode_cursor.point;
+ self.vi_highlighted_hint = hint::highlighted_at(&term, config, point, mods);
+ } else {
+ self.vi_highlighted_hint = None;
+ }
+
+ // Abort if mouse highlighting conditions are not met.
+ if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) {
+ self.highlighted_hint = None;
+ return;
+ }
+
+ // Find highlighted hint at mouse position.
+ let point = viewport_to_point(term.grid().display_offset(), mouse.point);
+ let highlighted_hint = hint::highlighted_at(&term, config, point, modifiers);
+
+ // Update cursor shape.
+ if highlighted_hint.is_some() {
+ self.window.set_mouse_cursor(CursorIcon::Hand);
+ } else if self.highlighted_hint.is_some() {
+ if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) {
+ self.window.set_mouse_cursor(CursorIcon::Default);
+ } else {
+ self.window.set_mouse_cursor(CursorIcon::Text);
+ }
+ }
+
+ self.highlighted_hint = highlighted_hint;
+ }
+
/// Format search regex to account for the cursor and fullwidth characters.
fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String {
// Add spacers for wide chars.
@@ -782,6 +808,18 @@ impl Display {
}
}
+/// Convert a terminal point to a viewport relative point.
+pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<Point<usize>> {
+ let viewport_line = point.line.0 + display_offset as i32;
+ usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column))
+}
+
+/// Convert a viewport relative point to a terminal point.
+pub fn viewport_to_point(display_offset: usize, point: Point<usize>) -> Point {
+ let line = Line(point.line as i32) - display_offset;
+ Point::new(line, point.column)
+}
+
/// Calculate the cell dimensions based on font metrics.
///
/// This will return a tuple of the cell width and height.