oxi_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Ident, Span};
3use quote::quote;
4use syn::{parse_macro_input, DeriveInput};
5
6mod derive_opts;
7
8/// TODO: docs
9#[proc_macro_derive(OptsBuilder, attributes(builder))]
10pub fn derive_opts_builder(input: TokenStream) -> TokenStream {
11    let input = parse_macro_input!(input as DeriveInput);
12    derive_opts::expand_derive_opts_builder(&input)
13        .unwrap_or_else(syn::Error::into_compile_error)
14        .into()
15}
16
17// *Heavily* inspired by mlua's `lua_module` proc macro.
18//
19/// Marks the plugin entrypoint.
20///
21/// # Examples
22///
23/// ```ignore
24/// use nvim_oxi as nvim;
25///
26/// #[nvim::module]
27/// fn foo() -> nvim::Result<()> {
28///     Ok(())
29/// }
30/// ```
31#[cfg(feature = "module")]
32#[proc_macro_attribute]
33pub fn oxi_module(_attr: TokenStream, item: TokenStream) -> TokenStream {
34    let item = parse_macro_input!(item as syn::ItemFn);
35
36    #[allow(clippy::redundant_clone)]
37    let module_name = item.sig.ident.clone();
38
39    let lua_module =
40        Ident::new(&format!("luaopen_{module_name}"), Span::call_site());
41
42    let module_body = quote! {
43        #item
44
45        #[no_mangle]
46        unsafe extern "C" fn #lua_module(
47            state: *mut ::nvim_oxi::lua::ffi::lua_State,
48        ) -> ::std::ffi::c_int {
49            ::nvim_oxi::entrypoint(state, #module_name)
50        }
51    };
52
53    module_body.into()
54}
55
56/// Tests a piece of code inside a Neovim session.
57///
58/// # Examples
59///
60/// ```ignore
61/// use nvim_oxi::{self as nvim, api};
62///
63/// #[nvim::test]
64/// fn set_get_del_var() {
65///     api::set_var("foo", 42).unwrap();
66///     assert_eq!(Ok(42), api::get_var("foo"));
67///     assert_eq!(Ok(()), api::del_var("foo"));
68/// }
69/// ```
70#[cfg(feature = "test")]
71#[proc_macro_attribute]
72pub fn oxi_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
73    let item = parse_macro_input!(item as syn::ItemFn);
74
75    let syn::ItemFn { sig, block, .. } = item;
76
77    // TODO: here we'd need to append something like the module path of the
78    // call site to `test_name` to avoid collisions between equally named tests
79    // across different modules. Unfortunately that doesn't seem to be possible
80    // yet?
81    // See https://www.reddit.com/r/rust/comments/a3fgp6/procmacro_determining_the_callers_module_path/
82    let test_name = sig.ident;
83    let test_body = block;
84
85    let module_name = Ident::new(&format!("__{test_name}"), Span::call_site());
86
87    quote! {
88        #[test]
89        fn #test_name() {
90            let mut library_filename = String::new();
91            library_filename.push_str(::std::env::consts::DLL_PREFIX);
92            library_filename.push_str(env!("CARGO_CRATE_NAME"));
93            library_filename.push_str(::std::env::consts::DLL_SUFFIX);
94
95            let mut target_filename = String::from("__");
96            target_filename.push_str(stringify!(#test_name));
97
98            #[cfg(not(target_os = "macos"))]
99            target_filename.push_str(::std::env::consts::DLL_SUFFIX);
100
101            #[cfg(target_os = "macos")]
102            target_filename.push_str(".so");
103
104            let manifest_dir = env!("CARGO_MANIFEST_DIR");
105            let target_dir = nvim_oxi::__test::get_target_dir(manifest_dir.as_ref()).join("debug");
106
107            let library_filepath = target_dir.join(library_filename);
108
109            if !library_filepath.exists() {
110                panic!(
111                    "Compiled library not found in '{}'. Please run `cargo \
112                     build` before running the tests.",
113                    library_filepath.display()
114                )
115            }
116
117            let target_filepath =
118                target_dir.join("oxi-test").join("lua").join(target_filename);
119
120            if !target_filepath.parent().unwrap().exists() {
121                if let Err(err) = ::std::fs::create_dir_all(
122                    target_filepath.parent().unwrap(),
123                ) {
124                    // It might happen that another test created the `lua`
125                    // directory between the first if and the `create_dir_all`.
126                    if !matches!(
127                        err.kind(),
128                        ::std::io::ErrorKind::AlreadyExists
129                    ) {
130                        panic!("{}", err)
131                    }
132                }
133            }
134
135            #[cfg(unix)]
136            let res = ::std::os::unix::fs::symlink(
137                &library_filepath,
138                &target_filepath,
139            );
140
141            #[cfg(windows)]
142            let res = ::std::os::windows::fs::symlink_file(
143                &library_filepath,
144                &target_filepath,
145            );
146
147            if let Err(err) = res {
148                if !matches!(err.kind(), ::std::io::ErrorKind::AlreadyExists) {
149                    panic!("{}", err)
150                }
151            }
152
153            let out = ::std::process::Command::new("nvim")
154                .args(["-u", "NONE", "--headless"])
155                .args(["-c", "set noswapfile"])
156                .args([
157                    "-c",
158                    &format!(
159                        "set rtp+={}",
160                        target_dir.join("oxi-test").display()
161                    ),
162                ])
163                .args([
164                    "-c",
165                    &format!("lua require('__{}')", stringify!(#test_name)),
166                ])
167                .args(["+quit"])
168                .output()
169                .expect("Couldn't find `nvim` binary in $PATH");
170
171            if out.status.success() {
172                return;
173            }
174
175            let stderr = String::from_utf8_lossy(&out.stderr);
176
177            if !stderr.is_empty() {
178                // Remove the last 2 lines from stderr for a cleaner error msg.
179                let stderr = {
180                    let lines = stderr.lines().collect::<Vec<_>>();
181                    let len = lines.len();
182                    lines[..lines.len() - 2].join("\n")
183                };
184
185                // The first 31 bytes are `thread '<unnamed>' panicked at `.
186                let (_, stderr) = stderr.split_at(31);
187
188                panic!("{}", stderr)
189            } else if let Some(code) = out.status.code() {
190                panic!("Neovim exited with non-zero exit code: {}", code);
191            } else {
192                panic!("Neovim segfaulted");
193            }
194        }
195
196        #[::nvim_oxi::module]
197        fn #module_name() -> ::nvim_oxi::Result<()> {
198            let result = ::std::panic::catch_unwind(|| {
199                #test_body
200            });
201
202            ::std::process::exit(match result {
203                Ok(_) => 0,
204
205                Err(err) => {
206                    eprintln!("{:?}", err);
207                    1
208                },
209            })
210        }
211    }
212    .into()
213}