mirror of
https://github.com/neovide/neovide.git
synced 2026-09-10 07:16:25 -04:00
feat: introduce macos matching pair highlight with find indicator (#3346)
This commit is contained in:
parent
4165be85db
commit
05111ef8cb
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue