1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::hover;
5use crate::reference::Reference;
6use crate::workspace::Workspace;
7
8const MODULE_EXTENSIONS: [&str; 3] = ["js", "json", "ds"];
9const TEMPLATE_DIRS: [&str; 1] = ["default"];
11
12pub struct Hit {
13 pub path: PathBuf,
14 pub line: u32,
16}
17
18impl From<PathBuf> for Hit {
19 fn from(path: PathBuf) -> Self {
20 Hit { path, line: 0 }
21 }
22}
23
24pub fn resolve(reference: &Reference, from: &Path, workspace: &Workspace) -> Vec<Hit> {
26 match reference {
27 Reference::Template(path) => templates(path, from, workspace),
28 Reference::Module(path) => modules(path, from, workspace),
29 Reference::Resource { key, bundle } => resources(key, bundle, from, workspace),
30 Reference::Route { .. } => routes(reference, from, workspace),
31 }
32}
33
34fn routes(reference: &Reference, from: &Path, workspace: &Workspace) -> Vec<Hit> {
36 let Some(route) = hover::route_of(reference, from) else {
37 return Vec::new();
38 };
39 hover::targets(&hover::chains(&route, workspace))
40 .into_iter()
41 .map(|(path, line)| Hit { path, line })
42 .collect()
43}
44
45fn templates(template: &str, from: &Path, workspace: &Workspace) -> Vec<Hit> {
46 let relative = format!("{}.isml", template.trim_start_matches('/'));
47 let mut hits = Vec::new();
48 for cartridge in workspace.cartridges_from(from) {
49 let templates_dir = cartridge.cartridge_dir().join("templates");
50 for locale in template_dirs(&templates_dir) {
51 let candidate = templates_dir.join(locale).join(&relative);
52 if candidate.is_file() {
53 hits.push(candidate.into());
54 }
55 }
56 }
57 hits
58}
59
60fn template_dirs(templates_dir: &Path) -> Vec<String> {
62 let mut dirs: Vec<String> = TEMPLATE_DIRS.iter().map(|dir| dir.to_string()).collect();
63 let Ok(entries) = fs::read_dir(templates_dir) else {
64 return dirs;
65 };
66 let mut others: Vec<String> = entries
67 .flatten()
68 .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_dir()))
69 .filter_map(|entry| entry.file_name().to_str().map(str::to_string))
70 .filter(|name| !dirs.contains(name) && name != "resources")
71 .collect();
72 others.sort();
73 dirs.extend(others);
74 dirs
75}
76
77fn modules(module: &str, from: &Path, workspace: &Workspace) -> Vec<Hit> {
78 if module.starts_with("dw/") || module.starts_with("dw.") {
79 return Vec::new();
80 }
81
82 if module.starts_with("./") || module.starts_with("../") {
83 let base = from.parent().unwrap_or(from);
84 return existing_module(&base.join(module.replace('/', std::path::MAIN_SEPARATOR_STR)))
85 .into_iter()
86 .map(Hit::from)
87 .collect();
88 }
89
90 let mut hits = Vec::new();
91
92 if let Some(rest) = module.strip_prefix("*/") {
93 for cartridge in workspace.cartridges_from(from) {
94 push_module(&mut hits, &cartridge.root.join(rest));
95 }
96 } else if let Some(rest) = module.strip_prefix("~/") {
97 if let Some(cartridge) = workspace.cartridge_of(from) {
98 push_module(&mut hits, &cartridge.root.join(rest));
99 }
100 } else if let Some((name, rest)) = module.split_once('/') {
101 for cartridge in workspace.cartridges_from(from) {
103 if cartridge.name == name {
104 push_module(&mut hits, &cartridge.root.join(rest));
105 }
106 }
107 }
108
109 if hits.is_empty() {
112 for module_root in &workspace.module_roots {
113 push_module(&mut hits, &module_root.join(module));
114 }
115 }
116
117 hits
118}
119
120fn push_module(hits: &mut Vec<Hit>, candidate: &Path) {
121 if let Some(path) = existing_module(candidate) {
122 hits.push(path.into());
123 }
124}
125
126fn existing_module(candidate: &Path) -> Option<PathBuf> {
128 if candidate.is_file() {
129 return Some(candidate.to_path_buf());
130 }
131 for extension in MODULE_EXTENSIONS {
132 let with_extension = PathBuf::from(format!("{}.{extension}", candidate.display()));
133 if with_extension.is_file() {
134 return Some(with_extension);
135 }
136 }
137 let index = candidate.join("index.js");
138 index.is_file().then_some(index)
139}
140
141fn resources(key: &str, bundle: &str, from: &Path, workspace: &Workspace) -> Vec<Hit> {
142 let file_name = format!("{bundle}.properties");
143 let bundles: Vec<PathBuf> = workspace
144 .cartridges_from(from)
145 .into_iter()
146 .map(|cartridge| {
147 cartridge
148 .cartridge_dir()
149 .join("templates")
150 .join("resources")
151 .join(&file_name)
152 })
153 .filter(|path| path.is_file())
154 .collect();
155
156 let defining: Vec<Hit> = bundles
157 .iter()
158 .filter_map(|path| {
159 line_of_key(path, key).map(|line| Hit {
160 path: path.clone(),
161 line,
162 })
163 })
164 .collect();
165
166 if defining.is_empty() {
169 return bundles.into_iter().map(Hit::from).collect();
170 }
171 defining
172}
173
174fn line_of_key(path: &Path, key: &str) -> Option<u32> {
175 let contents = fs::read_to_string(path).ok()?;
176 contents
177 .lines()
178 .position(|line| defines_key(line, key))
179 .map(|index| index as u32)
180}
181
182fn defines_key(line: &str, key: &str) -> bool {
183 let trimmed = line.trim_start();
184 trimmed
185 .strip_prefix(key)
186 .is_some_and(|rest| rest.trim_start().starts_with('='))
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn matches_only_a_whole_key() {
195 assert!(defines_key("label.a.b=Value", "label.a.b"));
196 assert!(defines_key(" label.a.b = Value", "label.a.b"));
197 assert!(!defines_key("label.a.bc=Value", "label.a.b"));
198 assert!(!defines_key("#label.a.b=Value", "label.a.b"));
199 }
200}