Skip to main content

mz_deploy/lsp/
workspace_symbol.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Workspace symbol search across all project objects.
11//!
12//! Provides fuzzy-find over every object in the project, enabling editor
13//! commands like "Go to Symbol in Workspace" (`Ctrl+T` / `Cmd+T`). Each
14//! result carries the object name, kind, file location, and a container
15//! label (`database.schema`) for grouping context.
16//!
17//! ## Filtering
18//!
19//! - An empty query returns all project objects.
20//! - A non-empty query matches by case-insensitive substring on the
21//!   fully-qualified name (`database.schema.object`).
22//! - External dependencies are excluded (no file path).
23
24use crate::project::compiler::cache::ProjectCache;
25use std::path::Path;
26use tower_lsp::lsp_types::{Location, Range, SymbolInformation, Url};
27
28use super::symbol_kind::object_kind_to_symbol_kind;
29
30/// Search project objects by name.
31///
32/// Returns a [`SymbolInformation`] for each matching object. External
33/// dependencies are excluded.
34#[allow(deprecated)] // SymbolInformation::deprecated field is deprecated but required
35pub(super) fn workspace_symbols(
36    query: &str,
37    project_cache: &ProjectCache,
38    root: &Path,
39) -> Vec<SymbolInformation> {
40    let query_lower = query.to_lowercase();
41
42    project_cache
43        .list_objects()
44        .into_iter()
45        .filter(|summary| {
46            query_lower.is_empty() || summary.fqn.to_lowercase().contains(&query_lower)
47        })
48        .filter_map(|summary| {
49            let full_path = root.join(&summary.file_path);
50            let uri = Url::from_file_path(&full_path).ok()?;
51            let kind = object_kind_to_symbol_kind(summary.kind);
52            let container = format!("{}.{}", summary.database, summary.schema);
53
54            Some(SymbolInformation {
55                name: summary.name,
56                kind,
57                tags: None,
58                deprecated: None,
59                location: Location {
60                    uri,
61                    range: Range::default(),
62                },
63                container_name: Some(container),
64            })
65        })
66        .collect()
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::project::compiler::cache::ProjectCache;
73    use std::path::Path;
74
75    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
76    #[mz_ore::test]
77    fn empty_query_returns_all_objects() {
78        let (root, cache) = build_test_project_cache();
79        let symbols = workspace_symbols("", &cache, root.path());
80        assert_eq!(symbols.len(), 2); // foo and bar
81    }
82
83    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
84    #[mz_ore::test]
85    fn query_matches_by_substring() {
86        let (root, cache) = build_test_project_cache();
87        let symbols = workspace_symbols("foo", &cache, root.path());
88        assert_eq!(symbols.len(), 1);
89        assert_eq!(symbols[0].name, "foo");
90    }
91
92    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
93    #[mz_ore::test]
94    fn query_case_insensitive() {
95        let (root, cache) = build_test_project_cache();
96        let symbols = workspace_symbols("FOO", &cache, root.path());
97        assert_eq!(symbols.len(), 1);
98        assert_eq!(symbols[0].name, "foo");
99    }
100
101    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
102    #[mz_ore::test]
103    fn query_matches_schema_in_fqn() {
104        let (root, cache) = build_test_project_cache();
105        let symbols = workspace_symbols("public", &cache, root.path());
106        // Both "mydb.public.foo" and "mydb.public.bar" match "public"
107        assert_eq!(symbols.len(), 2);
108    }
109
110    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
111    #[mz_ore::test]
112    fn no_matches_returns_empty() {
113        let (root, cache) = build_test_project_cache();
114        let symbols = workspace_symbols("nonexistent", &cache, root.path());
115        assert!(symbols.is_empty());
116    }
117
118    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
119    #[mz_ore::test]
120    fn symbols_have_container_name() {
121        let (root, cache) = build_test_project_cache();
122        let symbols = workspace_symbols("foo", &cache, root.path());
123        assert_eq!(symbols[0].container_name.as_deref(), Some("mydb.public"));
124    }
125
126    fn build_test_project_cache() -> (tempfile::TempDir, ProjectCache) {
127        let root = tempfile::tempdir().unwrap();
128        let models = root.path().join("models/mydb/public");
129        std::fs::create_dir_all(&models).unwrap();
130        std::fs::write(models.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
131        std::fs::write(
132            models.join("bar.sql"),
133            "CREATE VIEW bar AS SELECT * FROM foo;",
134        )
135        .unwrap();
136        write_project_toml(root.path());
137
138        let _project = crate::project::plan_sync(
139            &crate::fs::FileSystem::new(),
140            root.path(),
141            None,
142            None,
143            &Default::default(),
144        )
145        .expect("project should compile");
146        let cache = ProjectCache::open(root.path(), "", None, &Default::default())
147            .expect("cache should open")
148            .expect("cache DB should exist");
149        (root, cache)
150    }
151
152    fn write_project_toml(root: &Path) {
153        std::fs::write(root.join("project.toml"), "[project]\nname = \"test\"\n").unwrap();
154    }
155}