feat: introduce macos matching pair highlight with find indicator (#3346)

This commit is contained in:
Alexsander Falcucci 2026-02-06 22:25:12 +01:00 committed by GitHub
parent 4165be85db
commit 05111ef8cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 836 additions and 9 deletions

1
Cargo.lock generated
View file

@ -2129,6 +2129,7 @@ dependencies = [
"bitflags 2.10.0",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
]

View file

@ -158,7 +158,9 @@ objc2-app-kit = { version = "0.3.1", default-features = false, features = [
objc2-quartz-core = { version = "0.3.1", default-features = false, features = [
"std",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-metal",
"CATransaction",
"CALayer",
"CAMetalLayer",
] }
@ -169,6 +171,7 @@ objc2-core-video = { version = "0.3.1", default-features = false, features = [
"CVDisplayLink",
] }
objc2-core-graphics = { version = "0.3.1", default-features = false, features = [
"CGColor",
"CGDirectDisplay",
] }
objc2-metal = { version = "0.3.1", default-features = false, features = [

View file

@ -229,6 +229,11 @@ pub enum RedrawEvent {
HighlightAttributesDefine {
id: u64,
style: Style,
name: Option<String>,
},
HighlightGroupSet {
name: String,
id: u64,
},
/// Redraw a continuous part of a `row` on a `grid`.
///
@ -242,6 +247,14 @@ pub enum RedrawEvent {
column_start: u64,
cells: Vec<GridLineCell>,
},
/// Highlight a range of cells without redrawing text.
GridHighlight {
grid: u64,
row: u64,
column_start: u64,
column_end: u64,
highlight_id: u64,
},
/// Clear a `grid`.
Clear {
grid: u64,
@ -683,13 +696,57 @@ fn parse_style(style_map: Value, _info_array: Value) -> Result<Style> {
Ok(style)
}
fn parse_hl_name(infos: Value) -> Option<String> {
fn take_names(values: Vec<(Value, Value)>, names: &mut Vec<String>) {
let possible_keys = ["hi_name", "ui_name", "name", "link"];
for (key, value) in values {
let Some(key) = key.as_str() else { continue };
if !possible_keys.contains(&key) {
continue;
}
if let Some(name) = value.as_str() {
names.push(name.to_string());
}
}
}
let mut names: Vec<String> = Vec::new();
match infos {
Value::Map(values) => take_names(values, &mut names),
Value::Array(values) => {
for value in values {
if let Value::Map(v) = value {
take_names(v, &mut names);
}
}
}
_ => {}
}
if let Some(name) = names.iter().find(|name| name.starts_with("MatchParen")) {
return Some(name.clone());
}
names.into_iter().next()
}
fn parse_hl_attr_define(hl_attr_define_arguments: Vec<Value>) -> Result<RedrawEvent> {
let [id, attributes, _terminal_attributes, infos] = extract_values(hl_attr_define_arguments)?;
let style = parse_style(attributes, infos)?;
let style = parse_style(attributes, infos.clone())?;
Ok(RedrawEvent::HighlightAttributesDefine {
id: parse_u64(id)?,
style,
name: parse_hl_name(infos),
})
}
fn parse_hl_group_set(hl_group_set_arguments: Vec<Value>) -> Result<RedrawEvent> {
let [name, id] = extract_values(hl_group_set_arguments)?;
Ok(RedrawEvent::HighlightGroupSet {
name: parse_string(name)?,
id: parse_u64(id)?,
})
}
@ -737,6 +794,33 @@ fn parse_grid_line(grid_line_arguments: Vec<Value>) -> Result<RedrawEvent> {
})
}
fn parse_grid_highlight(grid_highlight_arguments: Vec<Value>) -> Result<RedrawEvent> {
let [grid_id, row, column_start, column_end, highlight_id] =
extract_values(grid_highlight_arguments)?;
let validate = |v, field| {
(if v < 0 {
warn!("Negative grid highlight {field} received from Neovim {v}");
0
} else {
v
}) as u64
};
let grid = parse_u64(grid_id)?;
let row = validate(parse_i64(row)?, "row");
let column_start = validate(parse_i64(column_start)?, "column_start");
let column_end = validate(parse_i64(column_end)?, "column_end");
let highlight_id = validate(parse_i64(highlight_id)?, "highlight_id");
Ok(RedrawEvent::GridHighlight {
grid,
row,
column_start,
column_end,
highlight_id,
})
}
fn parse_grid_clear(grid_clear_arguments: Vec<Value>) -> Result<RedrawEvent> {
let [grid_id] = extract_values(grid_clear_arguments)?;
@ -1039,7 +1123,9 @@ pub fn parse_redraw_event(event_value: Value) -> Result<Vec<RedrawEvent>> {
"grid_resize" => Some(parse_grid_resize(event_parameters)),
"default_colors_set" => Some(parse_default_colors(event_parameters)),
"hl_attr_define" => Some(parse_hl_attr_define(event_parameters)),
"hl_group_set" => Some(parse_hl_group_set(event_parameters)),
"grid_line" => Some(parse_grid_line(event_parameters)),
"grid_highlight" => Some(parse_grid_highlight(event_parameters)),
"grid_clear" => Some(parse_grid_clear(event_parameters)),
"grid_destroy" => Some(parse_grid_destroy(event_parameters)),
"grid_cursor_goto" => Some(parse_grid_cursor_goto(event_parameters)),

View file

@ -183,6 +183,8 @@ async fn launch(
options.set_linegrid_external(true);
options.set_multigrid_external(!cmdline_settings.no_multi_grid);
options.set_rgb(true);
#[cfg(target_os = "macos")]
options.set_hlstate_external(true);
// We can close the handle here, as Neovim already owns it
#[cfg(not(target_os = "windows"))]
if let Some(fd) = session.stdin_fd.take() {

View file

@ -7,6 +7,12 @@ 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;
@ -67,6 +73,24 @@ pub struct AnchorInfo {
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,
@ -98,6 +122,16 @@ pub struct Editor {
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 {
@ -106,6 +140,16 @@ impl 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,
@ -179,10 +223,19 @@ impl Editor {
trace!("Image flushed");
tracy_named_frame!("neovim draw command flush");
self.send_cursor_info();
{
trace!("send_batch");
self.draw_command_batcher.send_batch(&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");
@ -196,9 +249,25 @@ impl Editor {
self.redraw_screen();
self.draw_command_batcher.send_batch(&self.event_loop_proxy);
}
RedrawEvent::HighlightAttributesDefine { id, style } => {
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,
@ -227,17 +296,49 @@ impl Editor {
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 {
@ -250,6 +351,12 @@ impl Editor {
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(
@ -630,6 +737,9 @@ impl Editor {
}
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,
@ -641,6 +751,435 @@ impl Editor {
}
}
#[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 {
let _ = self.event_loop_proxy.send_event(
WindowCommand::HighlightMatchingPair {
grid,
row,
column,
text,
}
.into(),
);
}
}
#[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;

View file

@ -11,13 +11,13 @@ use objc2::{
};
use objc2_app_kit::{
NSApplication, NSAutoresizingMaskOptions, NSColor, NSEvent, NSEventModifierFlags, NSFont,
NSFontAttributeName, NSFontDescriptor, NSFontWeight, NSImage, NSMenu, NSMenuItem, NSView,
NSWindow, NSWindowStyleMask, NSWindowTabbingMode,
NSFontAttributeName, NSFontDescriptor, NSFontWeight, NSFontWeightLight, NSImage, NSMenu,
NSMenuItem, NSTextView, NSView, NSWindow, NSWindowStyleMask, NSWindowTabbingMode,
};
use objc2_core_foundation::CGFloat;
use objc2_foundation::{
ns_string, MainThreadMarker, NSArray, NSAttributedString, NSData, NSDictionary, NSInteger,
NSObject, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUserDefaults, NSURL,
NSObject, NSPoint, NSProcessInfo, NSRange, NSRect, NSSize, NSString, NSUserDefaults, NSURL,
};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
@ -31,7 +31,7 @@ use crate::{
};
use crate::{cmd_line::CmdLineSettings, frame::Frame};
use crate::units::Pixel;
use crate::units::{Pixel, PixelRect};
#[cfg(target_os = "macos")]
use crate::window::ForceClickKind;
use crate::window::{WindowSettings, WindowSettingsChanged};
@ -88,6 +88,31 @@ impl TitlebarClickHandler {
}
}
define_class!(
#[derive(Debug)]
#[unsafe(super = NSTextView)]
#[thread_kind = MainThreadOnly]
struct MatchParenIndicatorView;
impl MatchParenIndicatorView {
#[unsafe(method(acceptsFirstResponder))]
fn accepts_first_responder(&self) -> bool {
false
}
#[unsafe(method(hitTest:))]
fn hit_test(&self, _point: NSPoint) -> *mut NSView {
std::ptr::null_mut()
}
}
);
impl MatchParenIndicatorView {
fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained<Self> {
unsafe { msg_send![Self::alloc(mtm), initWithFrame: frame] }
}
}
define_class!(
#[derive(Debug)]
#[unsafe(super = NSObject)]
@ -194,6 +219,7 @@ pub struct MacosWindowFeature {
menu: Option<Menu>,
settings: Arc<Settings>,
pub definition_is_active: bool,
match_paren_indicator_view: Option<Retained<MatchParenIndicatorView>>,
}
impl MacosWindowFeature {
@ -254,6 +280,7 @@ impl MacosWindowFeature {
menu: None,
settings: settings.clone(),
definition_is_active: false,
match_paren_indicator_view: None,
};
macos_window_feature.update_background();
@ -437,6 +464,109 @@ impl MacosWindowFeature {
}
}
pub fn show_find_indicator_for_rect(&mut self, rect: PixelRect<f32>, text: Option<&str>) {
// just being defensive here in case of an invalid state.
let width = rect.max.x - rect.min.x;
let height = rect.max.y - rect.min.y;
if width <= 0.0 || height <= 0.0 {
return;
}
unsafe {
let ns_view = self.ns_window.contentView().unwrap();
let scale = self.ns_window.backingScaleFactor();
let size = NSSize::new(width as f64 / scale, height as f64 / scale);
let mut origin = NSPoint::new(rect.min.x as f64 / scale, rect.min.y as f64 / scale);
// future-proof for being defensive here.
//
// NSView flipped macOS standard value is false,
// https://developer.apple.com/documentation/appkit/nsview/isflipped
//
// but winit flips it since it uses the upper-left corner as the origin.
// https://docs.rs/crate/winit-appkit/0.31.0-beta.2/source/src/view.rs#149-153
if !ns_view.isFlipped() {
let view_height = ns_view.bounds().size.height;
origin.y = view_height - origin.y - size.height;
}
let ns_rect = NSRect::new(origin, size);
self.show_match_paren_indicator(ns_view.as_ref(), ns_rect, text)
}
}
unsafe fn show_match_paren_indicator(
&mut self,
ns_view: &NSView,
rect: NSRect,
text: Option<&str>,
) {
let text = match text {
Some(text) if !text.is_empty() => text,
_ => return,
};
let indicator_view = self.ensure_match_paren_indicator_view(ns_view, rect);
indicator_view.setFrame(rect);
let ns_text = NSString::from_str(text);
indicator_view.setString(&ns_text);
let font_size = (rect.size.height * 0.85).max(1.0);
let font =
NSFont::monospacedSystemFontOfSize_weight(CGFloat::from(font_size), NSFontWeightLight);
indicator_view.setFont(Some(font.as_ref()));
indicator_view.setTextColor(Some(NSColor::textColor().as_ref()));
let show_range_selector = sel!(showFindIndicatorForRange:);
let can_show = msg_send![&*indicator_view, respondsToSelector: show_range_selector];
if can_show {
let length = text.encode_utf16().count();
indicator_view.showFindIndicatorForRange(NSRange::new(0, length));
let clear_color = NSColor::clearColor();
let _: () = msg_send![
&*indicator_view,
performSelector: sel!(setTextColor:),
withObject: clear_color.as_ref() as *const NSColor,
afterDelay: 0.35
];
}
}
fn ensure_match_paren_indicator_view(
&mut self,
ns_view: &NSView,
rect: NSRect,
) -> Retained<MatchParenIndicatorView> {
if let Some(view) = self.match_paren_indicator_view.as_ref() {
return view.clone();
}
let mtm = MainThreadMarker::new()
.expect("MatchParen indicator must be created on the main thread.");
let view = MatchParenIndicatorView::new(mtm, rect);
self.setup_match_paren_indicator_view(&view);
ns_view.addSubview(&view);
self.match_paren_indicator_view = Some(view.clone());
view
}
fn setup_match_paren_indicator_view(&self, view: &MatchParenIndicatorView) {
view.setEditable(false);
view.setSelectable(false);
view.setDrawsBackground(false);
view.setTextContainerInset(NSSize::new(0.0, 0.0));
view.setString(ns_string!(""));
view.setTextColor(Some(NSColor::clearColor().as_ref()));
if let Some(container) = unsafe { view.textContainer() } {
container.setLineFragmentPadding(CGFloat::from(0.0));
}
}
fn definition_font_request(guifont: &str, cell_height_px: f32) -> (f64, Option<String>) {
let options = FontOptions::parse(guifont).unwrap_or_default();
let font_size = if options.size > 0.0 {

View file

@ -7,7 +7,7 @@ pub mod opengl;
pub mod profiler;
pub mod progress_bar;
mod rendered_layer;
mod rendered_window;
pub mod rendered_window;
mod vsync;
#[cfg(target_os = "windows")]

View file

@ -13,6 +13,10 @@ use crate::{
utils::RingBuffer,
};
#[cfg(target_os = "macos")]
pub const BASE_GRID_ID: u64 = 1;
pub const NO_MULTIGRID_GRID_ID: u64 = 0;
#[derive(Debug)]
pub struct ViewportMargins {
pub top: u64,
@ -98,7 +102,7 @@ pub struct WindowDrawDetails {
impl WindowDrawDetails {
pub fn event_grid_id(&self, settings: &Settings) -> u64 {
if settings.get::<CmdLineSettings>().no_multi_grid {
0
NO_MULTIGRID_GRID_ID
} else {
self.id
}

View file

@ -116,6 +116,13 @@ pub enum WindowCommand {
guifont: String,
kind: ForceClickKind,
},
#[cfg(target_os = "macos")]
HighlightMatchingPair {
grid: u64,
row: u64,
column: u64,
text: Option<String>,
},
Minimize,
ThemeChanged(Option<Theme>),
#[cfg(windows)]

View file

@ -39,6 +39,8 @@ pub struct WindowSettings {
pub input_macos_option_key_is_meta: OptionAsMeta,
#[cfg(target_os = "macos")]
pub macos_simple_fullscreen: bool,
#[cfg(target_os = "macos")]
pub highlight_matching_pair: bool,
#[cfg(target_os = "windows")]
pub title_background_color: String,
#[cfg(target_os = "windows")]
@ -85,6 +87,8 @@ impl Default for WindowSettings {
input_macos_option_key_is_meta: OptionAsMeta::None,
#[cfg(target_os = "macos")]
macos_simple_fullscreen: false,
#[cfg(target_os = "macos")]
highlight_matching_pair: false,
#[cfg(target_os = "windows")]
title_background_color: "".to_string(),
#[cfg(target_os = "windows")]

View file

@ -15,7 +15,7 @@ use super::{
#[cfg(target_os = "macos")]
use {
crate::units::{GridPos, Pixel},
crate::units::{GridPos, Pixel, PixelRect},
crate::{error_msg, window::settings},
glamour::Point2,
winit::platform::macos::{self, WindowExtMacOS},
@ -235,6 +235,38 @@ impl WinitWindowWrapper {
grid_scale_height,
);
}
#[cfg(target_os = "macos")]
WindowCommand::HighlightMatchingPair {
grid,
row,
column,
text,
} => {
use crate::renderer::rendered_window::BASE_GRID_ID;
use crate::renderer::rendered_window::NO_MULTIGRID_GRID_ID;
let target_grid = if grid == NO_MULTIGRID_GRID_ID {
BASE_GRID_ID
} else {
grid
};
let grid_scale = self.renderer.grid_renderer.grid_scale;
let cell_size = PixelSize::new(grid_scale.width(), grid_scale.height());
let grid_pos = GridPos::new(column as f32, row as f32);
let rect = if let Some(window) = self.renderer.rendered_windows.get(&target_grid) {
let mut adjusted_grid = grid_pos + window.grid_current_position.to_vector();
adjusted_grid.y -= window.scroll_animation.position;
let origin = adjusted_grid * grid_scale;
Some(PixelRect::from_origin_and_size(origin, cell_size))
} else {
None
};
if let Some(rect) = rect {
self.macos_feature_mut()
.show_find_indicator_for_rect(rect, text.as_deref());
}
}
WindowCommand::Minimize => {
self.minimize_window();
self.is_minimized = true;

View file

@ -740,6 +740,25 @@ some cases the hack itself is buggy and prevents the cursor from moving to the c
should. In that case you can try to disable it, especially if you are not using cursor animations
and the flickering does not bother as much.
#### Highlight Matching Pair (macOS only)
VimScript:
```vim
let g:neovide_highlight_matching_pair = v:true
```
Lua:
```lua
vim.g.neovide_highlight_matching_pair = true
```
**Nightly.**
When enabled, Neovide highlights the matching pair using the system find indicator. The
default is `false`.
### Input Settings
#### macOS Option Key is Meta