diff --git a/crates/maple_core/src/config.rs b/crates/maple_core/src/config.rs index 47f38e881..d58687d29 100644 --- a/crates/maple_core/src/config.rs +++ b/crates/maple_core/src/config.rs @@ -103,7 +103,7 @@ impl Default for LogConfig { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct HighlightCursorWordConfig { +pub struct CursorWordHighlighterConfig { /// Whether to enable this plugin. pub enable: bool, /// Whether to ignore the comment line @@ -112,7 +112,7 @@ pub struct HighlightCursorWordConfig { pub ignore_files: String, } -impl Default for HighlightCursorWordConfig { +impl Default for CursorWordHighlighterConfig { fn default() -> Self { Self { enable: false, @@ -124,7 +124,7 @@ impl Default for HighlightCursorWordConfig { #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct MarkdownTocConfig { +pub struct MarkdownPluginConfig { /// Whether to enable this plugin. pub enable: bool, } @@ -139,8 +139,8 @@ pub struct CtagsPluginConfig { #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct PluginConfig { - pub highlight_cursor_word: HighlightCursorWordConfig, - pub markdown_toc: MarkdownTocConfig, + pub cursor_word_highlighter: CursorWordHighlighterConfig, + pub markdown: MarkdownPluginConfig, pub ctags: CtagsPluginConfig, } @@ -260,7 +260,7 @@ mod tests { [matcher] tiebreak = "score,-begin,-end,-length" - [plugin.highlight-cursor-word] + [plugin.cursor-word-highlighter] enable = true [provider.debounce] diff --git a/crates/maple_core/src/stdio_server/input.rs b/crates/maple_core/src/stdio_server/input.rs index 97bab9568..e9dc15f8f 100644 --- a/crates/maple_core/src/stdio_server/input.rs +++ b/crates/maple_core/src/stdio_server/input.rs @@ -1,3 +1,4 @@ +use crate::stdio_server::plugin::{MarkdownPlugin, PluginId, SystemPlugin}; use crate::stdio_server::provider::ProviderId; use crate::stdio_server::service::ProviderSessionId; use rpc::{Params, RpcNotification}; @@ -10,6 +11,7 @@ pub type AutocmdEvent = (AutocmdEventType, Params); #[derive(Debug, Clone)] pub enum PluginEvent { Autocmd(AutocmdEvent), + Action(PluginAction), } impl PluginEvent { @@ -19,6 +21,7 @@ impl PluginEvent { Self::Autocmd((autocmd_event_type, _)) => { matches!(autocmd_event_type, AutocmdEventType::CursorMoved) } + _ => false, } } } @@ -73,12 +76,43 @@ pub enum AutocmdEventType { BufWinLeave, } +pub type Action = (PluginId, PluginAction); + #[derive(Debug, Clone)] -pub struct Action { - pub command: String, +pub struct PluginAction { + pub action: String, pub params: Params, } +impl PluginAction { + fn empty() -> Self { + Self { + action: Default::default(), + params: Params::None, + } + } +} + +impl From for PluginAction { + fn from(notification: RpcNotification) -> Self { + Self { + action: notification.method, + params: notification.params, + } + } +} + +fn parse_action(notification: RpcNotification) -> Action { + let action = notification.method.as_str(); + if SystemPlugin::ACTIONS.contains(&action) { + (PluginId::System, notification.into()) + } else if MarkdownPlugin::ACTIONS.contains(&action) { + (PluginId::Markdown, notification.into()) + } else { + (PluginId::Unknown, PluginAction::empty()) + } +} + #[derive(Debug)] pub enum Event { NewProvider(Params), @@ -114,10 +148,7 @@ impl Event { "BufWritePost" => Self::Autocmd((AutocmdEventType::BufWritePost, notification.params)), "BufWinEnter" => Self::Autocmd((AutocmdEventType::BufWinEnter, notification.params)), "BufWinLeave" => Self::Autocmd((AutocmdEventType::BufWinLeave, notification.params)), - _ => Self::Action(Action { - command: notification.method, - params: notification.params, - }), + _ => Self::Action(parse_action(notification)), } } } diff --git a/crates/maple_core/src/stdio_server/mod.rs b/crates/maple_core/src/stdio_server/mod.rs index 5793210fe..9593951ef 100644 --- a/crates/maple_core/src/stdio_server/mod.rs +++ b/crates/maple_core/src/stdio_server/mod.rs @@ -8,12 +8,13 @@ mod vim; pub use self::input::InputHistory; use self::input::{Event, PluginEvent, ProviderEvent}; -use self::plugin::{ClapPlugin, CtagsPlugin, CursorWordHighlighter}; +use self::plugin::{ + ClapPlugin, CtagsPlugin, CursorWordHighlighter, MarkdownPlugin, PluginId, SystemPlugin, +}; use self::provider::{create_provider, Context}; use self::service::ServiceManager; use self::vim::initialize_syntax_map; pub use self::vim::{Vim, VimProgressor}; -use crate::stdio_server::input::Action; use anyhow::{anyhow, Result}; use parking_lot::Mutex; use rpc::{RpcClient, RpcNotification, RpcRequest, VimMessage}; @@ -89,13 +90,27 @@ impl Client { /// Creates a new instnace of [`Client`]. fn new(vim: Vim) -> Self { let mut service_manager = ServiceManager::default(); - if crate::config::config().plugin.ctags.enable { - service_manager - .new_plugin(Box::new(CtagsPlugin::new(vim.clone())) as Box); + service_manager.new_plugin( + PluginId::System, + Box::new(SystemPlugin::new(vim.clone())) as Box, + ); + let plugin = &crate::config::config().plugin; + if plugin.ctags.enable { + service_manager.new_plugin( + PluginId::Ctags, + Box::new(CtagsPlugin::new(vim.clone())) as Box, + ); + } + if plugin.markdown.enable { + service_manager.new_plugin( + PluginId::Markdown, + Box::new(MarkdownPlugin::new(vim.clone())) as Box, + ); } - if crate::config::config().plugin.highlight_cursor_word.enable { + if plugin.cursor_word_highlighter.enable { service_manager.new_plugin( - Box::new(CursorWordHighlighter::new(vim.clone())) as Box + PluginId::CursorWordHighlighter, + Box::new(CursorWordHighlighter::new(vim.clone())) as Box, ); } Self { @@ -226,58 +241,11 @@ impl Client { .lock() .notify_plugins(PluginEvent::Autocmd(autocmd_event)); } - Event::Action(action) => self.handle_action(action).await?, - } - - Ok(()) - } - - async fn handle_action(&self, action: Action) -> Result<()> { - match action.command.as_str() { - "note_recent_files" => { - let bufnr: Vec = action.params.parse()?; - let bufnr = bufnr - .first() - .ok_or(anyhow!("bufnr not found in `note_recent_file`"))?; - let file_path: String = self.vim.expand(format!("#{bufnr}:p")).await?; - handler::messages::note_recent_file(file_path)? - } - "open-config" => { - let config_file = crate::config::config_file(); - self.vim - .exec("execute", format!("edit {}", config_file.display()))?; - } - "generate-toc" => { - let curlnum = self.vim.line(".").await?; - let file = self.vim.current_buffer_path().await?; - let shiftwidth = self.vim.getbufvar("", "&shiftwidth").await?; - let mut toc = plugin::generate_toc(file, curlnum, shiftwidth)?; - let prev_line = self.vim.curbufline(curlnum - 1).await?; - if !prev_line.map(|line| line.is_empty()).unwrap_or(false) { - toc.push_front(Default::default()); - } - self.vim - .exec("append_and_write", json!([curlnum - 1, toc]))?; - } - "update-toc" => { - let file = self.vim.current_buffer_path().await?; - let bufnr = self.vim.bufnr("").await?; - if let Some((start, end)) = plugin::find_toc_range(&file)? { - let shiftwidth = self.vim.getbufvar("", "&shiftwidth").await?; - // TODO: skip update if the new doc is the same as the old one. - let new_toc = plugin::generate_toc(file, start + 1, shiftwidth)?; - self.vim.deletebufline(bufnr, start + 1, end + 1).await?; - self.vim.exec("append_and_write", json!([start, new_toc]))?; - } - } - "delete-toc" => { - let file = self.vim.current_buffer_path().await?; - let bufnr = self.vim.bufnr("").await?; - if let Some((start, end)) = plugin::find_toc_range(file)? { - self.vim.deletebufline(bufnr, start + 1, end + 1).await?; - } + Event::Action((plugin_id, plugin_action)) => { + self.service_manager_mutex + .lock() + .notify_plugin(plugin_id, PluginEvent::Action(plugin_action)); } - _ => return Err(anyhow!("Unknown action: {action:?}")), } Ok(()) diff --git a/crates/maple_core/src/stdio_server/plugin/ctags.rs b/crates/maple_core/src/stdio_server/plugin/ctags.rs index 4e9824aff..3f5dbdb23 100644 --- a/crates/maple_core/src/stdio_server/plugin/ctags.rs +++ b/crates/maple_core/src/stdio_server/plugin/ctags.rs @@ -94,7 +94,9 @@ impl ClapPlugin for CtagsPlugin { async fn on_plugin_event(&mut self, plugin_event: PluginEvent) -> Result<()> { use AutocmdEventType::{BufDelete, BufEnter, BufWritePost, CursorMoved}; - let PluginEvent::Autocmd(autocmd_event) = plugin_event; + let PluginEvent::Autocmd(autocmd_event) = plugin_event else { + return Ok(()); + }; let (event_type, params) = autocmd_event; diff --git a/crates/maple_core/src/stdio_server/plugin/highlight_cursor_word.rs b/crates/maple_core/src/stdio_server/plugin/highlight_cursor_word.rs index acb2050e8..2d1d624fd 100644 --- a/crates/maple_core/src/stdio_server/plugin/highlight_cursor_word.rs +++ b/crates/maple_core/src/stdio_server/plugin/highlight_cursor_word.rs @@ -103,7 +103,7 @@ impl CursorWordHighlighter { pub fn new(vim: Vim) -> Self { let (ignore_extensions, ignore_file_names): (Vec<_>, Vec<_>) = crate::config::config() .plugin - .highlight_cursor_word + .cursor_word_highlighter .ignore_files .split(',') .partition(|s| s.starts_with("*.")); @@ -155,7 +155,7 @@ impl CursorWordHighlighter { if crate::config::config() .plugin - .highlight_cursor_word + .cursor_word_highlighter .ignore_comment_line { if let Some(ext) = source_file.extension().and_then(|s| s.to_str()) { @@ -219,7 +219,9 @@ impl ClapPlugin for CursorWordHighlighter { async fn on_plugin_event(&mut self, plugin_event: PluginEvent) -> Result<()> { use AutocmdEventType::{CursorMoved, InsertEnter}; - let PluginEvent::Autocmd(autocmd_event) = plugin_event; + let PluginEvent::Autocmd(autocmd_event) = plugin_event else { + return Ok(()); + }; let (event_type, _params) = autocmd_event; diff --git a/crates/maple_core/src/stdio_server/plugin/markdown_toc.rs b/crates/maple_core/src/stdio_server/plugin/markdown_toc.rs index 51feb54ae..fdd8d9622 100644 --- a/crates/maple_core/src/stdio_server/plugin/markdown_toc.rs +++ b/crates/maple_core/src/stdio_server/plugin/markdown_toc.rs @@ -1,6 +1,11 @@ +use crate::stdio_server::input::{PluginAction, PluginEvent}; +use crate::stdio_server::plugin::ClapPlugin; +use crate::stdio_server::vim::Vim; +use anyhow::{anyhow, Result}; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use regex::Regex; +use serde_json::json; use std::collections::VecDeque; use std::path::Path; use std::str::FromStr; @@ -192,6 +197,70 @@ pub fn find_toc_range(input_file: impl AsRef) -> std::io::Result Self { + Self { vim } + } +} + +#[async_trait::async_trait] +impl ClapPlugin for MarkdownPlugin { + async fn on_plugin_event(&mut self, plugin_event: PluginEvent) -> Result<()> { + match plugin_event { + PluginEvent::Autocmd(_) => Ok(()), + PluginEvent::Action(plugin_action) => { + let PluginAction { action, params: _ } = plugin_action; + match action.as_str() { + Self::GENERATE_TOC => { + let curlnum = self.vim.line(".").await?; + let file = self.vim.current_buffer_path().await?; + let shiftwidth = self.vim.getbufvar("", "&shiftwidth").await?; + let mut toc = generate_toc(file, curlnum, shiftwidth)?; + let prev_line = self.vim.curbufline(curlnum - 1).await?; + if !prev_line.map(|line| line.is_empty()).unwrap_or(false) { + toc.push_front(Default::default()); + } + self.vim + .exec("append_and_write", json!([curlnum - 1, toc]))?; + } + Self::UPDATE_TOC => { + let file = self.vim.current_buffer_path().await?; + let bufnr = self.vim.bufnr("").await?; + if let Some((start, end)) = find_toc_range(&file)? { + let shiftwidth = self.vim.getbufvar("", "&shiftwidth").await?; + // TODO: skip update if the new doc is the same as the old one. + let new_toc = generate_toc(file, start + 1, shiftwidth)?; + self.vim.deletebufline(bufnr, start + 1, end + 1).await?; + self.vim.exec("append_and_write", json!([start, new_toc]))?; + } + } + Self::DELETE_TOC => { + let file = self.vim.current_buffer_path().await?; + let bufnr = self.vim.bufnr("").await?; + if let Some((start, end)) = find_toc_range(file)? { + self.vim.deletebufline(bufnr, start + 1, end + 1).await?; + } + } + unknown_action => return Err(anyhow!("Unknown action: {unknown_action:?}")), + } + + Ok(()) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/maple_core/src/stdio_server/plugin/mod.rs b/crates/maple_core/src/stdio_server/plugin/mod.rs index 275ea7f4e..8d615e720 100644 --- a/crates/maple_core/src/stdio_server/plugin/mod.rs +++ b/crates/maple_core/src/stdio_server/plugin/mod.rs @@ -2,16 +2,70 @@ mod ctags; mod highlight_cursor_word; mod markdown_toc; -use crate::stdio_server::input::PluginEvent; -use anyhow::Result; +use crate::stdio_server::input::{PluginAction, PluginEvent}; +use crate::stdio_server::vim::Vim; +use anyhow::{anyhow, Result}; use std::fmt::Debug; pub use ctags::CtagsPlugin; pub use highlight_cursor_word::CursorWordHighlighter; -pub use markdown_toc::{find_toc_range, generate_toc}; +pub use markdown_toc::MarkdownPlugin; /// A trait each Clap plugin must implement. #[async_trait::async_trait] pub trait ClapPlugin: Debug + Send + Sync + 'static { async fn on_plugin_event(&mut self, plugin_event: PluginEvent) -> Result<()>; } + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PluginId { + Ctags, + CursorWordHighlighter, + Markdown, + System, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct SystemPlugin { + vim: Vim, +} + +impl SystemPlugin { + const NOTE_RECENT_FILES: &'static str = "note_recent_files"; + const OPEN_CONFIG: &'static str = "open-config"; + + pub const ACTIONS: &[&'static str] = &[Self::NOTE_RECENT_FILES, Self::OPEN_CONFIG]; + + pub fn new(vim: Vim) -> Self { + Self { vim } + } +} + +#[async_trait::async_trait] +impl ClapPlugin for SystemPlugin { + async fn on_plugin_event(&mut self, plugin_event: PluginEvent) -> Result<()> { + match plugin_event { + PluginEvent::Autocmd(_) => Ok(()), + PluginEvent::Action(plugin_action) => { + let PluginAction { action, params } = plugin_action; + match action.as_str() { + Self::NOTE_RECENT_FILES => { + let bufnr: Vec = params.parse()?; + let bufnr = bufnr + .first() + .ok_or(anyhow!("bufnr not found in `note_recent_files`"))?; + let file_path: String = self.vim.expand(format!("#{bufnr}:p")).await?; + crate::stdio_server::handler::messages::note_recent_file(file_path) + } + Self::OPEN_CONFIG => { + let config_file = crate::config::config_file(); + self.vim + .exec("execute", format!("edit {}", config_file.display())) + } + _ => Ok(()), + } + } + } + } +} diff --git a/crates/maple_core/src/stdio_server/service.rs b/crates/maple_core/src/stdio_server/service.rs index d0ccb1f7c..cd434e108 100644 --- a/crates/maple_core/src/stdio_server/service.rs +++ b/crates/maple_core/src/stdio_server/service.rs @@ -13,6 +13,8 @@ use std::time::Duration; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::time::Instant; +use super::plugin::PluginId; + pub type ProviderSessionId = u64; #[derive(Debug)] @@ -334,7 +336,7 @@ impl PluginSession { #[derive(Debug, Default)] pub struct ServiceManager { providers: HashMap, - plugins: Vec>, + plugins: HashMap>, } impl ServiceManager { @@ -373,21 +375,35 @@ impl ServiceManager { } /// Creates a new plugin session with the default debounce setting. - pub fn new_plugin(&mut self, plugin: Box) { - self.plugins.push(PluginSession::create( - plugin, - Some(Duration::from_millis(50)), - )); + pub fn new_plugin(&mut self, plugin_id: PluginId, plugin: Box) { + self.plugins.insert( + plugin_id, + PluginSession::create(plugin, Some(Duration::from_millis(50))), + ); } #[allow(unused)] - pub fn new_plugin_without_debounce(&mut self, plugin: Box) { - self.plugins.push(PluginSession::create(plugin, None)); + pub fn new_plugin_without_debounce( + &mut self, + plugin_id: PluginId, + plugin: Box, + ) { + self.plugins + .insert(plugin_id, PluginSession::create(plugin, None)); } + /// Sends event message to all plugins. pub fn notify_plugins(&mut self, plugin_event: PluginEvent) { self.plugins - .retain(|plugin_sender| plugin_sender.send(plugin_event.clone()).is_ok()) + .retain(|_plugin_id, plugin_sender| plugin_sender.send(plugin_event.clone()).is_ok()); + } + + pub fn notify_plugin(&mut self, plugin_id: PluginId, plugin_event: PluginEvent) { + if let Entry::Occupied(v) = self.plugins.entry(plugin_id) { + if v.get().send(plugin_event).is_err() { + v.remove_entry(); + } + } } pub fn exists(&self, provider_session_id: ProviderSessionId) -> bool {