@@ -2,6 +2,36 @@ use mlua::{Function, Lua, NavigateError, Require, Result, TextRequirer};
22use std:: io;
33use std:: path:: { Path , PathBuf } ;
44
5+ /// Normalize a chunk name for TextRequirer's `reset()`.
6+ ///
7+ /// TextRequirer probes `.lua`/`.luau` extensions automatically. If the chunk
8+ /// name already carries one (e.g. `@.../init.lua`) it would try `init.lua.lua`
9+ /// which doesn't exist. Additionally, Luau treats bare `init` as a directory
10+ /// marker — `resolve_module` only checks if the path *is* a directory, which
11+ /// fails for a bare file stem. We use the parent directory instead so that
12+ /// `resolve_module` finds `init.lua` inside it and relative requires resolve
13+ /// correctly.
14+ fn normalize_chunk_name ( chunk_name : & str ) -> Option < String > {
15+ let rest = chunk_name. strip_prefix ( '@' ) ?;
16+ let path = Path :: new ( rest) ;
17+ let ext = path. extension ( ) . and_then ( |e| e. to_str ( ) ) ;
18+
19+ if ext == Some ( "lua" ) || ext == Some ( "luau" ) {
20+ let stripped = path. with_extension ( "" ) ;
21+ let stem = stripped. file_name ( ) . and_then ( |s| s. to_str ( ) ) ;
22+
23+ if stem == Some ( "init" ) {
24+ if let Some ( parent) = stripped. parent ( ) {
25+ return Some ( format ! ( "@{}" , parent. display( ) ) ) ;
26+ }
27+ }
28+
29+ return Some ( format ! ( "@{}" , stripped. display( ) ) ) ;
30+ }
31+
32+ None
33+ }
34+
535/// A custom Require implementation that wraps TextRequirer and injects
636/// a synthetic `@rootbeer` alias pointing to `<lua_dir>/rootbeer`.
737/// This avoids needing a `.luaurc` file on disk.
@@ -36,31 +66,8 @@ impl Require for FsRequirer {
3666 }
3767
3868 fn reset ( & mut self , chunk_name : & str ) -> std:: result:: Result < ( ) , NavigateError > {
39- // TextRequirer probes `.lua`/`.luau` extensions automatically. If the
40- // chunk name already contains one (e.g. `@.../init.lua`), it would try
41- // `init.lua.lua` which doesn't exist. Strip the extension before
42- // delegating so the probe finds the correct file.
43- //
44- // Luau treats bare `init` as a directory marker — resolve_module only
45- // checks if the path is a directory, which fails for a file stem. Use
46- // the parent directory instead: resolve_module will find `init.lua`
47- // inside it, and relative requires (`./foo`) resolve correctly.
48- if let Some ( rest) = chunk_name. strip_prefix ( '@' ) {
49- let path = Path :: new ( rest) ;
50- let ext = path. extension ( ) . and_then ( |e| e. to_str ( ) ) ;
51-
52- if ext == Some ( "lua" ) || ext == Some ( "luau" ) {
53- let stripped = path. with_extension ( "" ) ;
54- let stem = stripped. file_name ( ) . and_then ( |s| s. to_str ( ) ) ;
55-
56- if stem == Some ( "init" ) {
57- if let Some ( parent) = stripped. parent ( ) {
58- return self . inner . reset ( & format ! ( "@{}" , parent. display( ) ) ) ;
59- }
60- }
61-
62- return self . inner . reset ( & format ! ( "@{}" , stripped. display( ) ) ) ;
63- }
69+ if let Some ( normalized) = normalize_chunk_name ( chunk_name) {
70+ return self . inner . reset ( & normalized) ;
6471 }
6572 self . inner . reset ( chunk_name)
6673 }
@@ -152,6 +159,10 @@ impl Require for EmbeddedRequirer {
152159 fn reset ( & mut self , chunk_name : & str ) -> std:: result:: Result < ( ) , NavigateError > {
153160 self . path . clear ( ) ;
154161 self . in_alias = false ;
162+
163+ if let Some ( normalized) = normalize_chunk_name ( chunk_name) {
164+ return self . inner . reset ( & normalized) ;
165+ }
155166 self . inner . reset ( chunk_name)
156167 }
157168
@@ -231,3 +242,77 @@ impl Require for EmbeddedRequirer {
231242 }
232243 }
233244}
245+
246+ #[ cfg( test) ]
247+ mod tests {
248+ use super :: * ;
249+
250+ #[ test]
251+ fn strips_lua_extension ( ) {
252+ assert_eq ! (
253+ normalize_chunk_name( "@/home/user/.config/rootbeer/modules/git.lua" ) ,
254+ Some ( "@/home/user/.config/rootbeer/modules/git" . into( ) ) ,
255+ ) ;
256+ }
257+
258+ #[ test]
259+ fn strips_luau_extension ( ) {
260+ assert_eq ! (
261+ normalize_chunk_name( "@/home/user/.config/rootbeer/modules/git.luau" ) ,
262+ Some ( "@/home/user/.config/rootbeer/modules/git" . into( ) ) ,
263+ ) ;
264+ }
265+
266+ #[ test]
267+ fn init_lua_resolves_to_parent ( ) {
268+ assert_eq ! (
269+ normalize_chunk_name( "@/home/user/.config/rootbeer/modules/nvim/init.lua" ) ,
270+ Some ( "@/home/user/.config/rootbeer/modules/nvim" . into( ) ) ,
271+ ) ;
272+ }
273+
274+ #[ test]
275+ fn init_luau_resolves_to_parent ( ) {
276+ assert_eq ! (
277+ normalize_chunk_name( "@/home/user/.config/rootbeer/modules/nvim/init.luau" ) ,
278+ Some ( "@/home/user/.config/rootbeer/modules/nvim" . into( ) ) ,
279+ ) ;
280+ }
281+
282+ #[ test]
283+ fn no_extension_passthrough ( ) {
284+ assert_eq ! (
285+ normalize_chunk_name( "@/home/user/.config/rootbeer/modules/git" ) ,
286+ None ,
287+ ) ;
288+ }
289+
290+ #[ test]
291+ fn non_lua_extension_passthrough ( ) {
292+ assert_eq ! (
293+ normalize_chunk_name( "@/home/user/.config/rootbeer/data.json" ) ,
294+ None ,
295+ ) ;
296+ }
297+
298+ #[ test]
299+ fn no_at_prefix_returns_none ( ) {
300+ assert_eq ! ( normalize_chunk_name( "modules/git.lua" ) , None ) ;
301+ }
302+
303+ #[ test]
304+ fn rootbeer_alias_path_with_extension ( ) {
305+ assert_eq ! (
306+ normalize_chunk_name( "@rootbeer/git.lua" ) ,
307+ Some ( "@rootbeer/git" . into( ) ) ,
308+ ) ;
309+ }
310+
311+ #[ test]
312+ fn source_alias_init ( ) {
313+ assert_eq ! (
314+ normalize_chunk_name( "@source/modules/nvim/init.lua" ) ,
315+ Some ( "@source/modules/nvim" . into( ) ) ,
316+ ) ;
317+ }
318+ }
0 commit comments