mz_deploy/lsp/
document_symbol.rs1use crate::project::compiler::cache::ProjectCache;
35use crate::project::ir::object_id::ObjectId;
36use std::path::Path;
37use tower_lsp::lsp_types::{DocumentSymbol, Range, SymbolKind, Url};
38
39use super::symbol_kind::object_kind_to_symbol_kind;
40
41#[allow(deprecated)] pub(super) fn document_symbols(
48 file_uri: &Url,
49 root: &Path,
50 project_cache: &ProjectCache,
51) -> Vec<DocumentSymbol> {
52 let (default_db, default_schema) = match ObjectId::default_db_schema_from_uri(file_uri, root) {
53 Some(pair) => pair,
54 None => return Vec::new(),
55 };
56
57 let file_path = match file_uri.to_file_path() {
58 Ok(p) => p,
59 Err(_) => return Vec::new(),
60 };
61 let object_name = match file_path.file_stem().and_then(|s| s.to_str()) {
62 Some(name) => name.to_string(),
63 None => return Vec::new(),
64 };
65
66 let id = ObjectId::new(default_db, default_schema, object_name);
67 let fqn = id.to_string();
68 let cached_obj = match project_cache.get_object(&id) {
69 Some(o) => o,
70 None => return Vec::new(),
71 };
72
73 let kind = object_kind_to_symbol_kind(cached_obj.kind);
74
75 let mut children = Vec::new();
76
77 for idx in &cached_obj.indexes {
79 let name = if idx.name.is_empty() {
80 "index".to_string()
81 } else {
82 idx.name.clone()
83 };
84 children.push(child_symbol(format!("INDEX {name}"), SymbolKind::KEY));
85 }
86
87 for g in &cached_obj.grants {
89 children.push(child_symbol(
90 format!("GRANT {} TO {}", g.privilege, g.grantee),
91 SymbolKind::EVENT,
92 ));
93 }
94
95 for c in &cached_obj.comments {
97 let label = if let Some(col) = &c.target_column {
98 format!("COMMENT ON COLUMN {col}")
99 } else {
100 "COMMENT".to_string()
101 };
102 children.push(child_symbol(label, SymbolKind::STRING));
103 }
104
105 let tests = project_cache.get_tests(&id);
107 for t in &tests {
108 children.push(child_symbol(format!("TEST {}", t.name), SymbolKind::METHOD));
109 }
110
111 vec![DocumentSymbol {
112 name: fqn,
113 detail: Some(format!("{}", cached_obj.kind)),
114 kind,
115 tags: None,
116 deprecated: None,
117 range: Range::default(),
118 selection_range: Range::default(),
119 children: if children.is_empty() {
120 None
121 } else {
122 Some(children)
123 },
124 }]
125}
126
127#[allow(deprecated)] fn child_symbol(name: String, kind: SymbolKind) -> DocumentSymbol {
130 DocumentSymbol {
131 name,
132 detail: None,
133 kind,
134 tags: None,
135 deprecated: None,
136 range: Range::default(),
137 selection_range: Range::default(),
138 children: None,
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::path::Path;
146
147 #[cfg_attr(miri, ignore)] #[mz_ore::test]
149 fn simple_view_single_root_symbol() {
150 let (root, cache) = build_test_cache("CREATE VIEW foo AS SELECT 1 AS id;");
151 let file_uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
152
153 let symbols = document_symbols(&file_uri, root.path(), &cache);
154 assert_eq!(symbols.len(), 1);
155 assert_eq!(symbols[0].name, "mydb.public.foo");
156 assert_eq!(symbols[0].detail.as_deref(), Some("view"));
157 assert!(symbols[0].children.is_none());
158 }
159
160 #[cfg_attr(miri, ignore)] #[mz_ore::test]
162 fn view_with_index_and_comment() {
163 let (root, cache) = build_test_cache(
164 "CREATE VIEW foo AS SELECT 1 AS id;\n\
165 CREATE DEFAULT INDEX IN CLUSTER default ON foo;\n\
166 COMMENT ON VIEW foo IS 'A test view';",
167 );
168 let file_uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
169
170 let symbols = document_symbols(&file_uri, root.path(), &cache);
171 assert_eq!(symbols.len(), 1);
172 let children = symbols[0].children.as_ref().unwrap();
173 assert_eq!(children.len(), 2);
174 assert!(children[0].name.contains("INDEX"));
176 assert_eq!(children[0].kind, SymbolKind::KEY);
177 assert_eq!(children[1].name, "COMMENT");
179 assert_eq!(children[1].kind, SymbolKind::STRING);
180 }
181
182 #[cfg_attr(miri, ignore)] #[mz_ore::test]
184 fn unknown_file_returns_empty() {
185 let (root, cache) = build_test_cache("CREATE VIEW foo AS SELECT 1 AS id;");
186 let file_uri =
187 Url::from_file_path(root.path().join("models/mydb/public/unknown.sql")).unwrap();
188
189 let symbols = document_symbols(&file_uri, root.path(), &cache);
190 assert!(symbols.is_empty());
191 }
192
193 fn build_test_cache(foo_sql: &str) -> (tempfile::TempDir, ProjectCache) {
194 let root = tempfile::tempdir().unwrap();
195 let models = root.path().join("models/mydb/public");
196 std::fs::create_dir_all(&models).unwrap();
197 std::fs::write(models.join("foo.sql"), foo_sql).unwrap();
198 write_project_toml(root.path());
199
200 let _project = crate::project::plan_sync(
201 &crate::fs::FileSystem::new(),
202 root.path(),
203 None,
204 None,
205 &Default::default(),
206 )
207 .expect("project should compile");
208 let cache = ProjectCache::open(root.path(), "", None, &Default::default())
209 .expect("cache should open")
210 .expect("cache DB should exist");
211 (root, cache)
212 }
213
214 fn write_project_toml(root: &Path) {
215 std::fs::write(root.join("project.toml"), "[project]\nname = \"test\"\n").unwrap();
216 }
217}