diff --git a/Cargo.lock b/Cargo.lock index 044ff7f5c84935093de4cece4e6914dd6a9224b1..1802682c611c12deb0bc9ccfdaca08a0b446a1aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12681,6 +12681,7 @@ dependencies = [ "postage", "prettier", "pretty_assertions", + "project", "rand 0.9.2", "regex", "release_channel", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 040ca841afe237d40422d58b8238bcc0b18ada78..5d8a36cca78be18a6836ee93ac9efc415039d80e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -11,6 +11,12 @@ workspace = true [lib] path = "src/project.rs" doctest = false +test = false + +[[test]] +name = "integration" +required-features = ["test-support"] +path = "tests/integration/project_tests.rs" [features] test-support = [ @@ -111,6 +117,7 @@ language = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } prettier = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true +project = {workspace = true, features = ["test-support"]} rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } snippet_provider = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index ce40356b77ad248dd3e7405823f4fb40066ebf27..9ddeb0c948f29e24ddec14be75d89f33214fc05b 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -146,15 +146,15 @@ enum AgentServerStoreState { Collab, } -struct ExternalAgentEntry { +pub struct ExternalAgentEntry { server: Box, icon: Option, display_name: Option, - source: ExternalAgentSource, + pub source: ExternalAgentSource, } impl ExternalAgentEntry { - fn new( + pub fn new( server: Box, source: ExternalAgentSource, icon: Option, @@ -171,241 +171,13 @@ impl ExternalAgentEntry { pub struct AgentServerStore { state: AgentServerStoreState, - external_agents: HashMap, + pub external_agents: HashMap, } pub struct AgentServersUpdated; impl EventEmitter for AgentServerStore {} -#[cfg(test)] -mod ext_agent_tests { - use super::*; - use std::{collections::HashSet, fmt::Write as _}; - - // Helper to build a store in Collab mode so we can mutate internal maps without - // needing to spin up a full project environment. - fn collab_store() -> AgentServerStore { - AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - } - } - - // A simple fake that implements ExternalAgentServer without needing async plumbing. - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn external_agent_server_name_display() { - let name = ExternalAgentServerName(SharedString::from("Ext: Tool")); - let mut s = String::new(); - write!(&mut s, "{name}").unwrap(); - assert_eq!(s, "Ext: Tool"); - } - - #[test] - fn sync_extension_agents_removes_previous_extension_entries() { - let mut store = collab_store(); - - // Seed with a couple of agents that will be replaced by extensions - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("foo-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("bar-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - - // Simulate the removal phase: if we're syncing extensions that provide - // "foo-agent" and "bar-agent", those should be removed first - let extension_agent_names: HashSet = - ["foo-agent".to_string(), "bar-agent".to_string()] - .into_iter() - .collect(); - - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| extension_agent_names.contains(name.0.as_ref())) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only the custom entry should remain. - let remaining: Vec<_> = store - .external_agents - .keys() - .map(|k| k.0.to_string()) - .collect(); - assert_eq!(remaining, vec!["custom".to_string()]); - } - - #[test] - fn resolve_extension_icon_path_allows_valid_paths() { - // Create a temporary directory structure for testing - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Create a valid icon file - let icon_path = ext_dir.join("icon.svg"); - std::fs::write(&icon_path, "").unwrap(); - - // Test that a valid relative path works - let result = super::resolve_extension_icon_path(extensions_dir, "my-extension", "icon.svg"); - assert!(result.is_some()); - assert!(result.unwrap().ends_with("icon.svg")); - } - - #[test] - fn resolve_extension_icon_path_allows_nested_paths() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - let icons_dir = ext_dir.join("assets").join("icons"); - std::fs::create_dir_all(&icons_dir).unwrap(); - - let icon_path = icons_dir.join("logo.svg"); - std::fs::write(&icon_path, "").unwrap(); - - let result = super::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "assets/icons/logo.svg", - ); - assert!(result.is_some()); - assert!(result.unwrap().ends_with("logo.svg")); - } - - #[test] - fn resolve_extension_icon_path_blocks_path_traversal() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - - // Create two extension directories - let ext1_dir = extensions_dir.join("extension1"); - let ext2_dir = extensions_dir.join("extension2"); - std::fs::create_dir_all(&ext1_dir).unwrap(); - std::fs::create_dir_all(&ext2_dir).unwrap(); - - // Create a file in extension2 - let secret_file = ext2_dir.join("secret.svg"); - std::fs::write(&secret_file, "secret").unwrap(); - - // Try to access extension2's file from extension1 using path traversal - let result = super::resolve_extension_icon_path( - extensions_dir, - "extension1", - "../extension2/secret.svg", - ); - assert!( - result.is_none(), - "Path traversal to sibling extension should be blocked" - ); - } - - #[test] - fn resolve_extension_icon_path_blocks_absolute_escape() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Create a file outside the extensions directory - let outside_file = temp_dir.path().join("outside.svg"); - std::fs::write(&outside_file, "outside").unwrap(); - - // Try to escape to parent directory - let result = - super::resolve_extension_icon_path(extensions_dir, "my-extension", "../outside.svg"); - assert!( - result.is_none(), - "Path traversal to parent directory should be blocked" - ); - } - - #[test] - fn resolve_extension_icon_path_blocks_deep_traversal() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Try deep path traversal - let result = super::resolve_extension_icon_path( - extensions_dir, - "my-extension", - "../../../../../../etc/passwd", - ); - assert!( - result.is_none(), - "Deep path traversal should be blocked (file doesn't exist)" - ); - } - - #[test] - fn resolve_extension_icon_path_returns_none_for_nonexistent() { - let temp_dir = tempfile::tempdir().unwrap(); - let extensions_dir = temp_dir.path(); - let ext_dir = extensions_dir.join("my-extension"); - std::fs::create_dir_all(&ext_dir).unwrap(); - - // Try to access a file that doesn't exist - let result = - super::resolve_extension_icon_path(extensions_dir, "my-extension", "nonexistent.svg"); - assert!(result.is_none(), "Nonexistent file should return None"); - } -} - impl AgentServerStore { /// Synchronizes extension-provided agent servers with the store. pub fn sync_extension_agents<'a, I>( @@ -535,7 +307,7 @@ impl AgentServerStore { /// Safely resolves an extension icon path, ensuring it stays within the extension directory. /// Returns `None` if the path would escape the extension directory (path traversal attack). -fn resolve_extension_icon_path( +pub fn resolve_extension_icon_path( extensions_dir: &Path, extension_id: &str, icon_relative_path: &str, @@ -960,7 +732,7 @@ impl AgentServerStore { } } - pub(crate) fn collab(_cx: &mut Context) -> Self { + pub fn collab() -> Self { Self { state: AgentServerStoreState::Collab, external_agents: Default::default(), @@ -1937,15 +1709,15 @@ fn asset_name(version: &str) -> Option { Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}")) } -struct LocalExtensionArchiveAgent { - fs: Arc, - http_client: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - extension_id: Arc, - agent_id: Arc, - targets: HashMap, - env: HashMap, +pub struct LocalExtensionArchiveAgent { + pub fs: Arc, + pub http_client: Arc, + pub node_runtime: NodeRuntime, + pub project_environment: Entity, + pub extension_id: Arc, + pub agent_id: Arc, + pub targets: HashMap, + pub env: HashMap, } impl ExternalAgentServer for LocalExtensionArchiveAgent { @@ -2772,353 +2544,3 @@ impl settings::Settings for AllAgentServersSettings { } } } - -#[cfg(test)] -mod extension_agent_tests { - use crate::worktree_store::WorktreeStore; - - use super::*; - use gpui::TestAppContext; - use std::sync::Arc; - - #[test] - fn extension_agent_constructs_proper_display_names() { - // Verify the display name format for extension-provided agents - let name1 = ExternalAgentServerName(SharedString::from("Extension: Agent")); - assert!(name1.0.contains(": ")); - - let name2 = ExternalAgentServerName(SharedString::from("MyExt: MyAgent")); - assert_eq!(name2.0, "MyExt: MyAgent"); - - // Non-extension agents shouldn't have the separator - let custom = ExternalAgentServerName(SharedString::from("custom")); - assert!(!custom.0.contains(": ")); - } - - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn sync_removes_only_extension_provided_agents() { - let mut store = AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - }; - - // Seed with extension agents (contain ": ") and custom agents (don't contain ": ") - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext1: Agent1")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Extension, - None, - None, - ), - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext2: Agent2")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Extension, - None, - None, - ), - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom-agent")), - ExternalAgentEntry::new( - Box::new(NoopExternalAgent) as Box, - ExternalAgentSource::Custom, - None, - None, - ), - ); - - // Simulate removal phase - store - .external_agents - .retain(|_, entry| entry.source != ExternalAgentSource::Extension); - - // Only custom-agent should remain - assert_eq!(store.external_agents.len(), 1); - assert!( - store - .external_agents - .contains_key(&ExternalAgentServerName(SharedString::from("custom-agent"))) - ); - } - - #[test] - fn archive_launcher_constructs_with_all_fields() { - use extension::AgentServerManifestEntry; - - let mut env = HashMap::default(); - env.insert("GITHUB_TOKEN".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: - "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip" - .into(), - cmd: "./agent".into(), - args: vec![], - sha256: None, - env: Default::default(), - }, - ); - - let _entry = AgentServerManifestEntry { - name: "GitHub Agent".into(), - targets, - env, - icon: None, - }; - - // Verify display name construction - let expected_name = ExternalAgentServerName(SharedString::from("GitHub Agent")); - assert_eq!(expected_name.0, "GitHub Agent"); - } - - #[gpui::test] - async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs, - http_client, - node_runtime: node_runtime::NodeRuntime::unavailable(), - project_environment, - extension_id: Arc::from("my-extension"), - agent_id: Arc::from("my-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/my-agent-darwin-arm64.zip".into(), - cmd: "./my-agent".into(), - args: vec!["--serve".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: { - let mut map = HashMap::default(); - map.insert("PORT".into(), "8080".into()); - map - }, - }; - - // Verify agent is properly constructed - assert_eq!(agent.extension_id.as_ref(), "my-extension"); - assert_eq!(agent.agent_id.as_ref(), "my-agent"); - assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string())); - assert!(agent.targets.contains_key("darwin-aarch64")); - } - - #[test] - fn sync_extension_agents_registers_archive_launcher() { - use extension::AgentServerManifestEntry; - - let expected_name = ExternalAgentServerName(SharedString::from("Release Agent")); - assert_eq!(expected_name.0, "Release Agent"); - - // Verify the manifest entry structure for archive-based installation - let mut env = HashMap::default(); - env.insert("API_KEY".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "linux-x86_64".to_string(), - extension::TargetConfig { - archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(), - cmd: "./release-agent".into(), - args: vec!["serve".into()], - sha256: None, - env: Default::default(), - }, - ); - - let manifest_entry = AgentServerManifestEntry { - name: "Release Agent".into(), - targets: targets.clone(), - env, - icon: None, - }; - - // Verify target config is present - assert!(manifest_entry.targets.contains_key("linux-x86_64")); - let target = manifest_entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.cmd, "./release-agent"); - } - - #[gpui::test] - async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("node-extension"), - agent_id: Arc::from("node-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/node-agent.zip".into(), - cmd: "node".into(), - args: vec!["index.js".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify that when cmd is "node", it attempts to use the node runtime - assert_eq!(agent.extension_id.as_ref(), "node-extension"); - assert_eq!(agent.agent_id.as_ref(), "node-agent"); - - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.cmd, "node"); - assert_eq!(target.args, vec!["index.js"]); - } - - #[gpui::test] - async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("test-ext"), - agent_id: Arc::from("test-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/test.zip".into(), - cmd: "node".into(), - args: vec![ - "server.js".into(), - "--config".into(), - "./config.json".into(), - ], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify the agent is configured with relative paths in args - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.args[0], "server.js"); - assert_eq!(target.args[2], "./config.json"); - // These relative paths will resolve relative to the extraction directory - // when the command is executed - } - - #[test] - fn test_tilde_expansion_in_settings() { - let settings = settings::BuiltinAgentServerSettings { - path: Some(PathBuf::from("~/bin/agent")), - args: Some(vec!["--flag".into()]), - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - favorite_models: vec![], - default_config_options: Default::default(), - favorite_config_option_values: Default::default(), - }; - - let BuiltinAgentServerSettings { path, .. } = settings.into(); - - let path = path.unwrap(); - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for builtin agent path" - ); - - let settings = settings::CustomAgentServerSettings::Custom { - path: PathBuf::from("~/custom/agent"), - args: vec!["serve".into()], - env: Default::default(), - default_mode: None, - default_model: None, - favorite_models: vec![], - default_config_options: Default::default(), - favorite_config_option_values: Default::default(), - }; - - let converted: CustomAgentServerSettings = settings.into(); - let CustomAgentServerSettings::Custom { - command: AgentServerCommand { path, .. }, - .. - } = converted - else { - panic!("Expected Custom variant"); - }; - - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for custom agent path" - ); - } -} diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index 6e9907e30b7393a3074f4af579536d74140418f9..0a3da0d81ff2e398d0593a349448688523e09d43 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -134,162 +134,3 @@ fn from_hsl(h: &str, s: &str, l: &str, a: Option<&str>) -> Option { Some(Hsla { h, s, l, a }) } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::rgba; - use lsp::{CompletionItem, CompletionItemKind}; - - pub const COLOR_TABLE: &[(&str, Option)] = &[ - // -- Invalid -- - // Invalid hex - ("f0f", None), - ("#fof", None), - // Extra field - ("rgb(255, 0, 0, 0.0)", None), - ("hsl(120, 0, 0, 0.0)", None), - // Missing field - ("rgba(255, 0, 0)", None), - ("hsla(120, 0, 0)", None), - // No decimal after zero - ("rgba(255, 0, 0, 0)", None), - ("hsla(120, 0, 0, 0)", None), - // Decimal after one - ("rgba(255, 0, 0, 1.0)", None), - ("hsla(120, 0, 0, 1.0)", None), - // HEX (sRGB) - ("#f0f", Some(0xFF00FFFF)), - ("#ff0000", Some(0xFF0000FF)), - // RGB / RGBA (sRGB) - ("rgb(255, 0, 0)", Some(0xFF0000FF)), - ("rgba(255, 0, 0, 0.4)", Some(0xFF000066)), - ("rgba(255, 0, 0, 1)", Some(0xFF0000FF)), - ("rgb(20%, 0%, 0%)", Some(0x330000FF)), - ("rgba(20%, 0%, 0%, 1)", Some(0x330000FF)), - ("rgb(0%, 20%, 0%)", Some(0x003300FF)), - ("rgba(0%, 20%, 0%, 1)", Some(0x003300FF)), - ("rgb(0%, 0%, 20%)", Some(0x000033FF)), - ("rgba(0%, 0%, 20%, 1)", Some(0x000033FF)), - // HSL / HSLA (sRGB) - ("hsl(0, 100%, 50%)", Some(0xFF0000FF)), - ("hsl(120, 100%, 50%)", Some(0x00FF00FF)), - ("hsla(0, 100%, 50%, 0.0)", Some(0xFF000000)), - ("hsla(0, 100%, 50%, 0.4)", Some(0xFF000066)), - ("hsla(0, 100%, 50%, 1)", Some(0xFF0000FF)), - ("hsla(120, 100%, 50%, 0.0)", Some(0x00FF0000)), - ("hsla(120, 100%, 50%, 0.4)", Some(0x00FF0066)), - ("hsla(120, 100%, 50%, 1)", Some(0x00FF00FF)), - ]; - - #[test] - fn can_extract_from_label() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: color_str.to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_label_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: format!("{} foo", color_str).to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_detail() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(color_str.to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_detail_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(format!("{} foo", color_str).to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_documentation_start() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("{} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn can_extract_from_documentation_end() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {}", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn cannot_extract_from_documentation_middle() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, None); - } - } -} diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index df41220cd4c2899325c090c9d65f856e1ff0a7a3..3eda664c4d422007782dd1d2fc91062ff4a8638e 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -289,7 +289,7 @@ impl ContextServerStore { .collect() } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] pub fn test( registry: Entity, worktree_store: Entity, @@ -310,7 +310,7 @@ impl ContextServerStore { ) } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] pub fn test_maintain_server_loop( context_server_factory: Option, registry: Entity, @@ -332,17 +332,17 @@ impl ContextServerStore { ) } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] pub fn set_context_server_factory(&mut self, factory: ContextServerFactory) { self.context_server_factory = Some(factory); } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] pub fn registry(&self) -> &Entity { &self.registry } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] pub fn test_start_server(&mut self, server: Arc, cx: &mut Context) { let configuration = Arc::new(ContextServerConfiguration::Custom { command: ContextServerCommand { @@ -598,7 +598,7 @@ impl ContextServerStore { Ok(()) } - async fn create_context_server( + pub async fn create_context_server( this: WeakEntity, id: ContextServerId, configuration: Arc, @@ -926,886 +926,3 @@ impl ContextServerStore { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - FakeFs, Project, context_server_store::registry::ContextServerDescriptor, - project_settings::ProjectSettings, - }; - use context_server::test::create_fake_transport; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use http_client::{FakeHttpClient, Response}; - use serde_json::json; - use std::{cell::RefCell, path::PathBuf, rc::Rc}; - use util::path; - - #[gpui::test] - async fn test_context_server_status(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - store.update(cx, |store, cx| store.test_start_server(server_1, cx)); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - - store.update(cx, |store, cx| { - store.test_start_server(server_2.clone(), cx) - }); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Running) - ); - }); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Stopped) - ); - }); - } - - #[gpui::test] - async fn test_context_server_status_events(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id, ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Stopped), - ], - cx, - ); - - store.update(cx, |store, cx| store.test_start_server(server_1, cx)); - - cx.run_until_parked(); - - store.update(cx, |store, cx| { - store.test_start_server(server_2.clone(), cx) - }); - - cx.run_until_parked(); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - } - - #[gpui::test(iterations = 25)] - async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let server_id = ContextServerId(SERVER_1_ID.into()); - - let server_with_same_id_1 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_with_same_id_2 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - - // If we start another server with the same id, we should report that we stopped the previous one - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Stopped), - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - - store.update(cx, |store, cx| { - store.test_start_server(server_with_same_id_1.clone(), cx) - }); - store.update(cx, |store, cx| { - store.test_start_server(server_with_same_id_2.clone(), cx) - }); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_id), - Some(ContextServerStatus::Running) - ); - }); - } - - #[gpui::test] - async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let fake_descriptor_1 = Arc::new(FakeContextServerDescriptor::new(SERVER_1_ID)); - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let executor = cx.executor(); - let store = project.read_with(cx, |project, _| project.context_server_store()); - store.update(cx, |store, cx| { - store.set_context_server_factory(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })); - store.registry().update(cx, |registry, cx| { - registry.register_context_server_descriptor( - SERVER_1_ID.into(), - fake_descriptor_1, - cx, - ); - }); - }); - - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": true - }), - }, - )], - cx, - ); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is restarted when the configuration was changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Stopped), - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is not restarted when the configuration was not changed - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is started once it is added to the settings - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - remote: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is restarted once the args have changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Stopped), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - remote: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["anotherArg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is removed once it is removed from the settings - { - let _server_events = assert_server_events( - &store, - vec![(server_2_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - - // Ensure that nothing happens if the settings do not change - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - remote: false, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - } - - #[gpui::test] - async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let executor = cx.executor(); - let store = project.read_with(cx, |project, _| project.context_server_store()); - store.update(cx, |store, _| { - store.set_context_server_factory(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })); - }); - - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - remote: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - cx, - ); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is stopped once it is disabled. - { - let _server_events = assert_server_events( - &store, - vec![(server_1_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: false, - remote: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is started once it is enabled again. - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - remote: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - timeout: None, - env: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - } - - fn set_context_server_configuration( - context_servers: Vec<(Arc, settings::ContextServerSettingsContent)>, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |content| { - content.project.context_servers.clear(); - for (id, config) in context_servers { - content.project.context_servers.insert(id, config); - } - }); - }) - }); - } - - #[gpui::test] - async fn test_remote_context_server(cx: &mut TestAppContext) { - const SERVER_ID: &str = "remote-server"; - let server_id = ContextServerId(SERVER_ID.into()); - let server_url = "http://example.com/api"; - - let client = FakeHttpClient::create(|_| async move { - use http_client::AsyncBody; - - let response = Response::builder() - .status(200) - .header("Content-Type", "application/json") - .body(AsyncBody::from( - serde_json::to_string(&json!({ - "jsonrpc": "2.0", - "id": 0, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "serverInfo": { - "name": "test-server", - "version": "1.0.0" - } - } - })) - .unwrap(), - )) - .unwrap(); - Ok(response) - }); - cx.update(|cx| cx.set_http_client(client)); - - let (_fs, project) = setup_context_server_test(cx, json!({ "code.rs": "" }), vec![]).await; - - let store = project.read_with(cx, |project, _| project.context_server_store()); - - set_context_server_configuration( - vec![( - server_id.0.clone(), - settings::ContextServerSettingsContent::Http { - enabled: true, - url: server_url.to_string(), - headers: Default::default(), - timeout: None, - }, - )], - cx, - ); - - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - struct ServerEvents { - received_event_count: Rc>, - expected_event_count: usize, - _subscription: Subscription, - } - - impl Drop for ServerEvents { - fn drop(&mut self) { - let actual_event_count = *self.received_event_count.borrow(); - assert_eq!( - actual_event_count, self.expected_event_count, - " - Expected to receive {} context server store events, but received {} events", - self.expected_event_count, actual_event_count - ); - } - } - - #[gpui::test] - async fn test_context_server_global_timeout(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - SettingsStore::update_global(cx, |store, cx| { - store - .set_user_settings(r#"{"context_server_timeout": 90}"#, cx) - .expect("Failed to set test user settings"); - }); - }); - - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let mut async_cx = cx.to_async(); - let result = ContextServerStore::create_context_server( - store.downgrade(), - ContextServerId("test-server".into()), - Arc::new(ContextServerConfiguration::Http { - url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"), - headers: Default::default(), - timeout: None, - }), - &mut async_cx, - ) - .await; - - assert!( - result.is_ok(), - "Server should be created successfully with global timeout" - ); - } - - #[gpui::test] - async fn test_context_server_per_server_timeout_override(cx: &mut TestAppContext) { - const SERVER_ID: &str = "test-server"; - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - SettingsStore::update_global(cx, |store, cx| { - store - .set_user_settings(r#"{"context_server_timeout": 60}"#, cx) - .expect("Failed to set test user settings"); - }); - }); - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![( - SERVER_ID.into(), - ContextServerSettings::Http { - enabled: true, - url: "http://localhost:8080".to_string(), - headers: Default::default(), - timeout: Some(120), - }, - )], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let mut async_cx = cx.to_async(); - let result = ContextServerStore::create_context_server( - store.downgrade(), - ContextServerId("test-server".into()), - Arc::new(ContextServerConfiguration::Http { - url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"), - headers: Default::default(), - timeout: Some(120), - }), - &mut async_cx, - ) - .await; - - assert!( - result.is_ok(), - "Server should be created successfully with per-server timeout override" - ); - } - - #[gpui::test] - async fn test_context_server_stdio_timeout(cx: &mut TestAppContext) { - let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - Some(project.downgrade()), - cx, - ) - }); - - let mut async_cx = cx.to_async(); - let result = ContextServerStore::create_context_server( - store.downgrade(), - ContextServerId("stdio-server".into()), - Arc::new(ContextServerConfiguration::Custom { - command: ContextServerCommand { - path: "/usr/bin/node".into(), - args: vec!["server.js".into()], - env: None, - timeout: Some(180000), - }, - remote: false, - }), - &mut async_cx, - ) - .await; - - assert!( - result.is_ok(), - "Stdio server should be created successfully with timeout" - ); - } - - fn assert_server_events( - store: &Entity, - expected_events: Vec<(ContextServerId, ContextServerStatus)>, - cx: &mut TestAppContext, - ) -> ServerEvents { - cx.update(|cx| { - let mut ix = 0; - let received_event_count = Rc::new(RefCell::new(0)); - let expected_event_count = expected_events.len(); - let subscription = cx.subscribe(store, { - let received_event_count = received_event_count.clone(); - move |_, event, _| match event { - Event::ServerStatusChanged { - server_id: actual_server_id, - status: actual_status, - } => { - let (expected_server_id, expected_status) = &expected_events[ix]; - - assert_eq!( - actual_server_id, expected_server_id, - "Expected different server id at index {}", - ix - ); - assert_eq!( - actual_status, expected_status, - "Expected different status at index {}", - ix - ); - ix += 1; - *received_event_count.borrow_mut() += 1; - } - } - }); - ServerEvents { - expected_event_count, - received_event_count, - _subscription: subscription, - } - }) - } - - async fn setup_context_server_test( - cx: &mut TestAppContext, - files: serde_json::Value, - context_server_configurations: Vec<(Arc, ContextServerSettings)>, - ) -> (Arc, Entity) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - let mut settings = ProjectSettings::get_global(cx).clone(); - for (id, config) in context_server_configurations { - settings.context_servers.insert(id, config); - } - ProjectSettings::override_global(settings, cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), files).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - (fs, project) - } - - struct FakeContextServerDescriptor { - path: PathBuf, - } - - impl FakeContextServerDescriptor { - fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - } - - impl ContextServerDescriptor for FakeContextServerDescriptor { - fn command( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task> { - Task::ready(Ok(ContextServerCommand { - path: self.path.clone(), - args: vec!["arg1".to_string(), "arg2".to_string()], - env: None, - timeout: None, - })) - } - - fn configuration( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task>> { - Task::ready(Ok(None)) - } - } -} diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 0bf6a0d61b792bd747992a821adc82150d93c8bf..ce97bf5cb9b4abe5bfe85f53641a8bf227a900ef 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -15,7 +15,7 @@ pub mod breakpoint_store; pub mod dap_command; pub mod dap_store; pub mod locators; -mod memory; +pub mod memory; pub mod session; #[cfg(any(feature = "test-support", test))] diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs index 2faa4c4ca97334ed2bee4205e9d54de2af775ae9..95eb8ca72704b26bd5654ab7af863b88e6bb31fa 100644 --- a/crates/project/src/debugger/locators.rs +++ b/crates/project/src/debugger/locators.rs @@ -1,4 +1,4 @@ pub(crate) mod cargo; -pub(crate) mod go; +pub mod go; pub(crate) mod node; -pub(crate) mod python; +pub mod python; diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index 49866665de4552422b8cb34f7772c1f5a5d72f0b..24cc428b0577df86494cbcc28a21875caf3ee909 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -6,19 +6,19 @@ use gpui::{BackgroundExecutor, SharedString}; use serde::{Deserialize, Serialize}; use task::{DebugScenario, SpawnInTerminal, TaskTemplate}; -pub(crate) struct GoLocator; +pub struct GoLocator; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -struct DelveLaunchRequest { - request: String, - mode: String, - program: String, +pub struct DelveLaunchRequest { + pub request: String, + pub mode: String, + pub program: String, #[serde(skip_serializing_if = "Option::is_none")] - cwd: Option, - args: Vec, - build_flags: Vec, - env: HashMap, + pub cwd: Option, + pub args: Vec, + pub build_flags: Vec, + pub env: HashMap, } fn is_debug_flag(arg: &str) -> Option { @@ -245,201 +245,3 @@ impl DapLocator for GoLocator { unreachable!() } } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate}; - - #[gpui::test] - async fn test_create_scenario_for_go_build(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go build".into(), - command: "go".into(), - args: vec!["build".into(), ".".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - - assert!(scenario.is_none()); - } - - #[gpui::test] - async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "cargo build".into(), - command: "cargo".into(), - args: vec!["build".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario( - &task, - "test label", - &DebugAdapterName("SomeOtherAdapter".into()), - ) - .await; - assert!(scenario.is_none()); - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } - #[gpui::test] - async fn test_go_locator_run(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - let task = TaskTemplate { - label: "go run with flags".into(), - command: "go".into(), - args: vec![ - "run".to_string(), - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string(), - "./cmd/myapp".to_string(), - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".into()), - ..Default::default() - }; - - let scenario = locator - .create_scenario(&task, "test run label", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "debug".to_string(), - program: "./cmd/myapp".to_string(), - build_flags: vec![ - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string() - ], - args: vec![ - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".to_string()), - } - ); - } - - #[gpui::test] - async fn test_go_locator_test(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - // Test with tags and run flag - let task_with_tags = TaskTemplate { - label: "test".into(), - command: "go".into(), - args: vec![ - "test".to_string(), - "-tags".to_string(), - "integration,unit".to_string(), - "-run".to_string(), - "Foo".to_string(), - ".".to_string(), - ], - ..Default::default() - }; - let result = locator - .create_scenario(&task_with_tags, "", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "test".to_string(), - program: ".".to_string(), - build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),], - args: vec![ - "-test.run".to_string(), - "Foo".to_string(), - "-test.v".to_string() - ], - env: HashMap::default(), - cwd: None, - } - ); - } - - #[gpui::test] - async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go clean".into(), - command: "go".into(), - args: vec!["clean".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } -} diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 2da26a927461c91bdc1ee6ca3b279aa34726d759..87219e4f8caa7f9e2f29ddd983c0112444d4533b 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -7,7 +7,7 @@ use gpui::{BackgroundExecutor, SharedString}; use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName}; -pub(crate) struct PythonLocator; +pub struct PythonLocator; #[async_trait] impl DapLocator for PythonLocator { @@ -94,53 +94,3 @@ impl DapLocator for PythonLocator { bail!("Python locator should not require DapLocator::run to be ran"); } } - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::*; - - #[gpui::test] - async fn test_python_locator() { - let adapter = DebugAdapterName("Debugpy".into()); - let build_task = TaskTemplate { - label: "run module '$ZED_FILE'".into(), - command: "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN".into(), - args: vec!["-m".into(), "$ZED_CUSTOM_PYTHON_MODULE_NAME".into()], - env: Default::default(), - cwd: Some("$ZED_WORKTREE_ROOT".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: task::RevealStrategy::Always, - reveal_target: task::RevealTarget::Dock, - hide: task::HideStrategy::Never, - tags: vec!["python-module-main-method".into()], - shell: task::Shell::System, - show_summary: false, - show_command: false, - }; - - let expected_scenario = DebugScenario { - adapter: "Debugpy".into(), - label: "run module 'main.py'".into(), - build: None, - config: json!({ - "request": "launch", - "python": "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN", - "args": [], - "cwd": "$ZED_WORKTREE_ROOT", - "module": "$ZED_CUSTOM_PYTHON_MODULE_NAME", - }), - tcp_connection: None, - }; - - assert_eq!( - PythonLocator - .create_scenario(&build_task, "run module 'main.py'", &adapter) - .await - .expect("Failed to create a scenario"), - expected_scenario - ); - } -} diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index 42ad64e6880ba653c6c95cb13f0e6bcc23c9bdae..15acc2b50e8517a567ac7ec38526534fdb37d97b 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -23,15 +23,15 @@ const PAGE_SIZE: u64 = 4096; /// Represents the contents of a single page. We special-case unmapped pages to be allocation-free, /// since they're going to make up the majority of the memory in a program space (even though the user might not even get to see them - ever). #[derive(Clone, Debug)] -pub(super) enum PageContents { +pub enum PageContents { /// Whole page is unreadable. Unmapped, Mapped(Arc), } impl PageContents { - #[cfg(test)] - fn mapped(contents: Vec) -> Self { + #[cfg(feature = "test-support")] + pub fn mapped(contents: Vec) -> Self { PageContents::Mapped(Arc::new(MappedPageContents( vec![PageChunk::Mapped(contents.into())].into(), ))) @@ -68,7 +68,7 @@ impl MappedPageContents { /// of the memory of a debuggee. #[derive(Default, Debug)] -pub(super) struct MappedPageContents( +pub struct MappedPageContents( /// Most of the time there should be only one chunk (either mapped or unmapped), /// but we do leave the possibility open of having multiple regions of memory in a single page. SmallVec<[PageChunk; 1]>, @@ -77,7 +77,7 @@ pub(super) struct MappedPageContents( type MemoryAddress = u64; #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] #[repr(transparent)] -pub(super) struct PageAddress(u64); +pub struct PageAddress(pub u64); impl PageAddress { pub(super) fn iter_range( @@ -273,7 +273,7 @@ pub struct MemoryIterator { } impl MemoryIterator { - fn new( + pub fn new( range: RangeInclusive, pages: std::vec::IntoIter<(PageAddress, PageContents)>, ) -> Self { @@ -336,49 +336,3 @@ impl Iterator for MemoryIterator { } } } - -#[cfg(test)] -mod tests { - use crate::debugger::{ - MemoryCell, - memory::{MemoryIterator, PageAddress, PageContents}, - }; - - #[test] - fn iterate_over_unmapped_memory() { - let empty_iterator = MemoryIterator::new(0..=127, Default::default()); - let actual = empty_iterator.collect::>(); - let expected = vec![MemoryCell(None); 128]; - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn iterate_over_partially_mapped_memory() { - let it = MemoryIterator::new( - 0..=127, - vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(), - ); - let actual = it.collect::>(); - let expected = std::iter::repeat_n(MemoryCell(None), 5) - .chain(std::iter::once(MemoryCell(Some(1)))) - .chain(std::iter::repeat_n(MemoryCell(None), 122)) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn reads_from_the_middle_of_a_page() { - let partial_iter = MemoryIterator::new( - 20..=30, - vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(), - ); - let actual = partial_iter.collect::>(); - let expected = (20..=30) - .map(|val| MemoryCell(Some(val))) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } -} diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 064b6998cdb72423d1123166a2ce6a75765db029..5647e7f3553d684e64c0aab713935af377a66149 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -274,469 +274,3 @@ impl ConflictSet { } impl EventEmitter for ConflictSet {} - -#[cfg(test)] -mod tests { - use std::sync::mpsc; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::{ - repository::{RepoPath, repo_path}, - status::{UnmergedStatus, UnmergedStatusCode}, - }; - use gpui::{BackgroundExecutor, TestAppContext}; - use serde_json::json; - use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _}; - use unindent::Unindent as _; - use util::{path, rel_path::rel_path}; - - #[test] - fn test_parse_conflicts_in_buffer() { - // Create a buffer with conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - ======= - This is their version - >>>>>>> branch-name - - Another conflict: - <<<<<<< HEAD - Our second change - ||||||| merged common ancestors - Original content - ======= - Their second change - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 2); - - let first = &conflict_snapshot.conflicts[0]; - assert!(first.base.is_none()); - assert_eq!(first.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(first.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(first.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(first.theirs.clone()) - .collect::(); - assert_eq!(our_text, "This is our version\n"); - assert_eq!(their_text, "This is their version\n"); - - let second = &conflict_snapshot.conflicts[1]; - assert!(second.base.is_some()); - assert_eq!(second.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(second.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(second.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(second.theirs.clone()) - .collect::(); - let base_text = snapshot - .text_for_range(second.base.as_ref().unwrap().clone()) - .collect::(); - assert_eq!(our_text, "Our second change\n"); - assert_eq!(their_text, "Their second change\n"); - assert_eq!(base_text, "Original content\n"); - - // Test conflicts_in_range - let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 2); - - // Test with a range that includes only the first conflict - let first_conflict_end = conflict_snapshot.conflicts[0].range.end; - let range = snapshot.anchor_before(0)..first_conflict_end; - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that includes only the second conflict - let second_conflict_start = conflict_snapshot.conflicts[1].range.start; - let range = second_conflict_start..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that doesn't include any conflicts - let range = buffer.anchor_after(first_conflict_end.to_next_offset(&buffer)) - ..buffer.anchor_before(second_conflict_start.to_previous_offset(&buffer)); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 0); - } - - #[test] - fn test_nested_conflict_markers() { - // Create a buffer with nested conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - <<<<<<< HEAD - This is a nested conflict marker - ======= - This is their version in a nested conflict - >>>>>>> branch-nested - ======= - This is their version - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - - assert_eq!(conflict_snapshot.conflicts.len(), 1); - - // The conflict should have our version, their version, but no base - let conflict = &conflict_snapshot.conflicts[0]; - assert!(conflict.base.is_none()); - assert_eq!(conflict.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(conflict.theirs_branch_name.as_ref(), "branch-nested"); - - // Check that the nested conflict was detected correctly - let our_text = snapshot - .text_for_range(conflict.ours.clone()) - .collect::(); - assert_eq!(our_text, "This is a nested conflict marker\n"); - let their_text = snapshot - .text_for_range(conflict.theirs.clone()) - .collect::(); - assert_eq!(their_text, "This is their version in a nested conflict\n"); - } - - #[test] - fn test_conflict_markers_at_eof() { - let test_content = r#" - <<<<<<< ours - ======= - This is their version - >>>>>>> "# - .unindent(); - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 1); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "ours" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "Origin" // default branch name if there is none - ); - } - - #[test] - fn test_conflicts_in_range() { - // Create a buffer with conflict markers - let test_content = r#" - one - <<<<<<< HEAD1 - two - ======= - three - >>>>>>> branch1 - four - five - <<<<<<< HEAD2 - six - ======= - seven - >>>>>>> branch2 - eight - nine - <<<<<<< HEAD3 - ten - ======= - eleven - >>>>>>> branch3 - twelve - <<<<<<< HEAD4 - thirteen - ======= - fourteen - >>>>>>> branch4 - fifteen - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone()); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 4); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "HEAD1" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "branch1" - ); - assert_eq!( - conflict_snapshot.conflicts[1].ours_branch_name.as_ref(), - "HEAD2" - ); - assert_eq!( - conflict_snapshot.conflicts[1].theirs_branch_name.as_ref(), - "branch2" - ); - assert_eq!( - conflict_snapshot.conflicts[2].ours_branch_name.as_ref(), - "HEAD3" - ); - assert_eq!( - conflict_snapshot.conflicts[2].theirs_branch_name.as_ref(), - "branch3" - ); - assert_eq!( - conflict_snapshot.conflicts[3].ours_branch_name.as_ref(), - "HEAD4" - ); - assert_eq!( - conflict_snapshot.conflicts[3].theirs_branch_name.as_ref(), - "branch4" - ); - - let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[0..=1] - ); - - let range = - test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("thirteen").unwrap() - 1..test_content.len(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[3..=3] - ); - } - - #[gpui::test] - async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - let initial_text = " - one - two - three - four - five - " - .unindent(); - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - let buffer = buffer.await.unwrap(); - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - let (events_tx, events_rx) = mpsc::channel::(); - let _conflict_set_subscription = cx.update(|cx| { - cx.subscribe(&conflict_set, move |_, event, _| { - events_tx.send(event.clone()).ok(); - }) - }); - let conflicts_snapshot = - conflict_set.read_with(cx, |conflict_set, _| conflict_set.snapshot()); - assert!(conflicts_snapshot.conflicts.is_empty()); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (4..4, "<<<<<<< HEAD\n"), - (14..14, "=======\nTWO\n>>>>>>> branch\n"), - ], - None, - cx, - ); - }); - - cx.run_until_parked(); - events_rx.try_recv().expect_err( - "no conflicts should be registered as long as the file's status is unchanged", - ); - - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ); - // Cause the repository to emit MergeHeadsChanged. - state.refs.insert("MERGE_HEAD".into(), "123".into()) - }) - .unwrap(); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("status change should trigger conflict parsing"); - assert_eq!(update.old_range, 0..0); - assert_eq!(update.new_range, 0..1); - - let conflict = conflict_set.read_with(cx, |conflict_set, _| { - conflict_set.snapshot().conflicts[0].clone() - }); - cx.update(|cx| { - conflict.resolve(buffer.clone(), std::slice::from_ref(&conflict.theirs), cx); - }); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("conflicts should be removed after resolution"); - assert_eq!(update.old_range, 0..1); - assert_eq!(update.new_range, 0..0); - } - - #[gpui::test] - async fn test_conflict_updates_without_merge_head( - executor: BackgroundExecutor, - cx: &mut TestAppContext, - ) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - - let initial_text = " - zero - <<<<<<< HEAD - one - ======= - two - >>>>>>> Stashed Changes - three - " - .unindent(); - - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - - cx.run_until_parked(); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - RepoPath::from_rel_path(rel_path("a.txt")), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - let buffer = buffer.await.unwrap(); - - // Open the conflict set for a file that currently has conflicts. - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - - // Simulate the conflict being removed by e.g. staging the file. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.remove(&repo_path("a.txt")) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, _| { - assert!(!conflict_set.has_conflict); - assert_eq!(conflict_set.snapshot.conflicts.len(), 0); - }); - - // Simulate the conflict being re-added. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - } -} diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 39857951ad5afa207489c9f00399ec51e5e7dfdc..506eb55f87cee1fe68afaae64344c4cdf41300c2 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -250,544 +250,3 @@ impl AsRef for GitEntry { &self.entry } } - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode}; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }); - const ADDED: GitSummary = GitSummary { - index: TrackedSummary::ADDED, - count: 1, - ..GitSummary::UNCHANGED - }; - const MODIFIED: GitSummary = GitSummary { - index: TrackedSummary::MODIFIED, - count: 1, - ..GitSummary::UNCHANGED - }; - - #[gpui::test] - async fn test_git_traversal_with_one_repo(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - let traversal = GitTraversal::new( - &repo_snapshots, - worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()), - ); - let entries = traversal - .map(|entry| (entry.path.clone(), entry.git_summary)) - .collect::>(); - pretty_assertions::assert_eq!( - entries, - [ - (rel_path("x/x1.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/x2.txt").into(), MODIFIED), - (rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT), - (rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/z.txt").into(), ADDED), - (rel_path("z/z1.txt").into(), GitSummary::UNCHANGED), - (rel_path("z/z2.txt").into(), ADDED), - ] - ) - } - - #[gpui::test] - async fn test_git_traversal_with_nested_repos(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - // Sanity check the propagation for x/y and z - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ], - ); - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - - // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ], - ); - - // Sanity check everything around it - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ], - ); - - // Test the other fundamental case, transitioning from git repository to non-git repository - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ], - ); - - // And all together now - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_simple(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a": { - "b": { - "c1.txt": "", - "c2.txt": "", - }, - "d": { - "e1.txt": "", - "e2.txt": "", - "e3.txt": "", - } - }, - "f": { - "no-status.txt": "" - }, - "g": { - "h1.txt": "", - "h2.txt": "" - }, - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/.git")), - &[ - ("a/b/c1.txt", StatusCode::Added.index()), - ("a/d/e2.txt", StatusCode::Modified.index()), - ("g/h2.txt", CONFLICT), - ], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + MODIFIED + ADDED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + ADDED + MODIFIED), - ("a", ADDED + MODIFIED), - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_with_repos_under_project(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar" - }, - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[("x1.txt", StatusCode::Added.index())], - ); - fs.set_status_for_repo( - Path::new(path!("/root/y/.git")), - &[ - ("y1.txt", CONFLICT), - ("y2.txt", StatusCode::Modified.index()), - ], - ); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Modified.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("z", MODIFIED), ("z/z2.txt", MODIFIED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", ADDED), - ("x/x1.txt", ADDED), - ("x/x2.txt", GitSummary::UNCHANGED), - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ("z", MODIFIED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", MODIFIED), - ], - ); - } - - fn init_test(cx: &mut gpui::TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { - init_test(cx); - - // Create a worktree with a git directory. - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a.txt": "", - "b": { - "c.txt": "", - }, - }), - ) - .await; - fs.set_head_and_index_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "".into())], - ); - cx.run_until_parked(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - - // Regression test: after the directory is scanned, touch the git repo's - // working directory, bumping its mtime. That directory keeps its project - // entry id after the directories are re-scanned. - fs.touch_path(path!("/root")).await; - cx.executor().run_until_parked(); - - let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - assert_eq!(new_entry_ids, old_entry_ids); - assert_ne!(new_mtimes, old_mtimes); - - // Regression test: changes to the git repository should still be - // detected. - fs.set_head_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "something-else".into())], - "deadbeef", - ); - cx.executor().run_until_parked(); - cx.executor().advance_clock(Duration::from_secs(1)); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", MODIFIED), - ("a.txt", GitSummary::UNCHANGED), - ("b/c.txt", MODIFIED), - ], - ); - } - - #[track_caller] - fn check_git_statuses( - repo_snapshots: &HashMap, - worktree_snapshot: &worktree::Snapshot, - expected_statuses: &[(&str, GitSummary)], - ) { - let mut traversal = GitTraversal::new( - repo_snapshots, - worktree_snapshot.traverse_from_path(true, true, false, RelPath::empty()), - ); - let found_statuses = expected_statuses - .iter() - .map(|&(path, _)| { - let git_entry = traversal - .find(|git_entry| git_entry.path.as_ref() == rel_path(path)) - .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}")); - (path, git_entry.git_summary) - }) - .collect::>(); - pretty_assertions::assert_eq!(found_statuses, expected_statuses); - } -} diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index c933df1be3a7497295202574e404a1c501086a49..654fb0344db4b7dc581234a5b446e8ac4d2b10ab 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -108,7 +108,7 @@ pub struct ImageItem { } impl ImageItem { - fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result { + pub fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result { let image_format = image::guess_format(image_bytes)?; let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes)); @@ -904,84 +904,3 @@ fn create_gpui_image(content: Vec) -> anyhow::Result> { content, ))) } - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::rel_path::rel_path; - - pub fn init_test(cx: &mut TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_image_not_loaded_twice(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree("/root", json!({})).await; - // Create a png file that consists of a single white pixel - fs.insert_file( - "/root/image_1.png", - vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ], - ) - .await; - - let project = Project::test(fs, ["/root".as_ref()], cx).await; - - let worktree_id = - cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - - let project_path = ProjectPath { - worktree_id, - path: rel_path("image_1.png").into(), - }; - - let (task1, task2) = project.update(cx, |project, cx| { - ( - project.open_image(project_path.clone(), cx), - project.open_image(project_path.clone(), cx), - ) - }); - - let image1 = task1.await.unwrap(); - let image2 = task2.await.unwrap(); - - assert_eq!(image1, image2); - } - - #[gpui::test] - fn test_compute_metadata_from_bytes() { - // Single white pixel PNG - let png_bytes = vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ]; - - let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap(); - - assert_eq!(metadata.width, 1); - assert_eq!(metadata.height, 1); - assert_eq!(metadata.file_size, png_bytes.len() as u64); - assert_eq!(metadata.format, image::ImageFormat::Png); - assert!(metadata.colors.is_some()); - } -} diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index e5b91094fe670b8ed740134fc5abcebe500caf81..9ec6d792558c7f4ba306e2bac3f735716384a6b0 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,4 +1,4 @@ -mod signature_help; +pub mod signature_help; use crate::{ CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor, @@ -273,7 +273,7 @@ pub(crate) struct LinkedEditingRange { } #[derive(Clone, Debug)] -pub(crate) struct GetDocumentDiagnostics { +pub struct GetDocumentDiagnostics { /// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas /// a server can register multiple diagnostic providers post-mortem. pub registration_id: Option, @@ -3797,7 +3797,7 @@ impl GetDocumentDiagnostics { .collect() } - fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { + pub fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { let start = diagnostic.start.context("invalid start range")?; let end = diagnostic.end.context("invalid end range")?; @@ -3871,7 +3871,7 @@ impl GetDocumentDiagnostics { }) } - fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { + pub fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { let range = language::range_from_lsp(diagnostic.range); let related_information = diagnostic .related_information @@ -4527,132 +4527,3 @@ fn process_full_diagnostics_report( } } } - -#[cfg(test)] -mod tests { - use super::*; - use lsp::{DiagnosticSeverity, DiagnosticTag}; - use serde_json::json; - - #[test] - fn test_serialize_lsp_diagnostic() { - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 1), - end: lsp::Position::new(2, 3), - }, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(lsp::NumberOrString::String("E001".to_string())), - source: Some("test-source".to_string()), - message: "Test error message".to_string(), - related_information: None, - tags: Some(vec![DiagnosticTag::DEPRECATED]), - code_description: None, - data: Some(json!({"detail": "test detail"})), - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - let start = proto_diagnostic.start.unwrap(); - let end = proto_diagnostic.end.unwrap(); - assert_eq!(start.row, 0); - assert_eq!(start.column, 1); - assert_eq!(end.row, 2); - assert_eq!(end.column, 3); - assert_eq!( - proto_diagnostic.severity, - proto::lsp_diagnostic::Severity::Error as i32 - ); - assert_eq!(proto_diagnostic.code, Some("E001".to_string())); - assert_eq!(proto_diagnostic.source, Some("test-source".to_string())); - assert_eq!(proto_diagnostic.message, "Test error message"); - } - - #[test] - fn test_deserialize_lsp_diagnostic() { - let proto_diagnostic = proto::LspDiagnostic { - start: Some(proto::PointUtf16 { row: 0, column: 1 }), - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Warning as i32, - code: Some("ERR".to_string()), - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic) - .expect("Failed to deserialize diagnostic"); - - assert_eq!(lsp_diagnostic.range.start.line, 0); - assert_eq!(lsp_diagnostic.range.start.character, 1); - assert_eq!(lsp_diagnostic.range.end.line, 2); - assert_eq!(lsp_diagnostic.range.end.character, 3); - assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING)); - assert_eq!( - lsp_diagnostic.code, - Some(lsp::NumberOrString::String("ERR".to_string())) - ); - assert_eq!(lsp_diagnostic.source, Some("Prism".to_string())); - assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a"); - } - - #[test] - fn test_related_information() { - let related_info = lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Uri::from_str("file:///test.rs").unwrap(), - range: lsp::Range { - start: lsp::Position::new(1, 1), - end: lsp::Position::new(1, 5), - }, - }, - message: "Related info message".to_string(), - }; - - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 0), - end: lsp::Position::new(0, 1), - }, - severity: Some(DiagnosticSeverity::INFORMATION), - code: None, - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: Some(vec![related_info]), - tags: None, - code_description: None, - data: None, - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - assert_eq!(proto_diagnostic.related_information.len(), 1); - let related = &proto_diagnostic.related_information[0]; - assert_eq!(related.location_url, Some("file:///test.rs".to_string())); - assert_eq!(related.message, "Related info message"); - } - - #[test] - fn test_invalid_ranges() { - let proto_diagnostic = proto::LspDiagnostic { - start: None, - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Error as i32, - code: None, - source: None, - message: "Test message".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic); - assert!(result.is_err()); - } -} diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs index 6a499311837b8ebd70874c89d9fac223b3c8ede1..2af9a19bf22b532bfb82f607c755f56f4713f5f4 100644 --- a/crates/project/src/lsp_command/signature_help.rs +++ b/crates/project/src/lsp_command/signature_help.rs @@ -269,525 +269,3 @@ fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option HighlightStyle { - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..Default::default() - } - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: "This is a test documentation".to_string(), - })), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(17..26, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test1(foo: u8, bar: &str)"), - vec![(9..16, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(23..33, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test3(one: usize, two: u32)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("one: usize".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("two: u32".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(2), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test3(one: usize, two: u32)"), - vec![(21..29, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![], - active_signature: None, - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_none()); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 15]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([17, 26]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_parameter_documentation(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: Some(Documentation::String("The foo parameter".to_string())), - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: Some(Documentation::String("The bar parameter".to_string())), - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_signature_help = - cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_signature_help.is_some()); - - let signature_help = maybe_signature_help.unwrap(); - let signature = &signature_help.signatures[signature_help.active_signature]; - - // Check that parameter documentation is extracted - assert_eq!(signature.parameters.len(), 2); - assert_eq!( - signature.parameters[0] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The foo parameter", - ); - assert_eq!( - signature.parameters[1] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The bar parameter", - ); - - // Check that the active parameter is correct - assert_eq!(signature.active_parameter, Some(0)); - } - - #[gpui::test] - fn test_create_signature_help_implements_utf16_spec(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(🦀: u8, 🦀: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 10]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([16, 18]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(signature_help.is_some()); - - let markdown = signature_help.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(🦀: u8, 🦀: &str)"), - vec![(8..12, current_parameter())] - ) - ); - } -} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b9963886458cc0b126576ab7e3f554af5f1ad896..72f04f8c3814834d86a7a12877b67c9178410954 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3012,7 +3012,7 @@ impl LocalLspStore { } #[allow(clippy::type_complexity)] - pub(crate) fn edits_from_lsp( + pub fn edits_from_lsp( &mut self, buffer: &Entity, lsp_edits: impl 'static + Send + IntoIterator, @@ -8474,7 +8474,7 @@ impl LspStore { .collect(); } - #[cfg(test)] + #[cfg(feature = "test-support")] pub fn update_diagnostic_entries( &mut self, server_id: LanguageServerId, @@ -14107,7 +14107,7 @@ pub enum ResolvedHint { Resolving(Shared>), } -fn glob_literal_prefix(glob: &Path) -> PathBuf { +pub fn glob_literal_prefix(glob: &Path) -> PathBuf { glob.components() .take_while(|component| match component { path::Component::Normal(part) => !part.to_string_lossy().contains(['*', '?', '{', '}']), @@ -14515,7 +14515,7 @@ fn include_text(server: &lsp::LanguageServer) -> Option { /// breaking the completions menu presentation. /// /// Sanitize the text to ensure there are no newlines, or, if there are some, remove them and also remove long space sequences if there were newlines. -fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { +pub fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { let mut new_text = String::with_capacity(label.text.len()); let mut offset_map = vec![0; label.text.len() + 1]; let mut last_char_was_space = false; @@ -14613,80 +14613,3 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { label.text = new_text; } - -#[cfg(test)] -mod tests { - use language::HighlightId; - - use super::*; - - #[test] - fn test_glob_literal_prefix() { - assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules/**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/bar/baz.js")), - Path::new("foo/bar/baz.js") - ); - - #[cfg(target_os = "windows")] - { - assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules\\**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo\\bar\\baz.js")), - Path::new("foo/bar/baz.js") - ); - } - } - - #[test] - fn test_multi_len_chars_normalization() { - let mut label = CodeLabel::new( - "myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ); - ensure_uniform_list_compatible_label(&mut label); - assert_eq!( - label, - CodeLabel::new( - "myElˇ (parameter) myElˇ: { foo: string; }".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ) - ); - } - - #[test] - fn test_trailing_newline_in_completion_documentation() { - let doc = lsp::Documentation::String( - "Inappropriate argument value (of correct type).\n".to_string(), - ); - let completion_doc: CompletionDocumentation = doc.into(); - assert!( - matches!(completion_doc, CompletionDocumentation::SingleLine(s) if s == "Inappropriate argument value (of correct type).") - ); - - let doc = lsp::Documentation::String(" some value \n".to_string()); - let completion_doc: CompletionDocumentation = doc.into(); - assert!(matches!( - completion_doc, - CompletionDocumentation::SingleLine(s) if s == "some value" - )); - } -} diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index ffa4872ca78e2295e18c515a03e81e4d7b63c07b..82dd1bc0d3fdd0149ced5ce3f2cf9ae480c9f2b7 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -4,7 +4,7 @@ //! This then is used to provide those locations to language servers & determine locations eligible for toolchain selection. mod manifest_store; -mod path_trie; +pub mod path_trie; mod server_tree; use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, sync::Arc}; diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 9710bb46d022382819d1edf7e84b85b5f325cdb7..99b4d523e0eff21c4e483d4fa5497d3403087a19 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -13,7 +13,7 @@ use util::rel_path::RelPath; /// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. /// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is /// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories. -pub(super) struct RootPathTrie