1use crate::project::compiler::cache::ProjectCache;
22use crate::project::ir::object_id::ObjectId;
23use crate::types::ObjectKind;
24use std::path::Path;
25use tower_lsp::lsp_types::*;
26
27pub(super) fn code_lenses(
32 file_uri: &Url,
33 file_text: &str,
34 root: &Path,
35 project_cache: Option<&ProjectCache>,
36) -> Vec<CodeLens> {
37 let project_cache = match project_cache {
38 Some(c) => c,
39 None => return Vec::new(),
40 };
41
42 let (default_db, default_schema) = match ObjectId::default_db_schema_from_uri(file_uri, root) {
43 Some(pair) => pair,
44 None => return Vec::new(),
45 };
46
47 let object_name = file_uri
48 .to_file_path()
49 .ok()
50 .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()));
51 let object_name = match object_name {
52 Some(name) => name,
53 None => return Vec::new(),
54 };
55
56 let file_object_id = ObjectId::new(default_db.clone(), default_schema.clone(), object_name);
57 let fqn = file_object_id.to_string();
58
59 let cached_obj = match project_cache.get_object(&file_object_id) {
60 Some(obj) => obj,
61 None => return Vec::new(),
62 };
63
64 let mut lenses = Vec::new();
65
66 if cached_obj.kind == ObjectKind::MaterializedView {
68 if let Some(line) = find_statement_line(file_text, "create materialized view") {
69 lenses.push(CodeLens {
70 range: Range::new(Position::new(line, 0), Position::new(line, 0)),
71 command: Some(Command {
72 title: "\u{25b6} Explain".to_string(),
73 command: "mz-deploy.runExplain".to_string(),
74 arguments: Some(vec![serde_json::Value::String(fqn.clone())]),
75 }),
76 data: None,
77 });
78 }
79 }
80
81 for index in &cached_obj.indexes {
83 if index.name.is_empty() {
84 continue;
85 }
86 let index_name = &index.name;
87 if let Some(line) = find_index_line(file_text, index_name) {
88 let target = format!("{}#{}", fqn, index_name);
89 lenses.push(CodeLens {
90 range: Range::new(Position::new(line, 0), Position::new(line, 0)),
91 command: Some(Command {
92 title: "\u{25b6} Explain".to_string(),
93 command: "mz-deploy.runExplain".to_string(),
94 arguments: Some(vec![serde_json::Value::String(target)]),
95 }),
96 data: None,
97 });
98 }
99 }
100
101 let tests = project_cache.get_tests(&file_object_id);
103 for test in &tests {
104 let test_name = &test.name;
105 if let Some(line) = find_test_line(file_text, test_name) {
106 let filter = format!("{}#{}", fqn, test_name);
107 lenses.push(CodeLens {
108 range: Range::new(Position::new(line, 0), Position::new(line, 0)),
109 command: Some(Command {
110 title: "\u{25b6} Run Test".to_string(),
111 command: "mz-deploy.runTest".to_string(),
112 arguments: Some(vec![serde_json::Value::String(filter)]),
113 }),
114 data: None,
115 });
116 }
117 }
118
119 lenses
120}
121
122fn find_statement_line(file_text: &str, keyword: &str) -> Option<u32> {
127 for (i, line) in file_text.lines().enumerate() {
128 if line.trim().to_lowercase().starts_with(keyword) {
129 return u32::try_from(i).ok();
130 }
131 }
132 None
133}
134
135fn find_index_line(file_text: &str, index_name: &str) -> Option<u32> {
137 let target = format!("create index {}", index_name);
138 let target_lower = target.to_lowercase();
139
140 for (i, line) in file_text.lines().enumerate() {
141 if line.trim().to_lowercase().starts_with(&target_lower) {
142 return u32::try_from(i).ok();
143 }
144 }
145 None
146}
147
148fn find_test_line(file_text: &str, test_name: &str) -> Option<u32> {
150 let target = format!("execute unit test {}", test_name);
151 let target_lower = target.to_lowercase();
152
153 for (i, line) in file_text.lines().enumerate() {
154 let trimmed = line.trim().to_lowercase();
155 if trimmed.starts_with(&target_lower) {
156 return u32::try_from(i).ok();
157 }
158 }
159 None
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn write_project_toml(root: &Path) {
167 std::fs::write(root.join("project.toml"), "[project]\nname = \"test\"\n").unwrap();
168 }
169
170 fn build_cache(root: &tempfile::TempDir) -> ProjectCache {
171 write_project_toml(root.path());
172 let _project = crate::project::plan_sync(
173 &crate::fs::FileSystem::new(),
174 root.path(),
175 None,
176 None,
177 &Default::default(),
178 )
179 .expect("project should compile");
180 ProjectCache::open(root.path(), "", None, &Default::default())
181 .expect("cache should open")
182 .expect("cache DB should exist")
183 }
184
185 #[mz_ore::test]
186 fn test_find_test_line_basic() {
187 let text = "CREATE VIEW foo AS SELECT 1;\n\nEXECUTE UNIT TEST my_test\nAS SELECT 1;\n";
188 assert_eq!(find_test_line(text, "my_test"), Some(2));
189 }
190
191 #[mz_ore::test]
192 fn test_find_test_line_case_insensitive() {
193 let text = "execute unit test my_test\nAS SELECT 1;\n";
194 assert_eq!(find_test_line(text, "my_test"), Some(0));
195 }
196
197 #[mz_ore::test]
198 fn test_find_test_line_not_found() {
199 let text = "CREATE VIEW foo AS SELECT 1;\n";
200 assert_eq!(find_test_line(text, "nonexistent"), None);
201 }
202
203 #[cfg_attr(miri, ignore)] #[mz_ore::test]
205 fn test_single_test() {
206 let root = tempfile::tempdir().unwrap();
207 let dir = root.path().join("models/mydb/public");
208 std::fs::create_dir_all(&dir).unwrap();
209 std::fs::write(
210 dir.join("foo.sql"),
211 "CREATE VIEW foo AS SELECT 1 AS id;\n\nEXECUTE UNIT TEST basic_test\nFOR mydb.public.foo\nEXPECTED(id integer) AS (\n SELECT 1\n);\n",
212 )
213 .unwrap();
214 let cache = build_cache(&root);
215
216 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
217 let file_text =
218 std::fs::read_to_string(root.path().join("models/mydb/public/foo.sql")).unwrap();
219 let lenses = code_lenses(&uri, &file_text, root.path(), Some(&cache));
220
221 assert_eq!(lenses.len(), 1);
222 let lens = &lenses[0];
223 assert_eq!(lens.range.start.line, 2);
224 let cmd = lens.command.as_ref().unwrap();
225 assert_eq!(cmd.command, "mz-deploy.runTest");
226 assert_eq!(
227 cmd.arguments.as_ref().unwrap()[0],
228 serde_json::Value::String("mydb.public.foo#basic_test".to_string())
229 );
230 }
231
232 #[cfg_attr(miri, ignore)] #[mz_ore::test]
234 fn test_multiple_tests() {
235 let root = tempfile::tempdir().unwrap();
236 let dir = root.path().join("models/mydb/public");
237 std::fs::create_dir_all(&dir).unwrap();
238 std::fs::write(
239 dir.join("foo.sql"),
240 "CREATE VIEW foo AS SELECT 1 AS id;\n\nEXECUTE UNIT TEST test_one\nFOR mydb.public.foo\nEXPECTED(id integer) AS (\n SELECT 1\n);\n\nEXECUTE UNIT TEST test_two\nFOR mydb.public.foo\nEXPECTED(id integer) AS (\n SELECT 1\n);\n",
241 )
242 .unwrap();
243 let cache = build_cache(&root);
244
245 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
246 let file_text =
247 std::fs::read_to_string(root.path().join("models/mydb/public/foo.sql")).unwrap();
248 let lenses = code_lenses(&uri, &file_text, root.path(), Some(&cache));
249
250 assert_eq!(lenses.len(), 2);
251
252 let filters: Vec<String> = lenses
253 .iter()
254 .filter_map(|l| {
255 l.command
256 .as_ref()
257 .and_then(|c| c.arguments.as_ref())
258 .and_then(|a| a[0].as_str().map(String::from))
259 })
260 .collect();
261 assert!(filters.contains(&"mydb.public.foo#test_one".to_string()));
262 assert!(filters.contains(&"mydb.public.foo#test_two".to_string()));
263
264 assert_ne!(lenses[0].range.start.line, lenses[1].range.start.line);
266 }
267
268 #[cfg_attr(miri, ignore)] #[mz_ore::test]
270 fn test_no_tests() {
271 let root = tempfile::tempdir().unwrap();
272 let dir = root.path().join("models/mydb/public");
273 std::fs::create_dir_all(&dir).unwrap();
274 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;\n").unwrap();
275 let cache = build_cache(&root);
276
277 let uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
278 let file_text =
279 std::fs::read_to_string(root.path().join("models/mydb/public/foo.sql")).unwrap();
280 let lenses = code_lenses(&uri, &file_text, root.path(), Some(&cache));
281
282 assert!(lenses.is_empty());
283 }
284
285 #[cfg_attr(miri, ignore)] #[mz_ore::test]
287 fn test_file_not_in_project() {
288 let root = tempfile::tempdir().unwrap();
289 let dir = root.path().join("models/mydb/public");
290 std::fs::create_dir_all(&dir).unwrap();
291 std::fs::write(dir.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;\n").unwrap();
292 let cache = build_cache(&root);
293
294 let outside = root.path().join("random/foo.sql");
296 std::fs::create_dir_all(root.path().join("random")).unwrap();
297 std::fs::write(&outside, "SELECT 1;").unwrap();
298 let uri = Url::from_file_path(&outside).unwrap();
299 let file_text = std::fs::read_to_string(&outside).unwrap();
300 let lenses = code_lenses(&uri, &file_text, root.path(), Some(&cache));
301
302 assert!(lenses.is_empty());
303 }
304}