diff --git a/crates/api/src/command.rs b/crates/api/src/command.rs index a38425ab..42900cf8 100644 --- a/crates/api/src/command.rs +++ b/crates/api/src/command.rs @@ -77,8 +77,17 @@ pub fn del_user_command(name: &str) -> Result<()> { /// Returns an iterator over the infos of the global ex commands. Only /// user-defined commands are returned, not builtin ones. /// +/// # Safety +/// +/// The underlying C API function creates a Lua registry slot for each +/// command and completion callback, which it expects the caller to clean up. +/// Currently `nvim-oxi` does not perform this cleanup, consequentially +/// calling this function creates a serious leak of Lua registry slots which +/// in turn also prevents the callbacks from being garbage collected if their +/// commands are deleted. +/// /// [1]: https://neovim.io/doc/user/api.html#nvim_get_commands() -pub fn get_commands( +pub unsafe fn get_commands( opts: &GetCommandsOpts, ) -> Result + use<>> { let mut err = nvim::Error::new(); diff --git a/crates/api/src/opts/create_command.rs b/crates/api/src/opts/create_command.rs index 1cbd3e6c..cefafdee 100644 --- a/crates/api/src/opts/create_command.rs +++ b/crates/api/src/opts/create_command.rs @@ -4,7 +4,7 @@ use crate::Buffer; use crate::types::{ CommandAddr, CommandArgs, - CommandComplete, + CommandCompleteOrFunction, CommandNArgs, CommandRange, }; @@ -27,8 +27,9 @@ pub struct CreateCommandOpts { bar: types::Boolean, #[builder( - argtype = "CommandComplete", - inline = "{0}.to_object().unwrap()" + generics = "C: CommandCompleteOrFunction", + argtype = "C", + inline = "{0}.to_object()" )] complete: types::Object, diff --git a/crates/api/src/types/command_complete.rs b/crates/api/src/types/command_complete.rs index cd096597..b6d66514 100644 --- a/crates/api/src/types/command_complete.rs +++ b/crates/api/src/types/command_complete.rs @@ -1,25 +1,33 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize, de::Error}; use types::{ Function, Object, - conversion::{self, ToObject}, - serde::Serializer, + conversion::{self, FromObject, ToObject}, + serde::{Deserializer, Serializer}, }; +use crate::ToFunction; + +pub type CompleteCallbackArgs = (String, String, usize); +pub type CompleteCallbackRet = Vec; +pub type CompleteCallbackFunc = + Function; + /// See `:h command-complete` for details. #[non_exhaustive] -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CommandComplete { Arglist, Augroup, Buffer, - Behave, + Breakpoint, Color, Command, Compiler, - Cscope, + DiffBuffer, Dir, + DirInPath, Environment, Event, Expression, @@ -30,6 +38,7 @@ pub enum CommandComplete { Help, Highlight, History, + Keymap, Locale, Lua, Mapclear, @@ -38,7 +47,12 @@ pub enum CommandComplete { Messages, Option, Packadd, + #[cfg(feature = "neovim-0-12")] // On 0.12 and Nightly + Retab, + Runtime, + Scriptnames, Shellcmd, + Shellcmdline, Sign, Syntax, Syntime, @@ -48,7 +62,27 @@ pub enum CommandComplete { Var, /// See `:h command-completion-customlist` for details. - CustomList(Function<(String, String, usize), Vec>), + /// *Note*: This variant contains a nvim_oxi::Function. + #[serde(untagged, deserialize_with = "deserialize_callback")] + Callback(CompleteCallbackFunc), + + /// See `:h command-completion-customlist` for details. + /// *Note*: This variant contains the name of a Vim Script function. + #[serde( + untagged, + serialize_with = "serialize_customlist", + deserialize_with = "deserialize_customlist" + )] + Customlist(String), + + /// See `:h command-completion-custom` for details. + /// *Note*: This variant contains the name of a Vim Script function. + #[serde( + untagged, + serialize_with = "serialize_custom", + deserialize_with = "deserialize_custom" + )] + Custom(String), } impl ToObject for CommandComplete { @@ -56,3 +90,70 @@ impl ToObject for CommandComplete { self.serialize(Serializer::new()).map_err(Into::into) } } + +impl FromObject for CommandComplete { + fn from_object(obj: Object) -> Result { + Self::deserialize(Deserializer::new(obj)).map_err(Into::into) + } +} + +pub trait CommandCompleteOrFunction { + fn to_object(self) -> Object; +} + +impl CommandCompleteOrFunction for T +where + T: ToFunction, +{ + fn to_object(self) -> Object { + Object::from_luaref(self.into_luaref()) + } +} + +impl CommandCompleteOrFunction for CommandComplete { + fn to_object(self) -> Object { + ToObject::to_object(self).unwrap() + } +} + +fn deserialize_callback<'de, D>( + deserializer: D, +) -> Result +where + D: serde::de::Deserializer<'de>, +{ + CompleteCallbackFunc::deserialize(deserializer) +} + +macro_rules! serde_complete_custom { + ($ser_fn_name:ident, $de_fn_name:ident, $variant:literal) => { + fn $ser_fn_name( + vim_fn_name: &str, + serializer: S, + ) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&[$variant, vim_fn_name].join(",")) + } + + fn $de_fn_name<'de, D>(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if let Some(remainder) = value.strip_prefix($variant) { + if remainder.is_empty() { + return Ok(String::new()); + } + if let Some(vim_fn_name) = remainder.strip_prefix(",") { + return Ok(vim_fn_name.to_string()); + } + } + Err(D::Error::custom("not custom or customlist")) + } + }; +} + +serde_complete_custom! {serialize_custom, deserialize_custom, "custom"} +serde_complete_custom! {serialize_customlist, deserialize_customlist, "customlist"} diff --git a/crates/api/src/types/command_infos.rs b/crates/api/src/types/command_infos.rs index 2d2f9696..fd7a54c7 100644 --- a/crates/api/src/types/command_infos.rs +++ b/crates/api/src/types/command_infos.rs @@ -10,6 +10,8 @@ use types::{ }; use super::{CommandAddr, CommandArgs, CommandNArgs, CommandRange}; +#[cfg(feature = "neovim-0-12")] // on 0.12 and Nightly. +use crate::types::CommandComplete; #[non_exhaustive] #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)] @@ -26,7 +28,10 @@ pub struct CommandInfos { /// Callback triggered by the command. pub callback: Option>, - /// Command complletion strategy. + /// Command completion strategy. + #[cfg(feature = "neovim-0-12")] // on 0.12 and Nightly. + pub complete: Option, + #[cfg(not(feature = "neovim-0-12"))] // Only on 0.11 pub complete: Option, /// TODO: docs diff --git a/tests/src/api/command.rs b/tests/src/api/command.rs index 1358a719..53d62ff5 100644 --- a/tests/src/api/command.rs +++ b/tests/src/api/command.rs @@ -27,3 +27,49 @@ fn regression_1() { .build(); api::create_user_command("Echo", "", &opts).unwrap(); } + +#[cfg(feature = "neovim-0-12")] +#[nvim_oxi::test] +fn command_complete_customlist() { + api::command("comclear").unwrap(); + let opts = CreateCommandOpts::builder() + .nargs(CommandNArgs::Any) + .complete(CommandComplete::Customlist("VimFunc".to_string())) + .build(); + api::create_user_command("Foo", ":", &opts).unwrap(); + let cmd_info = unsafe { api::get_commands(&Default::default()) } + .unwrap() + .find(|cmd| cmd.name == "Foo") + .unwrap(); + let complete = + cmd_info.complete.expect("Missing `CommandInfos::complete` value"); + assert_eq!(complete, CommandComplete::Customlist(String::new())); + let complete_arg = cmd_info + .complete_arg + .expect("Missing `CommandInfos::complete_arg` value"); + assert_eq!(complete_arg, "VimFunc".to_string()); +} + +#[cfg(feature = "neovim-0-12")] +#[nvim_oxi::test] +fn command_complete_callback() { + api::command("comclear").unwrap(); + let opts = CreateCommandOpts::builder() + .nargs(CommandNArgs::Any) + .complete(|_args: CompleteCallbackArgs| vec!["Bar".to_string()]) + .build(); + api::create_user_command("Foo", ":", &opts).unwrap(); + let cmd_info = unsafe { api::get_commands(&Default::default()) } + .unwrap() + .find(|cmd| cmd.name == "Foo") + .unwrap(); + let complete = + cmd_info.complete.expect("Missing `CommandInfos::complete` value"); + match complete { + CommandComplete::Callback(fun) => { + let res = fun.call((String::new(), String::new(), 0)).unwrap(); + assert_eq!(Some(&"Bar".to_string()), res.first()) + }, + _ => panic!("Wrong `complete::Callback` value"), + } +} diff --git a/tests/src/api/global.rs b/tests/src/api/global.rs index 208a72fc..3bd7bb5d 100644 --- a/tests/src/api/global.rs +++ b/tests/src/api/global.rs @@ -17,8 +17,9 @@ fn create_del_user_command() { assert_eq!(Ok(()), res); api::command("Bar").unwrap(); - let commands = - api::get_commands(&Default::default()).unwrap().collect::>(); + let commands = unsafe { api::get_commands(&Default::default()) } + .unwrap() + .collect::>(); assert!(commands.iter().any(|cmd| cmd.name == "Foo")); assert!(commands.iter().any(|cmd| cmd.name == "Bar")); @@ -289,7 +290,7 @@ fn user_command_with_count() { let opts = CreateCommandOpts::builder().count(32).build(); api::create_user_command("Foo", "echo 'foo'", &opts).unwrap(); - let res = api::get_commands(&Default::default()) + let res = unsafe { api::get_commands(&Default::default()) } .map(|cmds| cmds.collect::>()); assert!(res.is_ok(), "{res:?}");