Skip to content

Commit 4613cbd

Browse files
committed
fix: use the luau normalizer on both requirers and add tests
1 parent 2e79d2e commit 4613cbd

1 file changed

Lines changed: 110 additions & 25 deletions

File tree

crates/rootbeer-core/src/lua/require.rs

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@ use mlua::{Function, Lua, NavigateError, Require, Result, TextRequirer};
22
use std::io;
33
use 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

Comments
 (0)