diff options
Diffstat (limited to 'alacritty')
-rw-r--r-- | alacritty/Cargo.toml | 1 | ||||
-rw-r--r-- | alacritty/src/config/bindings.rs | 18 | ||||
-rw-r--r-- | alacritty/src/display.rs | 173 | ||||
-rw-r--r-- | alacritty/src/event.rs | 350 | ||||
-rw-r--r-- | alacritty/src/input.rs | 154 | ||||
-rw-r--r-- | alacritty/src/renderer/mod.rs | 20 | ||||
-rw-r--r-- | alacritty/src/scheduler.rs | 36 | ||||
-rw-r--r-- | alacritty/src/url.rs | 2 | ||||
-rw-r--r-- | alacritty/src/window.rs | 7 |
9 files changed, 634 insertions, 127 deletions
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 184f2102..62942985 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -23,6 +23,7 @@ parking_lot = "0.10.2" font = { path = "../font" } urlocator = "0.1.3" copypasta = { version = "0.7.0", default-features = false } +unicode-width = "0.1" [build-dependencies] gl_generator = "0.14.0" diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 547e168c..81a46d66 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -176,6 +176,12 @@ pub enum Action { /// Allow receiving char input. ReceiveChar, + /// Start a buffer search. + Search, + + /// Start a reverse buffer search. + SearchReverse, + /// No action. None, } @@ -208,6 +214,14 @@ pub enum ViAction { ToggleBlockSelection, /// Toggle semantic vi selection. ToggleSemanticSelection, + /// Jump to the beginning of the next match. + SearchNext, + /// Jump to the beginning of the previous match. + SearchPrevious, + /// Jump to the end of the next match. + SearchEndNext, + /// Jump to the end of the previous match. + SearchEndPrevious, /// Launch the URL below the vi mode cursor. Open, } @@ -364,10 +378,14 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { D, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageDown; Y, +TermMode::VI; Action::Copy; Y, +TermMode::VI; Action::ClearSelection; + Slash, +TermMode::VI; Action::Search; + Slash, ModifiersState::SHIFT, +TermMode::VI; Action::SearchReverse; V, +TermMode::VI; ViAction::ToggleNormalSelection; V, ModifiersState::SHIFT, +TermMode::VI; ViAction::ToggleLineSelection; V, ModifiersState::CTRL, +TermMode::VI; ViAction::ToggleBlockSelection; V, ModifiersState::ALT, +TermMode::VI; ViAction::ToggleSemanticSelection; + N, +TermMode::VI; ViAction::SearchNext; + N, ModifiersState::SHIFT, +TermMode::VI; ViAction::SearchPrevious; Return, +TermMode::VI; ViAction::Open; K, +TermMode::VI; ViMotion::Up; J, +TermMode::VI; ViMotion::Down; diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index d61a5bbd..53e6fc58 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -1,6 +1,7 @@ //! The display subsystem including window management, font rasterization, and //! GPU drawing. +use std::cmp::min; use std::f64; use std::fmt::{self, Formatter}; #[cfg(not(any(target_os = "macos", windows)))] @@ -15,6 +16,7 @@ use glutin::platform::unix::EventLoopWindowTargetExtUnix; use glutin::window::CursorIcon; use log::{debug, info}; use parking_lot::MutexGuard; +use unicode_width::UnicodeWidthChar; #[cfg(not(any(target_os = "macos", windows)))] use wayland_client::{Display as WaylandDisplay, EventQueue}; @@ -23,21 +25,23 @@ use font::set_font_smoothing; use font::{self, Rasterize}; use alacritty_terminal::config::{Font, StartupMode}; -use alacritty_terminal::event::OnResize; -use alacritty_terminal::index::Line; +use alacritty_terminal::event::{EventListener, OnResize}; +use alacritty_terminal::grid::Dimensions; +use alacritty_terminal::index::{Column, Line, Point}; use alacritty_terminal::message_bar::MessageBuffer; use alacritty_terminal::meter::Meter; use alacritty_terminal::selection::Selection; -use alacritty_terminal::term::color::Rgb; use alacritty_terminal::term::{RenderableCell, SizeInfo, Term, TermMode}; use crate::config::Config; -use crate::event::{DisplayUpdate, Mouse}; +use crate::event::Mouse; use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, QuadRenderer}; use crate::url::{Url, Urls}; use crate::window::{self, Window}; +const SEARCH_LABEL: &str = "Search: "; + #[derive(Debug)] pub enum Error { /// Error with window management. @@ -99,6 +103,44 @@ impl From<glutin::ContextError> for Error { } } +#[derive(Default, Clone, Debug, PartialEq)] +pub struct DisplayUpdate { + pub dirty: bool, + + dimensions: Option<PhysicalSize<u32>>, + font: Option<Font>, + cursor_dirty: bool, +} + +impl DisplayUpdate { + pub fn dimensions(&self) -> Option<PhysicalSize<u32>> { + self.dimensions + } + + pub fn font(&self) -> Option<&Font> { + self.font.as_ref() + } + + pub fn cursor_dirty(&self) -> bool { + self.cursor_dirty + } + + pub fn set_dimensions(&mut self, dimensions: PhysicalSize<u32>) { + self.dimensions = Some(dimensions); + self.dirty = true; + } + + pub fn set_font(&mut self, font: Font) { + self.font = Some(font); + self.dirty = true; + } + + pub fn set_cursor_dirty(&mut self) { + self.cursor_dirty = true; + self.dirty = true; + } +} + /// The display wraps a window, font rasterizer, and GPU renderer. pub struct Display { pub size_info: SizeInfo, @@ -300,7 +342,7 @@ impl Display { } /// Update font size and cell dimensions. - fn update_glyph_cache(&mut self, config: &Config, font: Font) { + fn update_glyph_cache(&mut self, config: &Config, font: &Font) { let size_info = &mut self.size_info; let cache = &mut self.glyph_cache; @@ -328,13 +370,16 @@ impl Display { terminal: &mut Term<T>, pty_resize_handle: &mut dyn OnResize, message_buffer: &MessageBuffer, + search_active: bool, config: &Config, update_pending: DisplayUpdate, - ) { + ) where + T: EventListener, + { // Update font size and cell dimensions. - if let Some(font) = update_pending.font { + if let Some(font) = update_pending.font() { self.update_glyph_cache(config, font); - } else if update_pending.cursor { + } else if update_pending.cursor_dirty() { self.clear_glyph_cache(); } @@ -346,7 +391,7 @@ impl Display { let mut padding_y = f32::from(config.window.padding.y) * self.size_info.dpr as f32; // Update the window dimensions. - if let Some(size) = update_pending.dimensions { + if let Some(size) = update_pending.dimensions() { // Ensure we have at least one column and row. self.size_info.width = (size.width as f32).max(cell_width + 2. * padding_x); self.size_info.height = (size.height as f32).max(cell_height + 2. * padding_y); @@ -369,6 +414,11 @@ impl Display { pty_size.height -= pty_size.cell_height * lines as f32; } + // Add an extra line for the current search regex. + if search_active { + pty_size.height -= pty_size.cell_height; + } + // Resize PTY. pty_resize_handle.on_resize(&pty_size); @@ -393,8 +443,10 @@ impl Display { config: &Config, mouse: &Mouse, mods: ModifiersState, + search_regex: Option<&String>, ) { let grid_cells: Vec<RenderableCell> = terminal.renderable_cells(config).collect(); + let search_regex = search_regex.map(|regex| Self::format_search(®ex)); let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); let metrics = self.glyph_cache.font_metrics(); @@ -413,7 +465,17 @@ impl Display { // Update IME position. #[cfg(not(windows))] - self.window.update_ime_position(&terminal, &self.size_info); + { + let point = match &search_regex { + Some(regex) => { + let column = min(regex.len() + SEARCH_LABEL.len() - 1, terminal.cols().0 - 1); + Point::new(terminal.screen_lines() - 1, Column(column)) + }, + None => terminal.grid().cursor.point, + }; + + self.window.update_ime_position(point, &self.size_info); + } // Drop terminal as early as possible to free lock. drop(terminal); @@ -484,11 +546,13 @@ impl Display { rects.push(visual_bell_rect); } + let mut message_bar_lines = 0; if let Some(message) = message_buffer.message() { let text = message.text(&size_info); + message_bar_lines = text.len(); // Create a new rectangle for the background. - let start_line = size_info.lines().0 - text.len(); + let start_line = size_info.lines().0 - message_bar_lines; let y = size_info.cell_height.mul_add(start_line as f32, size_info.padding_y); let message_bar_rect = RenderRect::new(0., y, size_info.width, size_info.height - y, message.color(), 1.); @@ -500,31 +564,25 @@ impl Display { self.renderer.draw_rects(&size_info, rects); // Relay messages to the user. - let mut offset = 1; - for message_text in text.iter().rev() { + let fg = config.colors.primary.background; + for (i, message_text) in text.iter().rev().enumerate() { self.renderer.with_api(&config, &size_info, |mut api| { api.render_string( - &message_text, - Line(size_info.lines().saturating_sub(offset)), glyph_cache, + Line(size_info.lines().saturating_sub(i + 1)), + &message_text, + fg, None, ); }); - offset += 1; } } else { // Draw rectangles. self.renderer.draw_rects(&size_info, rects); } - // Draw render timer. - if config.render_timer() { - let timing = format!("{:.3} usec", self.meter.average()); - let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 }; - self.renderer.with_api(&config, &size_info, |mut api| { - api.render_string(&timing[..], size_info.lines() - 2, glyph_cache, Some(color)); - }); - } + self.draw_search(config, &size_info, message_bar_lines, search_regex); + self.draw_render_timer(config, &size_info); // Frame event should be requested before swaping buffers, since it requires surface // `commit`, which is done by swap buffers under the hood. @@ -546,6 +604,73 @@ impl Display { } } + /// Format search regex to account for the cursor and fullwidth characters. + fn format_search(search_regex: &str) -> String { + // Add spacers for wide chars. + let mut text = String::with_capacity(search_regex.len()); + for c in search_regex.chars() { + text.push(c); + if c.width() == Some(2) { + text.push(' '); + } + } + + // Add cursor to show whitespace. + text.push('_'); + + text + } + + /// Draw current search regex. + fn draw_search( + &mut self, + config: &Config, + size_info: &SizeInfo, + message_bar_lines: usize, + search_regex: Option<String>, + ) { + let search_regex = match search_regex { + Some(search_regex) => search_regex, + None => return, + }; + let glyph_cache = &mut self.glyph_cache; + + let label_len = SEARCH_LABEL.len(); + let num_cols = size_info.cols().0; + + // Truncate beginning of text when it exceeds viewport width. + let text_len = search_regex.len(); + let truncate_len = min((text_len + label_len).saturating_sub(num_cols), text_len); + let text = &search_regex[truncate_len..]; + + // Assure text length is at least num_cols. + let padding_len = num_cols.saturating_sub(label_len); + let text = format!("{}{:<2$}", SEARCH_LABEL, text, padding_len); + + let fg = config.colors.search_bar_foreground(); + let bg = config.colors.search_bar_background(); + let line = size_info.lines() - message_bar_lines - 1; + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string(glyph_cache, line, &text, fg, Some(bg)); + }); + } + + /// Draw render timer. + fn draw_render_timer(&mut self, config: &Config, size_info: &SizeInfo) { + if !config.render_timer() { + return; + } + let glyph_cache = &mut self.glyph_cache; + + let timing = format!("{:.3} usec", self.meter.average()); + let fg = config.colors.normal().black; + let bg = config.colors.normal().red; + + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string(glyph_cache, size_info.lines() - 2, &timing[..], fg, Some(bg)); + }); + } + /// Requst a new frame for a window on Wayland. #[inline] #[cfg(not(any(target_os = "macos", windows)))] diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 630e8ef0..edbc4086 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -1,7 +1,7 @@ //! Process window events. use std::borrow::Cow; -use std::cmp::max; +use std::cmp::{max, min}; use std::env; #[cfg(unix)] use std::fs; @@ -12,7 +12,7 @@ use std::path::PathBuf; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use glutin::dpi::PhysicalSize; use glutin::event::{ElementState, Event as GlutinEvent, ModifiersState, MouseButton, WindowEvent}; @@ -27,12 +27,10 @@ use serde_json as json; use font::set_font_smoothing; use font::{self, Size}; -use alacritty_terminal::config::Font; use alacritty_terminal::config::LOG_TARGET_CONFIG; -use alacritty_terminal::event::OnResize; -use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; -use alacritty_terminal::grid::Scroll; -use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify, OnResize}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::message_bar::{Message, MessageBuffer}; use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::sync::FairMutex; @@ -46,12 +44,18 @@ use crate::cli::Options; use crate::clipboard::Clipboard; use crate::config; use crate::config::Config; -use crate::display::Display; +use crate::display::{Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; -use crate::scheduler::Scheduler; +use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; use crate::window::Window; +/// Duration after the last user input until an unlimited search is performed. +pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); + +/// Maximum number of lines for the blocking search while still typing the search regex. +const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000); + /// Events dispatched through the UI event loop. #[derive(Debug, Clone)] pub enum Event { @@ -60,6 +64,7 @@ pub enum Event { Scroll(Scroll), ConfigReload(PathBuf), Message(Message), + SearchNext, } impl From<Event> for GlutinEvent<'_, Event> { @@ -74,17 +79,35 @@ impl From<TerminalEvent> for Event { } } -#[derive(Default, Clone, Debug, PartialEq)] -pub struct DisplayUpdate { - pub dimensions: Option<PhysicalSize<u32>>, - pub message_buffer: bool, - pub font: Option<Font>, - pub cursor: bool, +/// Regex search state. +pub struct SearchState { + /// Search string regex. + regex: Option<String>, + + /// Search direction. + direction: Direction, + + /// Change in display offset since the beginning of the search. + display_offset_delta: isize, + + /// Vi cursor position before search. + vi_cursor_point: Point, } -impl DisplayUpdate { - fn is_empty(&self) -> bool { - self.dimensions.is_none() && self.font.is_none() && !self.message_buffer && !self.cursor +impl SearchState { + fn new() -> Self { + Self::default() + } +} + +impl Default for SearchState { + fn default() -> Self { + Self { + direction: Direction::Right, + display_offset_delta: 0, + vi_cursor_point: Point::default(), + regex: None, + } } } @@ -104,6 +127,7 @@ pub struct ActionContext<'a, N, T> { pub event_loop: &'a EventLoopWindowTarget<Event>, pub urls: &'a Urls, pub scheduler: &'a mut Scheduler, + pub search_state: &'a mut SearchState, font_size: &'a mut Size, } @@ -294,19 +318,148 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon fn change_font_size(&mut self, delta: f32) { *self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP)); let font = self.config.font.clone().with_size(*self.font_size); - self.display_update_pending.font = Some(font); + self.display_update_pending.set_font(font); self.terminal.dirty = true; } fn reset_font_size(&mut self) { *self.font_size = self.config.font.size; - self.display_update_pending.font = Some(self.config.font.clone()); + self.display_update_pending.set_font(self.config.font.clone()); self.terminal.dirty = true; } + #[inline] fn pop_message(&mut self) { - self.display_update_pending.message_buffer = true; - self.message_buffer.pop(); + if !self.message_buffer.is_empty() { + self.display_update_pending.dirty = true; + self.message_buffer.pop(); + } + } + + #[inline] + fn start_search(&mut self, direction: Direction) { + let num_lines = self.terminal.screen_lines(); + let num_cols = self.terminal.cols(); + + self.search_state.regex = Some(String::new()); + self.search_state.direction = direction; + + // Store original vi cursor position as search origin and for resetting. + self.search_state.vi_cursor_point = if self.terminal.mode().contains(TermMode::VI) { + self.terminal.vi_mode_cursor.point + } else { + match direction { + Direction::Right => Point::new(Line(0), Column(0)), + Direction::Left => Point::new(num_lines - 2, num_cols - 1), + } + }; + + self.display_update_pending.dirty = true; + self.terminal.dirty = true; + } + + #[inline] + fn confirm_search(&mut self) { + // Enter vi mode once search is confirmed. + self.terminal.set_vi_mode(); + + // Force unlimited search if the previous one was interrupted. + if self.scheduler.scheduled(TimerId::DelayedSearch) { + self.goto_match(None); + } + + // Move vi cursor down if resize will pull content from history. + if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 { + self.terminal.vi_mode_cursor.point.line += 1; + } + + // Clear reset state. + self.search_state.display_offset_delta = 0; + + self.display_update_pending.dirty = true; + self.search_state.regex = None; + self.terminal.dirty = true; + } + + #[inline] + fn cancel_search(&mut self) { + self.terminal.cancel_search(); + + // Recover pre-search state. + self.search_reset_state(); + + // Move vi cursor down if resize will pull from history. + if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 { + self.terminal.vi_mode_cursor.point.line += 1; + } + + self.display_update_pending.dirty = true; + self.search_state.regex = None; + self.terminal.dirty = true; + } + + #[inline] + fn push_search(&mut self, c: char) { + let regex = match self.search_state.regex.as_mut() { + Some(regex) => regex, + None => return, + }; + + // Hide cursor while typing into the search bar. + if self.config.ui_config.mouse.hide_when_typing { + self.window.set_mouse_visible(false); + } + + // Add new char to search string. + regex.push(c); + + // Create terminal search from the new regex string. + self.terminal.start_search(®ex); + + // Update search highlighting. + self.goto_match(MAX_SEARCH_WHILE_TYPING); + + self.terminal.dirty = true; + } + + #[inline] + fn pop_search(&mut self) { + let regex = match self.search_state.regex.as_mut() { + Some(regex) => regex, + None => return, + }; + + // Hide cursor while typing into the search bar. + if self.config.ui_config.mouse.hide_when_typing { + self.window.set_mouse_visible(false); + } + + // Remove last char from search string. + regex.pop(); + + if regex.is_empty() { + // Stop search if there's nothing to search for. + self.search_reset_state(); + self.terminal.cancel_search(); + } else { + // Create terminal search from the new regex string. + self.terminal.start_search(®ex); + + // Update search highlighting. + self.goto_match(MAX_SEARCH_WHILE_TYPING); + } + + self.terminal.dirty = true; + } + + #[inline] + fn search_direction(&self) -> Direction { + self.search_state.direction + } + + #[inline] + fn search_active(&self) -> bool { + self.search_state.regex.is_some() } fn message(&self) -> Option<&Message> { @@ -334,6 +487,73 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } +impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { + /// Reset terminal to the state before search was started. + fn search_reset_state(&mut self) { + // Reset display offset. + self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta)); + self.search_state.display_offset_delta = 0; + + // Reset vi mode cursor. + let mut vi_cursor_point = self.search_state.vi_cursor_point; + vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1); + vi_cursor_point.col = min(vi_cursor_point.col, self.terminal.cols() - 1); + self.terminal.vi_mode_cursor.point = vi_cursor_point; + + // Unschedule pending timers. + self.scheduler.unschedule(TimerId::DelayedSearch); + } + + /// Jump to the first regex match from the search origin. + fn goto_match(&mut self, mut limit: Option<usize>) { + let regex = match self.search_state.regex.take() { + Some(regex) => regex, + None => return, + }; + + // Limit search only when enough lines are available to run into the limit. + limit = limit.filter(|&limit| limit <= self.terminal.total_lines()); + + // Use original position as search origin. + let mut vi_cursor_point = self.search_state.vi_cursor_point; + vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1); + let mut origin = self.terminal.visible_to_buffer(vi_cursor_point); + origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize; + + // Jump to the next match. + let direction = self.search_state.direction; + match self.terminal.search_next(origin, direction, Side::Left, limit) { + Some(regex_match) => { + let old_offset = self.terminal.grid().display_offset() as isize; + + self.terminal.vi_goto_point(*regex_match.start()); + + // Store number of lines the viewport had to be moved. + let display_offset = self.terminal.grid().display_offset(); + self.search_state.display_offset_delta += old_offset - display_offset as isize; + + // Since we found a result, we require no delayed re-search. + self.scheduler.unschedule(TimerId::DelayedSearch); + }, + // Reset viewport only when we know there is no match, to prevent unnecessary jumping. + None if limit.is_none() => self.search_reset_state(), + None => { + // Schedule delayed search if we ran into our search limit. + if !self.scheduler.scheduled(TimerId::DelayedSearch) { + self.scheduler.schedule( + Event::SearchNext.into(), + TYPING_SEARCH_DELAY, + false, + TimerId::DelayedSearch, + ); + } + }, + } + + self.search_state.regex = Some(regex); + } +} + #[derive(Debug, Eq, PartialEq)] pub enum ClickState { None, @@ -400,6 +620,7 @@ pub struct Processor<N> { display: Display, font_size: Size, event_queue: Vec<GlutinEvent<'static, Event>>, + search_state: SearchState, } impl<N: Notify + OnResize> Processor<N> { @@ -429,6 +650,7 @@ impl<N: Notify + OnResize> Processor<N> { display, event_queue: Vec::new(), clipboard, + search_state: SearchState::new(), } } @@ -513,6 +735,7 @@ impl<N: Notify + OnResize> Processor<N> { let mut terminal = terminal.lock(); let mut display_update_pending = DisplayUpdate::default(); + let old_is_searching = self.search_state.regex.is_some(); let context = ActionContext { terminal: &mut terminal, @@ -530,6 +753,7 @@ impl<N: Notify + OnResize> Processor<N> { config: &mut self.config, urls: &self.display.urls, scheduler: &mut scheduler, + search_state: &mut self.search_state, event_loop, }; let mut processor = input::Processor::new(context, &self.display.highlighted_url); @@ -539,14 +763,8 @@ impl<N: Notify + OnResize> Processor<N> { } // Process DisplayUpdate events. - if !display_update_pending.is_empty() { - self.display.handle_update( - &mut terminal, - &mut self.notifier, - &self.message_buffer, - &self.config, - display_update_pending, - ); + if display_update_pending.dirty { + self.submit_display_update(&mut terminal, old_is_searching, display_update_pending); } #[cfg(not(any(target_os = "macos", windows)))] @@ -575,12 +793,15 @@ impl<N: Notify + OnResize> Processor<N> { &self.config, &self.mouse, self.modifiers, + self.search_state.regex.as_ref(), ); } }); // Write ref tests to disk. - self.write_ref_test_results(&terminal.lock()); + if self.config.debug.ref_test { + self.write_ref_test_results(&terminal.lock()); + } } /// Handle events from glutin. @@ -598,20 +819,21 @@ impl<N: Notify + OnResize> Processor<N> { let display_update_pending = &mut processor.ctx.display_update_pending; // Push current font to update its DPR. - display_update_pending.font = - Some(processor.ctx.config.font.clone().with_size(*processor.ctx.font_size)); + let font = processor.ctx.config.font.clone(); + display_update_pending.set_font(font.with_size(*processor.ctx.font_size)); // Resize to event's dimensions, since no resize event is emitted on Wayland. - display_update_pending.dimensions = Some(PhysicalSize::new(width, height)); + display_update_pending.set_dimensions(PhysicalSize::new(width, height)); processor.ctx.size_info.dpr = scale_factor; processor.ctx.terminal.dirty = true; }, Event::Message(message) => { processor.ctx.message_buffer.push(message); - processor.ctx.display_update_pending.message_buffer = true; + processor.ctx.display_update_pending.dirty = true; processor.ctx.terminal.dirty = true; }, + Event::SearchNext => processor.ctx.goto_match(None), Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::Scroll(scroll) => processor.ctx.scroll(scroll), Event::TerminalEvent(event) => match event { @@ -647,7 +869,7 @@ impl<N: Notify + OnResize> Processor<N> { } } - processor.ctx.display_update_pending.dimensions = Some(size); + processor.ctx.display_update_pending.set_dimensions(size); processor.ctx.terminal.dirty = true; }, WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => { @@ -741,14 +963,14 @@ impl<N: Notify + OnResize> Processor<N> { } } - pub fn reload_config<T>( - path: &PathBuf, - processor: &mut input::Processor<T, ActionContext<N, T>>, - ) where + fn reload_config<T>(path: &PathBuf, processor: &mut input::Processor<T, ActionContext<N, T>>) + where T: EventListener, { - processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG); - processor.ctx.display_update_pending.message_buffer = true; + if !processor.ctx.message_buffer.is_empty() { + processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG); + processor.ctx.display_update_pending.dirty = true; + } let config = match config::reload_from(&path) { Ok(config) => config, @@ -764,7 +986,7 @@ impl<N: Notify + OnResize> Processor<N> { if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs() > std::f64::EPSILON { - processor.ctx.display_update_pending.cursor = true; + processor.ctx.display_update_pending.set_cursor_dirty(); } if processor.ctx.config.font != config.font { @@ -774,7 +996,7 @@ impl<N: Notify + OnResize> Processor<N> { } let font = config.font.clone().with_size(*processor.ctx.font_size); - processor.ctx.display_update_pending.font = Some(font); + processor.ctx.display_update_pending.set_font(font); } #[cfg(not(any(target_os = "macos", windows)))] @@ -793,12 +1015,44 @@ impl<N: Notify + OnResize> Processor<N> { processor.ctx.terminal.dirty = true; } - // Write the ref test results to the disk. - pub fn write_ref_test_results<T>(&self, terminal: &Term<T>) { - if !self.config.debug.ref_test { - return; + /// Submit the pending changes to the `Display`. + fn submit_display_update<T>( + &mut self, + terminal: &mut Term<T>, + old_is_searching: bool, + display_update_pending: DisplayUpdate, + ) where + T: EventListener, + { + // Compute cursor positions before resize. + let num_lines = terminal.screen_lines(); + let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines; + let origin_at_bottom = (!terminal.mode().contains(TermMode::VI) + && self.search_state.direction == Direction::Left) + || terminal.vi_mode_cursor.point.line == num_lines - 1; + + self.display.handle_update( + terminal, + &mut self.notifier, + &self.message_buffer, + self.search_state.regex.is_some(), + &self.config, + display_update_pending, + ); + + // Scroll to make sure search origin is visible and content moves as little as possible. + if !old_is_searching && self.search_state.regex.is_some() { + let display_offset = terminal.grid().display_offset(); + if display_offset == 0 && cursor_at_bottom && !origin_at_bottom { + terminal.scroll_display(Scroll::Delta(1)); + } else if display_offset != 0 && origin_at_bottom { + terminal.scroll_display(Scroll::Delta(-1)); + } } + } + /// Write the ref test results to the disk. + fn write_ref_test_results<T>(&self, terminal: &Term<T>) { // Dump grid state. let mut grid = terminal.grid().clone(); grid.initialize_all(Cell::default()); diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 2819c850..48450b12 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -15,6 +15,7 @@ use log::{debug, trace, warn}; use glutin::dpi::PhysicalPosition; use glutin::event::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, + VirtualKeyCode, }; use glutin::event_loop::EventLoopWindowTarget; #[cfg(target_os = "macos")] @@ -23,8 +24,8 @@ use glutin::window::CursorIcon; use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::event::EventListener; -use alacritty_terminal::grid::Scroll; -use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::message_bar::{self, Message}; use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::mode::TermMode; @@ -34,7 +35,7 @@ use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, Binding, Config, Key, ViAction}; -use crate::event::{ClickState, Event, Mouse}; +use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; use crate::window::Window; @@ -93,6 +94,13 @@ pub trait ActionContext<T: EventListener> { fn mouse_mode(&self) -> bool; fn clipboard_mut(&mut self) -> &mut Clipboard; fn scheduler_mut(&mut self) -> &mut Scheduler; + fn start_search(&mut self, direction: Direction); + fn confirm_search(&mut self); + fn cancel_search(&mut self); + fn push_search(&mut self, c: char); + fn pop_search(&mut self); + fn search_direction(&self) -> Direction; + fn search_active(&self) -> bool; } trait Execute<T: EventListener> { @@ -159,6 +167,13 @@ impl<T: EventListener> Execute<T> for Action { }, Action::ClearSelection => ctx.clear_selection(), Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), + Action::ViMotion(motion) => { + if ctx.config().ui_config.mouse.hide_when_typing { + ctx.window_mut().set_mouse_visible(false); + } + + ctx.terminal_mut().vi_motion(motion) + }, Action::ViAction(ViAction::ToggleNormalSelection) => { Self::toggle_selection(ctx, SelectionType::Simple) }, @@ -177,13 +192,44 @@ impl<T: EventListener> Execute<T> for Action { ctx.launch_url(url); } }, - Action::ViMotion(motion) => { - if ctx.config().ui_config.mouse.hide_when_typing { - ctx.window_mut().set_mouse_visible(false); + Action::ViAction(ViAction::SearchNext) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Left, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); } + }, + Action::ViAction(ViAction::SearchPrevious) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction().opposite(); - ctx.terminal_mut().vi_motion(motion) + let regex_match = ctx.terminal().search_next(origin, direction, Side::Left, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); + } + }, + Action::ViAction(ViAction::SearchEndNext) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Right, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.end()); + } }, + Action::ViAction(ViAction::SearchEndPrevious) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction().opposite(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Right, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.end()); + } + }, + Action::Search => ctx.start_search(Direction::Right), + Action::SearchReverse => ctx.start_search(Direction::Left), Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), #[cfg(target_os = "macos")] Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), @@ -199,7 +245,7 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollPageUp => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = term.grid().num_lines().0 as isize; + let scroll_lines = term.grid().screen_lines().0 as isize; term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); ctx.scroll(Scroll::PageUp); @@ -207,7 +253,7 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollPageDown => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = -(term.grid().num_lines().0 as isize); + let scroll_lines = -(term.grid().screen_lines().0 as isize); term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); ctx.scroll(Scroll::PageDown); @@ -215,29 +261,29 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollHalfPageUp => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = term.grid().num_lines().0 as isize / 2; + let scroll_lines = term.grid().screen_lines().0 as isize / 2; term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - ctx.scroll(Scroll::Lines(scroll_lines)); + ctx.scroll(Scroll::Delta(scroll_lines)); }, Action::ScrollHalfPageDown => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = -(term.grid().num_lines().0 as isize / 2); + let scroll_lines = -(term.grid().screen_lines().0 as isize / 2); term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - ctx.scroll(Scroll::Lines(scroll_lines)); + ctx.scroll(Scroll::Delta(scroll_lines)); }, Action::ScrollLineUp => { // Move vi mode cursor. let term = ctx.terminal(); if term.grid().display_offset() != term.grid().history_size() - && term.vi_mode_cursor.point.line + 1 != term.grid().num_lines() + && term.vi_mode_cursor.point.line + 1 != term.grid().screen_lines() { ctx.terminal_mut().vi_mode_cursor.point.line += 1; } - ctx.scroll(Scroll::Lines(1)); + ctx.scroll(Scroll::Delta(1)); }, Action::ScrollLineDown => { // Move vi mode cursor. @@ -247,7 +293,7 @@ impl<T: EventListener> Execute<T> for Action { ctx.terminal_mut().vi_mode_cursor.point.line -= 1; } - ctx.scroll(Scroll::Lines(-1)); + ctx.scroll(Scroll::Delta(-1)); }, Action::ScrollToTop => { ctx.scroll(Scroll::Top); @@ -261,7 +307,7 @@ impl<T: EventListener> Execute<T> for Action { // Move vi mode cursor. let term = ctx.terminal_mut(); - term.vi_mode_cursor.point.line = term.grid().num_lines() - 1; + term.vi_mode_cursor.point.line = term.grid().screen_lines() - 1; // Move to beginning twice, to always jump across linewraps. term.vi_motion(ViMotion::FirstOccupied); @@ -360,7 +406,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { self.update_url_state(&mouse_state); self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); - let last_term_line = self.ctx.terminal().grid().num_lines() - 1; + let last_term_line = self.ctx.terminal().grid().screen_lines() - 1; if (lmb_pressed || self.ctx.mouse().right_button_state == ElementState::Pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { @@ -520,7 +566,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Load mouse point, treating message bar and padding as the closest cell. let mouse = self.ctx.mouse(); let mut point = self.ctx.size_info().pixels_to_coords(mouse.x, mouse.y); - point.line = min(point.line, self.ctx.terminal().grid().num_lines() - 1); + point.line = min(point.line, self.ctx.terminal().grid().screen_lines() - 1); match button { MouseButton::Left => self.on_left_click(point), @@ -690,7 +736,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { let term = self.ctx.terminal(); let absolute = term.visible_to_buffer(term.vi_mode_cursor.point); - self.ctx.scroll(Scroll::Lines(lines as isize)); + self.ctx.scroll(Scroll::Delta(lines as isize)); // Try to restore vi mode cursor position, to keep it above its previous content. let term = self.ctx.terminal_mut(); @@ -733,7 +779,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Reset cursor when message bar height changed or all messages are gone. let size = self.ctx.size_info(); - let current_lines = (size.lines() - self.ctx.terminal().grid().num_lines()).0; + let current_lines = (size.lines() - self.ctx.terminal().grid().screen_lines()).0; let new_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0); let new_icon = match current_lines.cmp(&new_lines) { @@ -749,7 +795,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { }; self.ctx.window_mut().set_mouse_cursor(new_icon); - } else { + } else if !self.ctx.search_active() { match state { ElementState::Pressed => { self.process_mouse_bindings(button); @@ -763,6 +809,28 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Process key input. pub fn key_input(&mut self, input: KeyboardInput) { match input.state { + ElementState::Pressed if self.ctx.search_active() => { + match input.virtual_keycode { + Some(VirtualKeyCode::Back) => { + self.ctx.pop_search(); + *self.ctx.suppress_chars() = true; + }, + Some(VirtualKeyCode::Return) => { + self.ctx.confirm_search(); + *self.ctx.suppress_chars() = true; + }, + Some(VirtualKeyCode::Escape) => { + self.ctx.cancel_search(); + *self.ctx.suppress_chars() = true; + }, + _ => (), + } + + // Reset search delay when the user is still typing. + if let Some(timer) = self.ctx.scheduler_mut().get_mut(TimerId::DelayedSearch) { + timer.deadline = Instant::now() + TYPING_SEARCH_DELAY; + } + }, ElementState::Pressed => { *self.ctx.received_count() = 0; self.process_key_bindings(input); @@ -783,7 +851,19 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Process a received character. pub fn received_char(&mut self, c: char) { - if *self.ctx.suppress_chars() || self.ctx.terminal().mode().contains(TermMode::VI) { + let suppress_chars = *self.ctx.suppress_chars(); + let search_active = self.ctx.search_active(); + if suppress_chars || self.ctx.terminal().mode().contains(TermMode::VI) || search_active { + if search_active { + // Skip control characters. + let c_decimal = c as isize; + let is_printable = (c_decimal >= 0x20 && c_decimal < 0x7f) || c_decimal >= 0xa0; + + if !suppress_chars && is_printable { + self.ctx.push_search(c); + } + } + return; } @@ -877,15 +957,19 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Check if the cursor is hovering above the message bar. fn message_at_cursor(&mut self) -> bool { - self.ctx.mouse().line >= self.ctx.terminal().grid().num_lines() + self.ctx.mouse().line >= self.ctx.terminal().grid().screen_lines() } /// Whether the point is over the message bar's close button. fn message_close_at_cursor(&self) -> bool { let mouse = self.ctx.mouse(); + + // Since search is above the message bar, the button is offset by search's height. + let search_height = if self.ctx.search_active() { 1 } else { 0 }; + mouse.inside_grid && mouse.column + message_bar::CLOSE_BUTTON_TEXT.len() >= self.ctx.size_info().cols() - && mouse.line == self.ctx.terminal().grid().num_lines() + && mouse.line == self.ctx.terminal().grid().screen_lines() + search_height } /// Copy text selection. @@ -967,7 +1051,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Scale number of lines scrolled based on distance to boundary. let delta = delta as isize / step as isize; - let event = Event::Scroll(Scroll::Lines(delta)); + let event = Event::Scroll(Scroll::Delta(delta)); // Schedule event. match scheduler.get_mut(TimerId::SelectionScrolling) { @@ -1036,6 +1120,24 @@ mod tests { fn reset_font_size(&mut self) {} + fn start_search(&mut self, _direction: Direction) {} + + fn confirm_search(&mut self) {} + + fn cancel_search(&mut self) {} + + fn push_search(&mut self, _c: char) {} + + fn pop_search(&mut self) {} + + fn search_direction(&self) -> Direction { + Direction::Right + } + + fn search_active(&self) -> bool { + false + } + fn terminal(&self) -> &Term<T> { &self.terminal } diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index b2940a93..7dc037a1 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -298,7 +298,7 @@ impl GlyphCache { pub fn update_font_size<L: LoadGlyph>( &mut self, - font: config::Font, + font: &config::Font, dpr: f64, loader: &mut L, ) -> Result<(), font::Error> { @@ -307,7 +307,7 @@ impl GlyphCache { // Recompute font keys. let (regular, bold, italic, bold_italic) = - Self::compute_font_keys(&font, &mut self.rasterizer)?; + Self::compute_font_keys(font, &mut self.rasterizer)?; self.rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size })?; let metrics = self.rasterizer.metrics(regular, font.size)?; @@ -968,29 +968,29 @@ impl<'a, C> RenderApi<'a, C> { /// errors. pub fn render_string( &mut self, - string: &str, - line: Line, glyph_cache: &mut GlyphCache, - color: Option<Rgb>, + line: Line, + string: &str, + fg: Rgb, + bg: Option<Rgb>, ) { - let bg_alpha = color.map(|_| 1.0).unwrap_or(0.0); - let col = Column(0); + let bg_alpha = bg.map(|_| 1.0).unwrap_or(0.0); let cells = string .chars() .enumerate() .map(|(i, c)| RenderableCell { line, - column: col + i, + column: Column(i), inner: RenderableCellContent::Chars({ let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1]; chars[0] = c; chars }), - bg: color.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), - fg: Rgb { r: 0, g: 0, b: 0 }, flags: Flags::empty(), bg_alpha, + fg, + bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), }) .collect::<Vec<_>>(); diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs index 673029ae..db6180ca 100644 --- a/alacritty/src/scheduler.rs +++ b/alacritty/src/scheduler.rs @@ -9,6 +9,22 @@ use crate::event::Event as AlacrittyEvent; type Event = GlutinEvent<'static, AlacrittyEvent>; +/// ID uniquely identifying a timer. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TimerId { + SelectionScrolling, + DelayedSearch, +} + +/// Event scheduled to be emitted at a specific time. +pub struct Timer { + pub deadline: Instant, + pub event: Event, + + interval: Option<Duration>, + id: TimerId, +} + /// Scheduler tracking all pending timers. pub struct Scheduler { timers: VecDeque<Timer>, @@ -74,23 +90,13 @@ impl Scheduler { self.timers.remove(index).map(|timer| timer.event) } + /// Check if a timer is already scheduled. + pub fn scheduled(&mut self, id: TimerId) -> bool { + self.timers.iter().any(|timer| timer.id == id) + } + /// Access a staged event by ID. pub fn get_mut(&mut self, id: TimerId) -> Option<&mut Timer> { self.timers.iter_mut().find(|timer| timer.id == id) } } - -/// ID uniquely identifying a timer. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TimerId { - SelectionScrolling, -} - -/// Event scheduled to be emitted at a specific time. -pub struct Timer { - pub deadline: Instant, - pub event: Event, - - interval: Option<Duration>, - id: TimerId, -} diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f7c7105c..08a85f1b 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -90,7 +90,7 @@ impl Urls { self.last_point = Some(end); // Extend current state if a wide char spacer is encountered. - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + 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; diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs index 4275f859..450329d4 100644 --- a/alacritty/src/window.rs +++ b/alacritty/src/window.rs @@ -35,7 +35,9 @@ use winapi::shared::minwindef::WORD; use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig}; #[cfg(not(windows))] -use alacritty_terminal::term::{SizeInfo, Term}; +use alacritty_terminal::index::Point; +#[cfg(not(windows))] +use alacritty_terminal::term::SizeInfo; use crate::config::Config; use crate::gl; @@ -398,8 +400,7 @@ impl Window { /// Adjust the IME editor position according to the new location of the cursor. #[cfg(not(windows))] - pub fn update_ime_position<T>(&mut self, terminal: &Term<T>, size_info: &SizeInfo) { - let point = terminal.grid().cursor.point; + pub fn update_ime_position(&mut self, point: Point, size_info: &SizeInfo) { let SizeInfo { cell_width, cell_height, padding_x, padding_y, .. } = size_info; let nspot_x = f64::from(padding_x + point.col.0 as f32 * cell_width); |