Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/api/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<impl SuperIterator<CommandInfos> + use<>> {
let mut err = nvim::Error::new();
Expand Down
7 changes: 4 additions & 3 deletions crates/api/src/opts/create_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::Buffer;
use crate::types::{
CommandAddr,
CommandArgs,
CommandComplete,
CommandCompleteOrFunction,
CommandNArgs,
CommandRange,
};
Expand All @@ -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,

Expand Down
115 changes: 108 additions & 7 deletions crates/api/src/types/command_complete.rs
Original file line number Diff line number Diff line change
@@ -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<String>;
pub type CompleteCallbackFunc =
Function<CompleteCallbackArgs, CompleteCallbackRet>;

/// 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,
Expand All @@ -30,6 +38,7 @@ pub enum CommandComplete {
Help,
Highlight,
History,
Keymap,
Locale,
Lua,
Mapclear,
Expand All @@ -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,
Expand All @@ -48,11 +62,98 @@ pub enum CommandComplete {
Var,

/// See `:h command-completion-customlist` for details.
CustomList(Function<(String, String, usize), Vec<String>>),
/// *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 {
fn to_object(self) -> Result<Object, conversion::Error> {
self.serialize(Serializer::new()).map_err(Into::into)
}
}

impl FromObject for CommandComplete {
fn from_object(obj: Object) -> Result<Self, conversion::Error> {
Self::deserialize(Deserializer::new(obj)).map_err(Into::into)
}
}

pub trait CommandCompleteOrFunction {
fn to_object(self) -> Object;
}

impl<T> CommandCompleteOrFunction for T
where
T: ToFunction<CompleteCallbackArgs, CompleteCallbackRet>,
{
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<CompleteCallbackFunc, D::Error>
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<S>(
vim_fn_name: &str,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(&[$variant, vim_fn_name].join(","))
}

fn $de_fn_name<'de, D>(deserializer: D) -> Result<String, D::Error>
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"}
7 changes: 6 additions & 1 deletion crates/api/src/types/command_infos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -26,7 +28,10 @@ pub struct CommandInfos {
/// Callback triggered by the command.
pub callback: Option<Function<CommandArgs, ()>>,

/// Command complletion strategy.
/// Command completion strategy.
#[cfg(feature = "neovim-0-12")] // on 0.12 and Nightly.
pub complete: Option<CommandComplete>,
#[cfg(not(feature = "neovim-0-12"))] // Only on 0.11
pub complete: Option<String>,

/// TODO: docs
Expand Down
46 changes: 46 additions & 0 deletions tests/src/api/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
}
7 changes: 4 additions & 3 deletions tests/src/api/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();
let commands = unsafe { api::get_commands(&Default::default()) }
.unwrap()
.collect::<Vec<_>>();

assert!(commands.iter().any(|cmd| cmd.name == "Foo"));
assert!(commands.iter().any(|cmd| cmd.name == "Bar"));
Expand Down Expand Up @@ -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::<Vec<_>>());

assert!(res.is_ok(), "{res:?}");
Expand Down
Loading