diff --git a/Cargo.lock b/Cargo.lock index f48e24c8d8..2c2ec4595c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3975,8 +3975,11 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", + "futures", "fuzzy", "gpui", + "language", + "lsp", "ordered-float", "picker", "postage", diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 720fcfd544..cf07def52a 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -139,7 +139,7 @@ impl Picker { max_size: vec2f(540., 420.), confirmed: false, }; - cx.defer(|this, cx| this.update_matches(cx)); + cx.defer(|this, cx| this.update_matches(String::new(), cx)); this } @@ -159,7 +159,7 @@ impl Picker { cx: &mut ViewContext, ) { match event { - editor::Event::BufferEdited { .. } => self.update_matches(cx), + editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx), editor::Event::Blurred if !self.confirmed => { if let Some(delegate) = self.delegate.upgrade(cx) { delegate.update(cx, |delegate, cx| { @@ -171,9 +171,8 @@ impl Picker { } } - fn update_matches(&mut self, cx: &mut ViewContext) { + pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { if let Some(delegate) = self.delegate.upgrade(cx) { - let query = self.query(cx); let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); cx.notify(); cx.spawn(|this, mut cx| async move { diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index e199b700f6..cb1f186bde 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -21,3 +21,11 @@ anyhow = "1.0.38" ordered-float = "2.1.1" postage = { version = "0.4", features = ["futures-traits"] } smol = "1.2" + +[dev-dependencies] +futures = "0.3" +settings = { path = "../settings", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } \ No newline at end of file diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 8e89951a00..cbdc83d19e 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -90,7 +90,7 @@ impl ProjectSymbolsView { }) .collect() } else { - smol::block_on(fuzzy::match_strings( + cx.background_executor().block(fuzzy::match_strings( &self.match_candidates, query, false, @@ -263,3 +263,138 @@ impl PickerDelegate for ProjectSymbolsView { .boxed() } } + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use gpui::{serde_json::json, TestAppContext}; + use language::{FakeLspAdapter, Language, LanguageConfig}; + use project::FakeFs; + use std::sync::Arc; + + #[gpui::test] + async fn test_project_symbols(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| cx.set_global(Settings::test(cx))); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter::default()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + + let project = Project::test(fs.clone(), cx); + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + + let _buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "test.rs"), cx) + }) + .await + .unwrap(); + + // Set up fake langauge server to return fuzzy matches against + // a fixed set of symbol names. + let fake_symbol_names = ["one", "ton", "uno"]; + let fake_server = fake_servers.next().await.unwrap(); + fake_server.handle_request::( + move |params: lsp::WorkspaceSymbolParams, cx| { + let executor = cx.background(); + async move { + let candidates = fake_symbol_names + .into_iter() + .map(|name| StringMatchCandidate::new(0, name.into())) + .collect::>(); + let matches = fuzzy::match_strings( + &candidates, + ¶ms.query, + true, + 100, + &Default::default(), + executor.clone(), + ) + .await; + Ok(Some( + matches.into_iter().map(|mat| symbol(&mat.string)).collect(), + )) + } + }, + ); + + // Create the project symbols view. + let (_, symbols_view) = cx.add_window(|cx| ProjectSymbolsView::new(project.clone(), cx)); + let picker = symbols_view.read_with(cx, |symbols_view, _| symbols_view.picker.clone()); + + // Spawn multiples updates before the first update completes, + // such that in the end, there are no matches. Testing for regression: + // https://github.com/zed-industries/zed/issues/861 + picker.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("on".to_string(), cx); + p.update_matches("onex".to_string(), cx); + }); + + cx.foreground().run_until_parked(); + symbols_view.read_with(cx, |symbols_view, _| { + assert_eq!(symbols_view.matches.len(), 0); + }); + + // Spawn more updates such that in the end, there are matches. + picker.update(cx, |p, cx| { + p.update_matches("one".to_string(), cx); + p.update_matches("on".to_string(), cx); + }); + + cx.foreground().run_until_parked(); + symbols_view.read_with(cx, |symbols_view, _| { + assert_eq!(symbols_view.matches.len(), 2); + assert_eq!(symbols_view.matches[0].string, "one"); + assert_eq!(symbols_view.matches[1].string, "ton"); + }); + + // Spawn more updates such that in the end, there are again no matches. + picker.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("".to_string(), cx); + }); + + cx.foreground().run_until_parked(); + symbols_view.read_with(cx, |symbols_view, _| { + assert_eq!(symbols_view.matches.len(), 0); + }); + } + + fn symbol(name: &str) -> lsp::SymbolInformation { + #[allow(deprecated)] + lsp::SymbolInformation { + name: name.to_string(), + kind: lsp::SymbolKind::FUNCTION, + tags: None, + deprecated: None, + container_name: None, + location: lsp::Location::new( + lsp::Url::from_file_path("/a/b").unwrap(), + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + ), + } + } +}