use std::collections::HashMap;
use std::path::Path;
use tower_lsp_server::ls_types::{CompletionItem, CompletionItemKind, Location, Position, Uri};
use super::translation_index::find_json_key_range;
#[derive(Debug, Default, Clone)]
pub struct MixIndex {
manifest: HashMap<String, Location>,
}
impl MixIndex {
pub fn get(&self, path: &str) -> Option<&Location> {
self.manifest.get(path.strip_prefix('/').unwrap_or(path))
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.manifest.keys().map(String::as_str)
}
pub fn key_at(&self, uri: &Uri, position: Position) -> Option<&str> {
crate::laravel::location_lookup::key_at(&self.manifest, uri, position)
}
pub(super) fn load(root: &Path) -> Self {
let mut manifest = HashMap::new();
load_manifest(
&root.join("public").join("mix-manifest.json"),
&mut manifest,
);
Self { manifest }
}
}
fn load_manifest(path: &Path, out: &mut HashMap<String, Location>) {
let Ok(text) = std::fs::read_to_string(path) else {
return;
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
return;
};
let Some(map) = json.as_object() else {
return;
};
let Some(uri) = Uri::from_file_path(path) else {
return;
};
for key in map.keys() {
let bare = key.strip_prefix('/').unwrap_or(key);
if out.contains_key(bare) {
continue;
}
if let Some(range) = find_json_key_range(&text, key) {
out.insert(
bare.to_string(),
Location {
uri: uri.clone(),
range,
},
);
}
}
}
pub(crate) fn mix_completions(index: &MixIndex, prefix: &str) -> Vec<CompletionItem> {
let mut items: Vec<CompletionItem> = index
.names()
.filter(|name| name.starts_with(prefix))
.map(|name| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::FILE),
insert_text: Some(name.to_string()),
..Default::default()
})
.collect();
items.sort_by(|a, b| a.label.cmp(&b.label));
items
}
#[cfg(test)]
mod tests {
use super::*;
fn write_manifest(root: &Path, contents: &str) {
let public = root.join("public");
std::fs::create_dir_all(&public).unwrap();
std::fs::write(public.join("mix-manifest.json"), contents).unwrap();
}
#[test]
fn resolves_bare_path_against_leading_slash_key() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(tmp.path(), r#"{"/js/app.js": "/js/app.js?id=abc123"}"#);
let idx = MixIndex::load(tmp.path());
let loc = idx.get("js/app.js").unwrap();
assert!(loc.uri.as_str().ends_with("mix-manifest.json"));
assert_eq!(idx.get("/js/app.js").unwrap().range, loc.range);
}
#[test]
fn location_points_at_the_manifest_key_text() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(tmp.path(), r#"{"/js/app.js": "/js/app.js?id=abc123"}"#);
let idx = MixIndex::load(tmp.path());
let loc = idx.get("js/app.js").unwrap();
assert_eq!((loc.range.start.line, loc.range.start.character), (0, 2));
assert_eq!(loc.range.end.character, 12);
}
#[test]
fn unknown_path_resolves_to_none() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(tmp.path(), r#"{"/js/app.js": "/js/app.js"}"#);
let idx = MixIndex::load(tmp.path());
assert!(idx.get("js/missing.js").is_none());
}
#[test]
fn missing_manifest_yields_empty_index() {
let tmp = tempfile::tempdir().unwrap();
let idx = MixIndex::load(tmp.path());
assert_eq!(idx.names().count(), 0);
}
#[test]
fn mix_completions_filters_by_prefix_and_sorts() {
let tmp = tempfile::tempdir().unwrap();
write_manifest(
tmp.path(),
r#"{"/css/app.css": "/css/app.css?id=1", "/css/admin.css": "/css/admin.css?id=2", "/js/app.js": "/js/app.js?id=3"}"#,
);
let idx = MixIndex::load(tmp.path());
let items = mix_completions(&idx, "css/");
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert_eq!(labels, vec!["css/admin.css", "css/app.css"]);
}
}