use std::fmt;
use std::sync::{
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
};
use async_trait::async_trait;
use log::{trace, warn};
use nvim_rs::{Handler, Neovim, call_args};
use rmpv::Value;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use winit::event_loop::EventLoopProxy;
#[cfg(target_os = "macos")]
use crate::window::ForceClickKind;
use crate::{
LoggingReceiver, LoggingSender,
bridge::{
GuiOption, NeovimWriter, ParallelCommand, RedrawEvent,
clipboard::{get_clipboard_contents, set_clipboard_contents},
events::parse_redraw_event,
parse_progress_bar_event, send_ui,
},
clipboard::ClipboardHandle,
error_handling::ResultPanicExplanation,
running_tracker::RunningTracker,
settings::{FontConfigState, Settings},
window::{EventPayload, RouteId, UserEvent, WindowCommand},
};
use super::ui_commands::UiCommand;
#[derive(Debug, PartialEq, Eq)]
enum ClipboardRequestError {
Unavailable,
LockUnavailable(String),
CannotGetContents,
CannotSetContents,
}
impl fmt::Display for ClipboardRequestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable => write!(f, "clipboard unavailable"),
Self::LockUnavailable(source) => write!(f, "clipboard unavailable: {source}"),
Self::CannotGetContents => write!(f, "cannot get clipboard contents"),
Self::CannotSetContents => write!(f, "cannot set clipboard contents"),
}
}
}
impl From<ClipboardRequestError> for Value {
fn from(error: ClipboardRequestError) -> Self {
Value::from(error.to_string())
}
}
fn handle_clipboard_request<T, F>(
clipboard_handle: &ClipboardHandle,
request: F,
) -> Result<T, ClipboardRequestError>
where
F: FnOnce(&mut crate::clipboard::Clipboard) -> Result<T, ClipboardRequestError>,
{
let clipboard = clipboard_handle.upgrade().ok_or(ClipboardRequestError::Unavailable)?;
let mut clipboard = clipboard
.lock()
.map_err(|error| ClipboardRequestError::LockUnavailable(error.to_string()))?;
request(&mut clipboard)
}
#[derive(Default)]
struct NeovimState {
nvim: Option<Neovim<NeovimWriter>>,
can_support_ime_api: bool,
}
#[derive(Clone)]
pub struct NeovimHandler {
proxy: Arc<Mutex<EventLoopProxy<EventPayload>>>,
redraw_event_sender: LoggingSender<RedrawEvent>,
ui_command_sender: LoggingSender<UiCommand>,
ui_command_receiver: LoggingReceiver<UiCommand>,
current_neovim: Arc<RwLock<NeovimState>>,
ui_command_started: Arc<AtomicBool>,
running_tracker: RunningTracker,
route_id: RouteId,
#[allow(dead_code)]
settings: Arc<Settings>,
clipboard: ClipboardHandle,
}
impl std::fmt::Debug for NeovimHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NeovimHandler").finish()
}
}
impl NeovimHandler {
#[allow(clippy::too_many_arguments)]
pub fn new(
redraw_event_sender: UnboundedSender<RedrawEvent>,
ui_command_sender: UnboundedSender<UiCommand>,
ui_command_receiver: UnboundedReceiver<UiCommand>,
proxy: EventLoopProxy<EventPayload>,
running_tracker: RunningTracker,
route_id: RouteId,
settings: Arc<Settings>,
clipboard: ClipboardHandle,
) -> Self {
Self {
proxy: Arc::new(Mutex::new(proxy)),
redraw_event_sender: LoggingSender::attach(redraw_event_sender, "neovim_handler"),
ui_command_sender: LoggingSender::attach(ui_command_sender, "UICommand"),
ui_command_receiver: LoggingReceiver::attach(ui_command_receiver, "UICommand"),
current_neovim: Arc::new(RwLock::new(NeovimState::default())),
ui_command_started: Arc::new(AtomicBool::new(false)),
running_tracker,
route_id,
settings,
clipboard,
}
}
fn send_window_command(&self, command: WindowCommand) {
let payload = EventPayload::for_route(UserEvent::WindowCommand(command), self.route_id);
let _ = self.proxy.lock().unwrap().send_event(payload);
}
pub fn get_ui_command_channel(&self) -> (LoggingSender<UiCommand>, LoggingReceiver<UiCommand>) {
(self.ui_command_sender.clone(), self.ui_command_receiver.clone())
}
pub fn update_current_neovim(&self, neovim: Neovim<NeovimWriter>, can_support_ime_api: bool) {
if let Ok(mut guard) = self.current_neovim.write() {
guard.nvim = Some(neovim);
guard.can_support_ime_api = can_support_ime_api;
}
}
pub fn clone_current_neovim(&self) -> Option<Neovim<NeovimWriter>> {
self.current_neovim.read().ok().and_then(|guard| guard.nvim.as_ref().cloned())
}
pub fn clone_current_neovim_with_ime(&self) -> Option<(Neovim<NeovimWriter>, bool)> {
self.current_neovim.read().ok().and_then(|guard| {
guard.nvim.as_ref().cloned().map(|nvim| (nvim, guard.can_support_ime_api))
})
}
pub fn mark_ui_command_started(&self) -> bool {
self.ui_command_started.swap(true, Ordering::SeqCst)
}
}
#[async_trait]
impl Handler for NeovimHandler {
type Writer = NeovimWriter;
async fn handle_request(
&self,
event_name: String,
arguments: Vec<Value>,
_neovim: Neovim<Self::Writer>,
) -> Result<Value, Value> {
trace!("Neovim request: {:?}", &event_name);
match event_name.as_ref() {
"neovide.get_clipboard" => handle_clipboard_request(&self.clipboard, |clipboard| {
get_clipboard_contents(clipboard, &arguments[0])
.map_err(|_| ClipboardRequestError::CannotGetContents)
})
.map_err(Value::from),
"neovide.set_clipboard" => handle_clipboard_request(&self.clipboard, |clipboard| {
set_clipboard_contents(clipboard, &arguments[0], &arguments[1])
.map_err(|_| ClipboardRequestError::CannotSetContents)
})
.map_err(Value::from),
"neovide.quit" => {
let error_code =
arguments[0].as_i64().expect("Could not parse error code from neovim");
self.running_tracker.quit_with_code(error_code as u8, "Quit from neovim");
Ok(Value::Nil)
}
_ => Ok(Value::from("rpcrequest not handled")),
}
}
async fn handle_notify(
&self,
event_name: String,
arguments: Vec<Value>,
neovim: Neovim<Self::Writer>,
) {
trace!("Neovim notification: {:?}", &event_name);
match event_name.as_ref() {
"redraw" => {
for events in arguments {
let parsed_events = parse_redraw_event(events)
.unwrap_or_explained_panic("Could not parse event from neovim");
for parsed_event in parsed_events {
if skip_default_guifont(&parsed_event, &self.settings, &neovim).await {
continue;
}
match parsed_event {
RedrawEvent::Restart { details } => {
let payload = EventPayload::for_route(
UserEvent::NeovimRestart(details),
self.route_id,
);
let _ = self.proxy.lock().unwrap().send_event(payload);
}
_ => {
let _ = self.redraw_event_sender.send(parsed_event);
}
}
}
}
}
"setting_changed" => {
self.settings.handle_setting_changed_notification(
arguments,
&self.proxy.lock().unwrap(),
self.route_id,
);
}
"option_changed" => {
self.settings.handle_option_changed_notification(
arguments,
&self.proxy.lock().unwrap(),
self.route_id,
);
}
#[cfg(windows)]
"neovide.register_right_click" => {
self.send_window_command(WindowCommand::RegisterRightClick);
}
#[cfg(windows)]
"neovide.unregister_right_click" => {
self.send_window_command(WindowCommand::UnregisterRightClick);
}
"neovide.focus_window" => {
self.send_window_command(WindowCommand::FocusWindow);
}
#[cfg(target_os = "macos")]
"neovide.force_click" => match parse_force_click_args(&arguments) {
Some((col, row, entity, guifont, kind)) => {
self.send_window_command(WindowCommand::TouchpadPressure {
col,
row,
entity,
guifont,
kind,
});
}
None => warn!("neovide.force_click called with invalid arguments: {arguments:?}"),
},
"neovide.exec_detach_handler" => {
send_ui(ParallelCommand::Quit, self);
}
"neovide.set_redraw" => {
if let Some(value) = arguments.first() {
let value = value.as_bool().unwrap_or(true);
let _ = self.redraw_event_sender.send(RedrawEvent::NeovideSetRedraw(value));
}
}
"neovide.intro_banner_allowed" => {
if let Some(value) = arguments.first() {
if let Some(allowed) = value.as_bool() {
let _ = self
.redraw_event_sender
.send(RedrawEvent::NeovideIntroBannerAllowed(allowed));
}
}
}
"neovide.progress_bar" => {
parse_progress_bar_event(arguments.first())
.map(|event| {
let _ = self
.proxy
.lock()
.unwrap()
.send_event(EventPayload::for_route(event, self.route_id));
})
.unwrap_or_else(|| {
log::info!(
"Failed to parse neovide.progress_bar event data: {:?}",
arguments
);
});
}
_ => {}
}
}
}
#[cfg(target_os = "macos")]
fn parse_force_click_args(
arguments: &[Value],
) -> Option<(i64, i64, String, String, ForceClickKind)> {
let (col, row, entity, guifont, kind_value) = match arguments {
[col, row, entity, guifont, kind, ..] => (col, row, entity, guifont, Some(kind)),
[col, row, entity, guifont] => (col, row, entity, guifont, None),
_ => return None,
};
let col = col.as_i64()?;
let row = row.as_i64()?;
let entity = entity.as_str().unwrap_or("").to_string();
let guifont = guifont.as_str().unwrap_or("").to_string();
let kind_str = kind_value.and_then(Value::as_str).unwrap_or("text");
let kind = ForceClickKind::from(kind_str);
Some((col, row, entity, guifont, kind))
}
async fn skip_default_guifont(
event: &RedrawEvent,
settings: &Settings,
nvim: &Neovim<NeovimWriter>,
) -> bool {
if !settings.get::<FontConfigState>().has_font || !is_guifont_option_set(event) {
return false;
}
guifont_was_set(nvim).await.map(|was_set| !was_set).unwrap_or_else(|error| {
warn!("Failed to determine if guifont was set: {error}");
false
})
}
async fn guifont_was_set(nvim: &Neovim<NeovimWriter>) -> Result<bool, String> {
let value = nvim
.exec_lua("return vim.api.nvim_get_option_info2('guifont', {}).was_set", call_args![])
.await
.map_err(|error| error.to_string())?;
Ok(value.as_bool().unwrap_or(false))
}
fn is_guifont_option_set(event: &RedrawEvent) -> bool {
matches!(event, RedrawEvent::OptionSet { gui_option: GuiOption::GuiFont(_) })
}
#[cfg(test)]
mod tests {
use std::{
panic::{AssertUnwindSafe, catch_unwind},
sync::{Arc, Mutex},
};
use super::{ClipboardRequestError, handle_clipboard_request};
use crate::clipboard::{Clipboard, ClipboardError, ClipboardHandle, ProviderState};
fn unavailable_clipboard(error: ClipboardError) -> Arc<Mutex<Clipboard>> {
Arc::new(Mutex::new(Clipboard::from_provider_states_for_test(
ProviderState::unavailable_for_test(error.clone()),
#[cfg(target_os = "linux")]
ProviderState::unavailable_for_test(error),
)))
}
#[test]
fn clipboard_request_returns_generic_clipboard_error_message() {
let clipboard = unavailable_clipboard(ClipboardError::ProviderInitializationFailed {
provider: "clipboard",
backend: "x11",
source: "setup failed".to_string(),
});
let handle = ClipboardHandle::new(&clipboard);
let error = handle_clipboard_request(&handle, |clipboard| {
clipboard.get_contents("+").map_err(|_| ClipboardRequestError::CannotGetContents)
})
.unwrap_err();
assert_eq!(error, ClipboardRequestError::CannotGetContents);
}
#[test]
fn clipboard_request_returns_lock_error_instead_of_panicking() {
let clipboard = unavailable_clipboard(ClipboardError::ProviderInitializationFailed {
provider: "clipboard",
backend: "x11",
source: "setup failed".to_string(),
});
let handle = ClipboardHandle::new(&clipboard);
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = clipboard.lock().unwrap();
panic!("poison clipboard mutex");
}));
let error = handle_clipboard_request(&handle, |clipboard| {
clipboard.get_contents("+").map_err(|_| ClipboardRequestError::CannotGetContents)
})
.unwrap_err();
assert!(matches!(error, ClipboardRequestError::LockUnavailable(_)));
}
}