mod cursor;
mod draw_command_batcher;
mod grid;
mod intro;
mod style;
mod window;
use std::{collections::HashMap, sync::Arc, thread};
#[cfg(target_os = "macos")]
use {
std::collections::HashSet,
std::time::{Duration, Instant},
};
use log::{error, trace, warn};
use skia_safe::Color4f;
use tokio::sync::mpsc::unbounded_channel;
use winit::event_loop::EventLoopProxy;
use winit::window::Theme;
use crate::{
bridge::{GridLineCell, GuiOption, NeovimHandler, RedrawEvent, WindowAnchor},
clipboard::ClipboardHandle,
profiling::{tracy_named_frame, tracy_zone},
renderer::{DrawCommand, WindowDrawCommand},
running_tracker::RunningTracker,
settings::Settings,
units::{GridRect, GridSize},
window::{EventPayload, RouteId, UserEvent, WindowCommand, WindowSettings},
};
pub use cursor::{Cursor, CursorMode, CursorShape};
pub use draw_command_batcher::DrawCommandBatcher;
pub use style::{Colors, Style, UnderlineStyle};
pub use window::*;
use intro::{IntroMessageExtender, IntroProcessing};
const MODE_CMDLINE: u64 = 4;
pub const MSG_ZINDEX: u64 = 200;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SortOrder {
pub z_index: u64,
composition_order: u64,
}
impl Ord for SortOrder {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let a = (self.z_index, (self.composition_order as i64));
let b = (other.z_index, (other.composition_order as i64));
a.cmp(&b)
}
}
impl PartialOrd for SortOrder {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnchorInfo {
pub anchor_grid_id: u64,
pub anchor_type: WindowAnchor,
pub anchor_left: f64,
pub anchor_top: f64,
pub sort_order: SortOrder,
}
#[cfg(target_os = "macos")]
#[derive(Clone, Debug)]
struct MatchParenCandidate {
row: u64,
column: u64,
text: Option<String>,
is_cursor: bool,
}
#[cfg(target_os = "macos")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum MatchParenKind {
Paren,
Bracket,
Brace,
Angle,
}
impl WindowAnchor {
fn modified_top_left(
&self,
grid_left: f64,
grid_top: f64,
width: u64,
height: u64,
) -> (f64, f64) {
match self {
WindowAnchor::NorthWest => (grid_left, grid_top),
WindowAnchor::NorthEast => (grid_left - width as f64, grid_top),
WindowAnchor::SouthWest => (grid_left, grid_top - height as f64),
WindowAnchor::SouthEast => (grid_left - width as f64, grid_top - height as f64),
WindowAnchor::Absolute => (grid_left, grid_top),
}
}
}
pub struct Editor {
pub windows: HashMap<u64, Window>,
pub cursor: Cursor,
pub defined_styles: HashMap<u64, Arc<Style>>,
pub mode_list: Vec<CursorMode>,
pub draw_command_batcher: DrawCommandBatcher,
pub current_mode_index: Option<u64>,
pub ui_ready: bool,
event_loop_proxy: EventLoopProxy<EventPayload>,
route_id: RouteId,
#[allow(dead_code)]
settings: Arc<Settings>,
composition_order: u64,
intro_message_extender: IntroMessageExtender,
#[cfg(target_os = "macos")]
match_paren_highlight_ids: HashSet<u64>,
#[cfg(target_os = "macos")]
last_match_paren_flash: Option<(u64, u64, u64, Instant)>,
#[cfg(target_os = "macos")]
match_paren_cache: HashMap<u64, HashMap<(u64, u64), Option<String>>>,
#[cfg(target_os = "macos")]
match_paren_dirty: bool,
#[cfg(target_os = "macos")]
match_paren_cache_cleared_in_batch: bool,
}
impl Editor {
pub fn new(
route_id: RouteId,
event_loop_proxy: EventLoopProxy<EventPayload>,
settings: Arc<Settings>,
) -> Self {
Editor {
windows: HashMap::new(),
cursor: Cursor::new(),
defined_styles: HashMap::new(),
#[cfg(target_os = "macos")]
match_paren_highlight_ids: HashSet::new(),
#[cfg(target_os = "macos")]
last_match_paren_flash: None,
#[cfg(target_os = "macos")]
match_paren_cache: HashMap::new(),
#[cfg(target_os = "macos")]
match_paren_dirty: false,
#[cfg(target_os = "macos")]
match_paren_cache_cleared_in_batch: false,
mode_list: Vec::new(),
draw_command_batcher: DrawCommandBatcher::new(),
current_mode_index: None,
ui_ready: false,
settings,
event_loop_proxy,
route_id,
composition_order: 0,
intro_message_extender: IntroMessageExtender::new(),
}
}
fn send_window_command(&self, command: WindowCommand) {
let payload = EventPayload::for_route(UserEvent::WindowCommand(command), self.route_id);
let _ = self.event_loop_proxy.send_event(payload);
}
pub fn handle_redraw_event(&mut self, event: RedrawEvent) {
match event {
RedrawEvent::SetTitle { mut title } => {
tracy_zone!("EditorSetTitle");
if title.is_empty() {
title = "Neovide".to_string()
}
self.send_window_command(WindowCommand::TitleChanged(title));
}
RedrawEvent::ModeInfoSet { cursor_modes } => {
tracy_zone!("EditorModeInfoSet");
self.mode_list = cursor_modes;
if let Some(current_mode_i) = self.current_mode_index {
if let Some(current_mode) = self.mode_list.get(current_mode_i as usize) {
self.cursor.change_mode(current_mode, &self.defined_styles)
}
}
}
RedrawEvent::OptionSet { gui_option } => {
tracy_zone!("EditorOptionSet");
self.set_option(gui_option);
}
RedrawEvent::ModeChange { mode, mode_index } => {
tracy_zone!("ModeChange");
if let Some(cursor_mode) = self.mode_list.get(mode_index as usize) {
self.cursor.change_mode(cursor_mode, &self.defined_styles);
self.current_mode_index = Some(mode_index)
} else {
self.current_mode_index = None
}
self.draw_command_batcher.queue(DrawCommand::ModeChanged(mode));
}
RedrawEvent::MouseOn => {
tracy_zone!("EditorMouseOn");
self.send_window_command(WindowCommand::SetMouseEnabled(true));
}
RedrawEvent::MouseOff => {
tracy_zone!("EditorMouseOff");
self.send_window_command(WindowCommand::SetMouseEnabled(false));
}
RedrawEvent::BusyStart => {
tracy_zone!("EditorBusyStart");
trace!("Cursor off");
self.cursor.enabled = false;
}
RedrawEvent::BusyStop => {
tracy_zone!("EditorBusyStop");
trace!("Cursor on");
self.cursor.enabled = true;
}
RedrawEvent::Flush => {
tracy_zone!("EditorFlush");
trace!("Image flushed");
tracy_named_frame!("neovim draw command flush");
self.send_cursor_info();
{
trace!("send_batch");
self.draw_command_batcher.send_batch(self.route_id, &self.event_loop_proxy);
}
#[cfg(target_os = "macos")]
self.maybe_flash_match_paren_from_cache();
#[cfg(target_os = "macos")]
{
self.match_paren_cache_cleared_in_batch = false;
}
}
RedrawEvent::DefaultColorsSet { colors } => {
tracy_zone!("EditorDefaultColorsSet");
self.send_window_command(WindowCommand::ThemeChanged(window_theme_for_background(
colors.background,
)));
self.draw_command_batcher
.queue(DrawCommand::DefaultStyleChanged(Style::new(colors)));
self.redraw_screen();
self.draw_command_batcher.send_batch(self.route_id, &self.event_loop_proxy);
}
RedrawEvent::HighlightAttributesDefine { id, style, name } => {
tracy_zone!("EditorHighlightAttributesDefine");
self.defined_styles.insert(id, Arc::new(style));
#[cfg(target_os = "macos")]
self.update_match_paren_highlight(id, name.as_deref());
#[cfg(not(target_os = "macos"))]
let _ = name;
}
RedrawEvent::HighlightGroupSet { name, id } => {
tracy_zone!("EditorHighlightGroupSet");
#[cfg(target_os = "macos")]
if name.starts_with("MatchParen") {
self.register_match_paren_highlight_id(id);
}
#[cfg(not(target_os = "macos"))]
let _ = (name, id);
}
RedrawEvent::CursorGoto { grid, column: left, row: top } => {
tracy_zone!("EditorCursorGoto");
self.set_cursor_position(grid, left, top);
}
RedrawEvent::Resize { grid, width, height } => {
tracy_zone!("EditorResize");
self.resize_window(grid, width, height);
}
RedrawEvent::GridLine { grid, row, column_start, cells } => {
tracy_zone!("EditorGridLine");
self.set_ui_ready();
self.draw_grid_line(grid, row, column_start, &cells);
self.handle_intro_banner_for_line(grid, row, &cells);
}
RedrawEvent::GridHighlight { grid, row, column_start, column_end, highlight_id } => {
tracy_zone!("EditorGridHighlight");
#[cfg(target_os = "macos")]
self.handle_match_paren_grid_highlight(
grid,
row,
column_start,
column_end,
highlight_id,
);
#[cfg(not(target_os = "macos"))]
let _ = (grid, row, column_start, column_end, highlight_id);
}
RedrawEvent::Clear { grid } => {
tracy_zone!("EditorClear");
let window = self.windows.get_mut(&grid);
if let Some(window) = window {
window.clear(&mut self.draw_command_batcher);
}
#[cfg(target_os = "macos")]
{
self.match_paren_cache.remove(&grid);
self.match_paren_dirty = false;
self.match_paren_cache_cleared_in_batch = false;
}
self.intro_message_extender.reset(grid);
}
RedrawEvent::Destroy { grid } => {
tracy_zone!("EditorDestroy");
self.intro_message_extender.reset(grid);
#[cfg(target_os = "macos")]
{
self.match_paren_cache.remove(&grid);
self.match_paren_dirty = false;
self.match_paren_cache_cleared_in_batch = false;
}
self.close_window(grid)
}
RedrawEvent::Scroll { grid, top, bottom, left, right, rows, columns } => {
tracy_zone!("EditorScroll");
#[cfg(target_os = "macos")]
{
self.match_paren_cache.remove(&grid);
self.match_paren_dirty = false;
self.match_paren_cache_cleared_in_batch = false;
}
let window = self.windows.get_mut(&grid);
if let Some(window) = window {
window.scroll_region(
&mut self.draw_command_batcher,
GridRect::from_min_max((left, top), (right, bottom)),
GridSize::new(columns, rows),
);
}
}
RedrawEvent::WindowPosition { grid, start_row, start_column, width, height } => {
tracy_zone!("EditorWindowPosition");
self.set_window_position(grid, start_column, start_row, width, height)
}
RedrawEvent::WindowFloatPosition {
grid,
anchor,
anchor_grid,
anchor_column: anchor_left,
anchor_row: anchor_top,
z_index,
comp_index,
screen_row,
screen_col,
..
} => {
tracy_zone!("EditorWindowFloatPosition");
let anchor_type = if comp_index.is_some() {
WindowAnchor::Absolute
} else {
self.composition_order += 1;
anchor
};
let sort_order = SortOrder {
z_index,
composition_order: comp_index.unwrap_or(self.composition_order),
};
let anchor = AnchorInfo {
anchor_grid_id: anchor_grid,
anchor_type,
anchor_left,
anchor_top,
sort_order,
};
self.set_window_float_position(grid, anchor, screen_col, screen_row)
}
RedrawEvent::WindowHide { grid } => {
tracy_zone!("EditorWindowHide");
let window = self.windows.get_mut(&grid);
if let Some(window) = window {
window.anchor_info = None;
window.hide(&mut self.draw_command_batcher);
}
}
RedrawEvent::WindowClose { grid } => {
tracy_zone!("EditorWindowClose");
self.close_window(grid)
}
RedrawEvent::MessageSetPosition {
grid, row, scrolled, z_index, comp_index, ..
} => {
tracy_zone!("EditorMessageSetPosition");
self.set_message_position(grid, row, scrolled, z_index, comp_index)
}
RedrawEvent::WindowViewport {
grid,
scroll_delta: Some(scroll_delta),
..
} => {
tracy_zone!("EditorWindowViewport");
self.set_ui_ready();
self.draw_command_batcher.queue(DrawCommand::Window {
grid_id: grid,
command: WindowDrawCommand::Viewport { scroll_delta },
});
}
RedrawEvent::WindowViewportMargins { grid, top, bottom, left, right } => {
tracy_zone!("EditorWindowViewportMargins");
self.draw_command_batcher.queue(DrawCommand::Window {
grid_id: grid,
command: WindowDrawCommand::ViewportMargins { top, bottom, left, right },
});
}
RedrawEvent::Suspend => {
self.send_window_command(WindowCommand::Minimize);
}
RedrawEvent::NeovideSetRedraw(enable) => {
self.draw_command_batcher.set_enabled(enable, self.route_id, &self.event_loop_proxy)
}
RedrawEvent::NeovideIntroBannerAllowed(allowed) => {
self.intro_message_extender.set_sponsor_allowed(
allowed,
&mut self.windows,
&mut self.draw_command_batcher,
);
}
_ => {}
};
}
fn close_window(&mut self, grid: u64) {
if let Some(window) = self.windows.remove(&grid) {
window.close(&mut self.draw_command_batcher);
}
}
fn resize_window(&mut self, grid: u64, width: u64, height: u64) {
if let Some(window) = self.windows.get_mut(&grid) {
window.resize(&mut self.draw_command_batcher, (width, height));
if let Some(anchor_info) = &window.anchor_info {
let anchor_info = anchor_info.clone();
self.set_window_float_position(grid, anchor_info, None, None)
}
} else {
let window = Window::new(
grid,
WindowType::Editor,
None,
(0.0, 0.0),
(width, height),
&mut self.draw_command_batcher,
);
self.windows.insert(grid, window);
}
}
fn set_window_position(
&mut self,
grid: u64,
start_left: u64,
start_top: u64,
width: u64,
height: u64,
) {
if let Some(window) = self.windows.get_mut(&grid) {
window.position(
&mut self.draw_command_batcher,
None,
(width, height),
(start_left as f64, start_top as f64),
);
window.show(&mut self.draw_command_batcher);
} else {
let new_window = Window::new(
grid,
WindowType::Editor,
None,
(start_left as f64, start_top as f64),
(width, height),
&mut self.draw_command_batcher,
);
self.windows.insert(grid, new_window);
}
}
fn set_window_float_position(
&mut self,
grid: u64,
anchor: AnchorInfo,
screen_col: Option<u64>,
screen_row: Option<u64>,
) {
if anchor.anchor_grid_id == grid {
warn!("NeoVim requested a window to float relative to itself. This is not supported.");
return;
}
let parent_position = self.get_window_top_left(anchor.anchor_grid_id);
if let Some(window) = self.windows.get_mut(&grid) {
let width = window.get_width();
let height = window.get_height();
let neovim_composed = anchor.anchor_type == WindowAnchor::Absolute;
let (left, top, sort_order) =
if neovim_composed {
if let (Some(screen_col), Some(screen_row)) = (screen_col, screen_row) {
(screen_col as f64, screen_row as f64, anchor.sort_order.clone())
} else {
let (left, top) = window.get_grid_position();
(left, top, anchor.sort_order.clone())
}
} else {
let (mut modified_left, mut modified_top) = anchor
.anchor_type
.modified_top_left(anchor.anchor_left, anchor.anchor_top, width, height);
if let Some((parent_left, parent_top)) = parent_position {
modified_left += parent_left;
modified_top += parent_top;
}
let sort_order = if let Some(old_anchor) = &window.anchor_info {
if anchor.sort_order.z_index == old_anchor.sort_order.z_index {
old_anchor.sort_order.clone()
} else {
anchor.sort_order.clone()
}
} else {
anchor.sort_order.clone()
};
(modified_left, modified_top, sort_order)
};
let mut anchor = anchor;
anchor.sort_order = sort_order;
window.position(
&mut self.draw_command_batcher,
Some(anchor),
(width, height),
(left, top),
);
window.show(&mut self.draw_command_batcher);
} else {
error!("Attempted to float window that does not exist.");
}
}
fn set_message_position(
&mut self,
grid: u64,
grid_top: u64,
scrolled: bool,
z_index: Option<u64>,
comp_index: Option<u64>,
) {
if grid == 0 {
return;
}
let z_index = z_index.unwrap_or(MSG_ZINDEX); let parent_width = self.windows.get(&1).map(|parent| parent.get_width()).unwrap_or(1);
let anchor_info = AnchorInfo {
anchor_grid_id: 1, anchor_type: WindowAnchor::NorthWest,
anchor_left: 0.0,
anchor_top: grid_top as f64,
sort_order: SortOrder {
z_index,
composition_order: comp_index.unwrap_or(self.composition_order),
},
};
if let Some(window) = self.windows.get_mut(&grid) {
window.window_type = WindowType::Message { scrolled };
window.position(
&mut self.draw_command_batcher,
Some(anchor_info),
(parent_width, window.get_height()),
(0.0, grid_top as f64),
);
window.show(&mut self.draw_command_batcher);
} else {
let new_window = Window::new(
grid,
WindowType::Message { scrolled },
Some(anchor_info),
(0.0, grid_top as f64),
(parent_width, 1),
&mut self.draw_command_batcher,
);
self.windows.insert(grid, new_window);
}
}
fn get_window_top_left(&self, grid: u64) -> Option<(f64, f64)> {
let window = self.windows.get(&grid)?;
let window_anchor_info = &window.anchor_info;
match window_anchor_info {
Some(AnchorInfo { anchor_type: WindowAnchor::Absolute, .. }) => {
Some(window.get_grid_position())
}
Some(anchor_info) => {
let (parent_anchor_left, parent_anchor_top) =
self.get_window_top_left(anchor_info.anchor_grid_id)?;
let (anchor_modified_left, anchor_modified_top) =
anchor_info.anchor_type.modified_top_left(
anchor_info.anchor_left,
anchor_info.anchor_top,
window.get_width(),
window.get_height(),
);
Some((
parent_anchor_left + anchor_modified_left,
parent_anchor_top + anchor_modified_top,
))
}
None => Some(window.get_grid_position()),
}
}
fn set_cursor_position(&mut self, grid: u64, grid_left: u64, grid_top: u64) {
let mut window = self.windows.get_mut(&grid);
if let Some(window) = &mut window {
if let Some(anchor) = window.anchor_info.as_mut() {
self.composition_order += 1;
anchor.sort_order.composition_order = self.composition_order;
self.draw_command_batcher.queue(DrawCommand::Window {
grid_id: grid,
command: WindowDrawCommand::SortOrder(anchor.sort_order.clone()),
});
}
}
if self.settings.get::<WindowSettings>().cursor_hack {
if let Some(Window { window_type: WindowType::Message { .. }, .. }) = window {
let intentional = grid_left == 1;
let already_there = self.cursor.parent_window_id == grid;
let using_cmdline =
self.current_mode_index.map(|current| current == MODE_CMDLINE).unwrap_or(false);
if !intentional && !already_there && !using_cmdline {
trace!(
"Cursor unexpectedly sent to message buffer {grid} ({grid_left}, {grid_top})"
);
return;
}
}
}
self.cursor.parent_window_id = grid;
self.cursor.grid_position = (grid_left, grid_top);
}
fn draw_grid_line(&mut self, grid: u64, row: u64, column_start: u64, cells: &[GridLineCell]) {
#[cfg(target_os = "macos")]
self.update_match_paren_cache_from_grid_line(grid, row, column_start, cells);
if let Some(window) = self.windows.get_mut(&grid) {
window.draw_grid_line(
&mut self.draw_command_batcher,
row,
column_start,
cells.to_vec(),
&self.defined_styles,
);
}
}
#[cfg(target_os = "macos")]
fn reset_match_paren_cache_state(&mut self) {
self.match_paren_cache.clear();
self.match_paren_dirty = false;
self.match_paren_cache_cleared_in_batch = false;
}
#[cfg(target_os = "macos")]
fn update_match_paren_highlight_id(&mut self, id: u64, name: &str) {
if name.starts_with("MatchParen") {
log::info!("MatchParen highlight id defined: {id} ({name})");
self.match_paren_highlight_ids.insert(id);
} else {
self.match_paren_highlight_ids.remove(&id);
}
self.reset_match_paren_cache_state();
}
#[cfg(target_os = "macos")]
fn update_match_paren_highlight(&mut self, id: u64, name: Option<&str>) {
let Some(name) = name else {
return;
};
self.update_match_paren_highlight_id(id, name);
}
#[cfg(target_os = "macos")]
fn register_match_paren_highlight_id(&mut self, id: u64) {
self.match_paren_highlight_ids.insert(id);
self.reset_match_paren_cache_state();
}
#[cfg(target_os = "macos")]
fn grid_cell_text(&self, grid: u64, row: u64, column: u64) -> Option<String> {
self.windows.get(&grid).and_then(|window| {
let (text, _, _) = window.get_cursor_grid_cell(column, row);
(!text.is_empty()).then_some(text)
})
}
#[cfg(target_os = "macos")]
fn match_paren_expected_char(&self) -> Option<char> {
let (cursor_column, cursor_row) = self.cursor.grid_position;
let cursor_grid = self.cursor.parent_window_id;
if let Some(text) = self.grid_cell_text(cursor_grid, cursor_row, cursor_column) {
if let Some(expected_char) = Self::match_paren_expected_char_from_text(&text) {
return Some(expected_char);
}
}
if let Some(left_column) = cursor_column.checked_sub(1) {
if let Some(text) = self.grid_cell_text(cursor_grid, cursor_row, left_column) {
if let Some(expected_char) = Self::match_paren_expected_char_from_text(&text) {
return Some(expected_char);
}
}
}
Self::match_paren_expected_char_from_text(&self.cursor.grid_cell.0)
}
#[cfg(target_os = "macos")]
fn text_matches_expected_char(text: &Option<String>, expected: char) -> bool {
text.as_deref().and_then(|value| value.chars().next()) == Some(expected)
}
#[cfg(target_os = "macos")]
fn match_paren_expected_char_from_text(text: &str) -> Option<char> {
let cursor_char = text.chars().next()?;
match cursor_char {
'(' => Some(')'),
')' => Some('('),
'[' => Some(']'),
']' => Some('['),
'{' => Some('}'),
'}' => Some('{'),
'<' => Some('>'),
'>' => Some('<'),
_ => None,
}
}
#[cfg(target_os = "macos")]
fn match_paren_kind_for_char(cursor_char: char) -> Option<MatchParenKind> {
match cursor_char {
'(' | ')' => Some(MatchParenKind::Paren),
'[' | ']' => Some(MatchParenKind::Bracket),
'{' | '}' => Some(MatchParenKind::Brace),
'<' | '>' => Some(MatchParenKind::Angle),
_ => None,
}
}
#[cfg(target_os = "macos")]
fn remove_match_paren_cache_range(
&mut self,
grid: u64,
row: u64,
column_start: u64,
column_end: u64,
) {
let Some(cache) = self.match_paren_cache.get_mut(&grid) else {
return;
};
let mut to_remove = Vec::new();
for &(cached_row, cached_column) in cache.keys() {
if cached_row == row && cached_column >= column_start && cached_column < column_end {
to_remove.push((cached_row, cached_column));
}
}
for key in to_remove {
cache.remove(&key);
}
if cache.is_empty() {
self.match_paren_cache.remove(&grid);
}
}
#[cfg(target_os = "macos")]
fn update_match_paren_cache_range_with_text(
&mut self,
grid: u64,
row: u64,
column_start: u64,
column_end: u64,
highlight_id: u64,
text: Option<&str>,
) {
if !self.match_paren_highlight_ids.contains(&highlight_id) {
return;
}
let cache = self.match_paren_cache.entry(grid).or_default();
let value = text.map(str::to_string);
for column in column_start..column_end {
cache.insert((row, column), value.clone());
}
}
#[cfg(target_os = "macos")]
fn update_match_paren_cache_range_from_grid(
&mut self,
grid: u64,
row: u64,
column_start: u64,
column_end: u64,
highlight_id: u64,
) {
if !self.match_paren_highlight_ids.contains(&highlight_id) {
self.remove_match_paren_cache_range(grid, row, column_start, column_end);
return;
}
let mut values = Vec::new();
for column in column_start..column_end {
let text = self.grid_cell_text(grid, row, column);
values.push((column, text));
}
let cache = self.match_paren_cache.entry(grid).or_default();
for (column, text) in values {
cache.insert((row, column), text);
}
}
#[cfg(target_os = "macos")]
fn handle_match_paren_grid_highlight(
&mut self,
grid: u64,
row: u64,
column_start: u64,
column_end: u64,
highlight_id: u64,
) {
let is_match_paren = self.match_paren_highlight_ids.contains(&highlight_id);
if is_match_paren && !self.match_paren_cache_cleared_in_batch {
self.match_paren_cache.clear();
self.match_paren_cache_cleared_in_batch = true;
}
self.update_match_paren_cache_range_from_grid(
grid,
row,
column_start,
column_end,
highlight_id,
);
if is_match_paren {
self.match_paren_dirty = true;
}
}
#[cfg(target_os = "macos")]
fn match_paren_candidates_from_cache(&self, grid: u64) -> Vec<MatchParenCandidate> {
if self.cursor.parent_window_id != grid {
return Vec::new();
}
let Some(cache) = self.match_paren_cache.get(&grid) else {
return Vec::new();
};
let cursor_column = self.cursor.grid_position.0;
let cursor_row = self.cursor.grid_position.1;
let cursor_span = if self.cursor.double_width { 2 } else { 1 };
cache
.iter()
.map(|(&(row, column), text)| {
let text = text.clone().or_else(|| self.grid_cell_text(grid, row, column));
let is_cursor = row == cursor_row
&& column >= cursor_column
&& column < cursor_column.saturating_add(cursor_span);
MatchParenCandidate { row, column, text, is_cursor }
})
.collect()
}
#[cfg(target_os = "macos")]
fn match_paren_candidate_from_cache(&self, grid: u64) -> Option<MatchParenCandidate> {
let candidates = self.match_paren_candidates_from_cache(grid);
self.select_match_paren_candidate(candidates)
}
#[cfg(target_os = "macos")]
fn select_match_paren_candidate(
&self,
candidates: Vec<MatchParenCandidate>,
) -> Option<MatchParenCandidate> {
if candidates.is_empty() {
return None;
}
let cursor_column = self.cursor.grid_position.0;
let cursor_row = self.cursor.grid_position.1;
let distance = |candidate: &MatchParenCandidate| {
(candidate.row.abs_diff(cursor_row), candidate.column.abs_diff(cursor_column))
};
let expected = self.match_paren_expected_char().or_else(|| {
candidates
.iter()
.find_map(|candidate| {
candidate.is_cursor.then(|| {
candidate
.text
.as_deref()
.and_then(Self::match_paren_expected_char_from_text)
})
})
.flatten()
});
let mut non_cursor: Vec<MatchParenCandidate> =
candidates.into_iter().filter(|candidate| !candidate.is_cursor).collect();
if non_cursor.is_empty() {
return None;
}
let expected = expected?;
let expected_kind = Self::match_paren_kind_for_char(expected);
if let Some(expected_kind) = expected_kind {
non_cursor.retain(|candidate| {
candidate
.text
.as_deref()
.and_then(|value| value.chars().next())
.and_then(Self::match_paren_kind_for_char)
== Some(expected_kind)
});
}
let matching_candidates: Vec<MatchParenCandidate> = non_cursor
.into_iter()
.filter(|candidate| Self::text_matches_expected_char(&candidate.text, expected))
.collect();
if matching_candidates.is_empty() {
return None;
}
matching_candidates.into_iter().min_by_key(distance)
}
#[cfg(target_os = "macos")]
fn update_match_paren_cache_from_grid_line(
&mut self,
grid: u64,
row: u64,
column_start: u64,
cells: &[GridLineCell],
) {
if self.match_paren_highlight_ids.is_empty() {
return;
}
let mut column = column_start;
let mut current_highlight = None;
let mut saw_match_paren = false;
for cell in cells {
current_highlight = cell.highlight_id.or(current_highlight);
let repeat = cell.repeat.unwrap_or(1);
if repeat == 0 {
continue;
}
let column_end = column.saturating_add(repeat);
let Some(highlight_id) = current_highlight else {
column = column_end;
continue;
};
if self.match_paren_highlight_ids.contains(&highlight_id) {
if !self.match_paren_cache_cleared_in_batch {
self.match_paren_cache.clear();
self.match_paren_cache_cleared_in_batch = true;
}
saw_match_paren = true;
}
let text = if cell.text.is_empty() { None } else { Some(cell.text.as_str()) };
self.update_match_paren_cache_range_with_text(
grid,
row,
column,
column_end,
highlight_id,
text,
);
column = column_end;
}
if !saw_match_paren {
return;
}
self.match_paren_dirty = true;
}
#[cfg(target_os = "macos")]
fn maybe_flash_match_paren(&mut self, grid: u64, row: u64, column: u64, text: Option<String>) {
if self.cursor.parent_window_id == grid && self.cursor.grid_position.1 == row {
let cursor_column = self.cursor.grid_position.0;
let cursor_span = if self.cursor.double_width { 2 } else { 1 };
let cursor_end = cursor_column.saturating_add(cursor_span);
if column >= cursor_column && column < cursor_end {
return;
}
}
const MATCH_PAREN_FLASH_MIN_INTERVAL: Duration = Duration::from_millis(200);
let now = Instant::now();
let match_position = (grid, row, column);
let should_flash = !matches!(
self.last_match_paren_flash,
Some((last_grid, last_row, last_column, last_time))
if (last_grid, last_row, last_column) == match_position
&& now.duration_since(last_time) < MATCH_PAREN_FLASH_MIN_INTERVAL
);
self.last_match_paren_flash = Some((grid, row, column, now));
if should_flash {
self.send_window_command(WindowCommand::HighlightMatchingPair {
grid,
row,
column,
text,
});
}
}
#[cfg(target_os = "macos")]
fn maybe_flash_match_paren_from_cache(&mut self) {
if !self.match_paren_dirty {
return;
}
if !self.settings.get::<WindowSettings>().highlight_matching_pair {
return;
}
self.match_paren_dirty = false;
if self.match_paren_expected_char().is_none() {
return;
}
let grid = self.cursor.parent_window_id;
if let Some(candidate) = self.match_paren_candidate_from_cache(grid) {
self.maybe_flash_match_paren(grid, candidate.row, candidate.column, candidate.text);
}
}
fn handle_intro_banner_for_line(&mut self, grid: u64, row: u64, cells: &[GridLineCell]) {
if !self.intro_message_extender.sponsor_allowed() {
return;
}
match self.intro_message_extender.preprocess_line(grid, cells) {
IntroProcessing::Skip => return,
IntroProcessing::ClearBanner => {
self.intro_message_extender.maybe_hide_banner(
grid,
&mut self.windows,
&mut self.draw_command_batcher,
);
return;
}
IntroProcessing::Process => {}
}
let line_text = grid_line_cells_to_text(cells);
let sponsor_banner_row =
self.intro_message_extender.banner_injection_row(grid, row, &line_text);
self.maybe_inject_intro_banner(grid, sponsor_banner_row);
if sponsor_banner_row.is_none() {
self.intro_message_extender.maybe_hide_banner(
grid,
&mut self.windows,
&mut self.draw_command_batcher,
);
}
}
fn maybe_inject_intro_banner(&mut self, grid: u64, banner_row: Option<u64>) {
if let Some(start_row) = banner_row {
if let Some(window) = self.windows.get_mut(&grid) {
self.intro_message_extender.inject_banner(
grid,
window,
start_row,
&mut self.draw_command_batcher,
);
}
}
}
fn send_cursor_info(&mut self) {
tracy_zone!("send_cursor_info");
let (grid_left, grid_top) = self.cursor.grid_position;
if let Some(window) = self.windows.get(&self.cursor.parent_window_id) {
let (character, style, double_width) = window.get_cursor_grid_cell(grid_left, grid_top);
self.cursor.grid_cell = (character, style);
self.cursor.double_width = double_width;
} else {
self.cursor.double_width = false;
self.cursor.grid_cell = (" ".to_string(), None);
}
self.draw_command_batcher.queue(DrawCommand::UpdateCursor(self.cursor.clone()));
}
fn set_option(&mut self, gui_option: GuiOption) {
trace!("Option set {:?}", &gui_option);
match gui_option {
GuiOption::GuiFont(guifont) => {
if guifont == *"*" {
self.send_window_command(WindowCommand::ListAvailableFonts);
} else {
self.draw_command_batcher.queue(DrawCommand::FontChanged(guifont));
self.redraw_screen();
}
}
GuiOption::LineSpace(linespace) => {
self.draw_command_batcher.queue(DrawCommand::LineSpaceChanged(linespace as f32));
self.redraw_screen();
}
_ => (),
}
}
fn redraw_screen(&mut self) {
for window in self.windows.values() {
window.redraw(&mut self.draw_command_batcher);
}
}
fn set_ui_ready(&mut self) {
if !self.ui_ready {
self.ui_ready = true;
self.draw_command_batcher.queue(DrawCommand::UIReady);
}
}
}
fn grid_line_cells_to_text(cells: &[GridLineCell]) -> String {
let mut text = String::new();
for cell in cells {
let repeat = cell.repeat.unwrap_or(1);
for _ in 0..repeat {
text.push_str(&cell.text);
}
}
text
}
pub fn start_editor_handler(
route_id: RouteId,
event_loop_proxy: EventLoopProxy<EventPayload>,
running_tracker: RunningTracker,
settings: Arc<Settings>,
clipboard: ClipboardHandle,
) -> NeovimHandler {
let (redraw_event_sender, mut redraw_event_receiver) = unbounded_channel();
let (ui_command_sender, ui_command_receiver) = unbounded_channel();
let handler = NeovimHandler::new(
redraw_event_sender,
ui_command_sender,
ui_command_receiver,
event_loop_proxy.clone(),
running_tracker,
route_id,
settings.clone(),
clipboard,
);
thread::spawn(move || {
let mut editor = Editor::new(route_id, event_loop_proxy.clone(), settings.clone());
while let Some(editor_command) = redraw_event_receiver.blocking_recv() {
editor.handle_redraw_event(editor_command);
}
});
handler
}
fn is_light_color(color: &Color4f) -> bool {
0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b > 0.5
}
fn window_theme_for_background(background_color: Option<Color4f>) -> Option<Theme> {
background_color?;
match background_color.unwrap() {
color if is_light_color(&color) => Some(Theme::Light),
_ => Some(Theme::Dark),
}
}