diff --git a/crates/languages/src/package_json.rs b/crates/languages/src/package_json.rs index 8c1cb9f068d34873a4cd27c1b2f21deb236c789d..80e9e3cc0d5789b79592fdb490089b8d2f7879eb 100644 --- a/crates/languages/src/package_json.rs +++ b/crates/languages/src/package_json.rs @@ -15,6 +15,8 @@ pub struct PackageJsonData { pub mocha_package_path: Option>, pub vitest_package_path: Option>, pub jasmine_package_path: Option>, + pub bun_package_path: Option>, + pub node_package_path: Option>, pub scripts: BTreeSet<(Arc, String)>, pub package_manager: Option<&'static str>, } @@ -35,6 +37,8 @@ impl PackageJsonData { let mut mocha_package_path = None; let mut vitest_package_path = None; let mut jasmine_package_path = None; + let mut bun_package_path = None; + let mut node_package_path = None; if let Some(Value::Object(dependencies)) = package_json.get("devDependencies") { if dependencies.contains_key("jest") { jest_package_path.get_or_insert_with(|| path.clone()); @@ -48,6 +52,12 @@ impl PackageJsonData { if dependencies.contains_key("jasmine") { jasmine_package_path.get_or_insert_with(|| path.clone()); } + if dependencies.contains_key("@types/bun") { + bun_package_path.get_or_insert_with(|| path.clone()); + } + if dependencies.contains_key("@types/node") { + node_package_path.get_or_insert_with(|| path.clone()); + } } if let Some(Value::Object(dev_dependencies)) = package_json.get("dependencies") { if dev_dependencies.contains_key("jest") { @@ -62,6 +72,12 @@ impl PackageJsonData { if dev_dependencies.contains_key("jasmine") { jasmine_package_path.get_or_insert_with(|| path.clone()); } + if dev_dependencies.contains_key("@types/bun") { + bun_package_path.get_or_insert_with(|| path.clone()); + } + if dev_dependencies.contains_key("@types/node") { + node_package_path.get_or_insert_with(|| path.clone()); + } } let package_manager = package_json @@ -74,6 +90,8 @@ impl PackageJsonData { Some("yarn") } else if value.starts_with("npm") { Some("npm") + } else if value.starts_with("bun") { + Some("bun") } else { None } @@ -84,6 +102,8 @@ impl PackageJsonData { mocha_package_path, vitest_package_path, jasmine_package_path, + bun_package_path, + node_package_path, scripts, package_manager, } @@ -100,6 +120,8 @@ impl PackageJsonData { .jasmine_package_path .take() .or(other.jasmine_package_path); + self.bun_package_path = self.bun_package_path.take().or(other.bun_package_path); + self.node_package_path = self.node_package_path.take().or(other.node_package_path); self.scripts.extend(other.scripts); self.package_manager = self.package_manager.or(other.package_manager); } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 68fb11bf3526e6e4301d118e6be33dfcc3b3ee2c..3a4eb259261c5a26f55da388b2a62504cdeced1a 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -54,6 +54,12 @@ const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName = const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH")); +const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH")); + +const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_PACKAGE_PATH")); + #[derive(Clone, Debug, Default)] struct PackageJsonContents(Arc>>); @@ -220,6 +226,65 @@ impl PackageJsonData { }); } + if self.bun_package_path.is_some() { + task_templates.0.push(TaskTemplate { + label: format!("{} file test", "bun test".to_owned()), + command: "bun".to_owned(), + args: vec!["test".to_owned(), VariableName::File.template_value()], + cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()), + ..TaskTemplate::default() + }); + task_templates.0.push(TaskTemplate { + label: format!("bun test {}", VariableName::Symbol.template_value(),), + command: "bun".to_owned(), + args: vec![ + "test".to_owned(), + "--test-name-pattern".to_owned(), + format!("\"{}\"", VariableName::Symbol.template_value()), + VariableName::File.template_value(), + ], + tags: vec![ + "ts-test".to_owned(), + "js-test".to_owned(), + "tsx-test".to_owned(), + ], + cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()), + ..TaskTemplate::default() + }); + } + + if self.node_package_path.is_some() { + task_templates.0.push(TaskTemplate { + label: format!("{} file test", "node test".to_owned()), + command: "node".to_owned(), + args: vec!["--test".to_owned(), VariableName::File.template_value()], + tags: vec![ + "ts-test".to_owned(), + "js-test".to_owned(), + "tsx-test".to_owned(), + ], + cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()), + ..TaskTemplate::default() + }); + task_templates.0.push(TaskTemplate { + label: format!("node test {}", VariableName::Symbol.template_value()), + command: "node".to_owned(), + args: vec![ + "--test".to_owned(), + "--test-name-pattern".to_owned(), + format!("\"{}\"", VariableName::Symbol.template_value()), + VariableName::File.template_value(), + ], + tags: vec![ + "ts-test".to_owned(), + "js-test".to_owned(), + "tsx-test".to_owned(), + ], + cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()), + ..TaskTemplate::default() + }); + } + let script_name_counts: HashMap<_, usize> = self.scripts .iter() @@ -493,6 +558,26 @@ impl ContextProvider for TypeScriptContextProvider { .to_string(), ); } + + if let Some(path) = package_json_data.bun_package_path { + vars.insert( + TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE, + path.parent() + .unwrap_or(Path::new("")) + .to_string_lossy() + .to_string(), + ); + } + + if let Some(path) = package_json_data.node_package_path { + vars.insert( + TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE, + path.parent() + .unwrap_or(Path::new("")) + .to_string_lossy() + .to_string(), + ); + } } } Ok(vars) @@ -1178,6 +1263,8 @@ mod tests { mocha_package_path: Some(Path::new(path!("/root/package.json")).into()), vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()), jasmine_package_path: None, + bun_package_path: None, + node_package_path: None, scripts: [ ( Path::new(path!("/root/package.json")).into(), @@ -1231,6 +1318,7 @@ mod tests { ] ); } + #[test] fn test_escaping_name() { let cases = [ @@ -1264,4 +1352,110 @@ mod tests { assert_eq!(replace_test_name_parameters(input), expected); } } + + // The order of test runner tasks is based on inferred user preference: + // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized. + // 2. Bun's built-in test runner (`bun test`) comes next. + // 3. Node.js's built-in test runner (`node --test`) is last. + // This hierarchy assumes that if a dedicated test framework is installed, it is the + // preferred testing mechanism. Between runtime-specific options, `bun test` is + // typically preferred over `node --test` when @types/bun is present. + #[gpui::test] + async fn test_task_ordering_with_multiple_test_runners( + executor: BackgroundExecutor, + cx: &mut TestAppContext, + ) { + cx.update(|cx| { + settings::init(cx); + Project::init_settings(cx); + language_settings::init(cx); + }); + + // Test case with all test runners present + let package_json_all_runners = json!({ + "devDependencies": { + "@types/bun": "1.0.0", + "@types/node": "^20.0.0", + "jest": "29.0.0", + "mocha": "10.0.0", + "vitest": "1.0.0", + "jasmine": "5.0.0", + }, + "scripts": { + "test": "jest" + } + }) + .to_string(); + + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/root"), + json!({ + "package.json": package_json_all_runners, + "file.js": "", + }), + ) + .await; + + let provider = TypeScriptContextProvider::new(fs.clone()); + + let package_json_data = cx + .update(|cx| { + provider.combined_package_json_data( + fs.clone(), + path!("/root").as_ref(), + rel_path("file.js"), + cx, + ) + }) + .await + .unwrap(); + + assert!(package_json_data.jest_package_path.is_some()); + assert!(package_json_data.mocha_package_path.is_some()); + assert!(package_json_data.vitest_package_path.is_some()); + assert!(package_json_data.jasmine_package_path.is_some()); + assert!(package_json_data.bun_package_path.is_some()); + assert!(package_json_data.node_package_path.is_some()); + + let mut task_templates = TaskTemplates::default(); + package_json_data.fill_task_templates(&mut task_templates); + + let test_tasks: Vec<_> = task_templates + .0 + .iter() + .filter(|template| { + template.tags.contains(&"ts-test".to_owned()) + || template.tags.contains(&"js-test".to_owned()) + }) + .map(|template| &template.label) + .collect(); + + let node_test_index = test_tasks + .iter() + .position(|label| label.contains("node test")); + let jest_test_index = test_tasks.iter().position(|label| label.contains("jest")); + let bun_test_index = test_tasks + .iter() + .position(|label| label.contains("bun test")); + + assert!( + node_test_index.is_some(), + "Node test tasks should be present" + ); + assert!( + jest_test_index.is_some(), + "Jest test tasks should be present" + ); + assert!(bun_test_index.is_some(), "Bun test tasks should be present"); + + assert!( + jest_test_index.unwrap() < bun_test_index.unwrap(), + "Jest should come before Bun" + ); + assert!( + bun_test_index.unwrap() < node_test_index.unwrap(), + "Bun should come before Node" + ); + } } diff --git a/docs/src/languages/javascript.md b/docs/src/languages/javascript.md index 8926e5c3736f0c8fa5b3ed6bc5b1b7d34ce15cd6..6c7eff5f38977d2d52417fd3491fc4af6da52e9a 100644 --- a/docs/src/languages/javascript.md +++ b/docs/src/languages/javascript.md @@ -179,10 +179,14 @@ Zed supports debugging JavaScript code out of the box. The following can be debugged without writing additional configuration: - Tasks from `package.json` -- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine) +- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node) Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks. +> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`. +> +> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+). + As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed. If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations. diff --git a/docs/src/languages/typescript.md b/docs/src/languages/typescript.md index e6ed5a83d204ae6ae14911be7c7e830b4a4ae1aa..8df4a1685d1dc75a6c9ae32d8ee77a3b09599c0b 100644 --- a/docs/src/languages/typescript.md +++ b/docs/src/languages/typescript.md @@ -162,10 +162,14 @@ Zed supports debugging TypeScript code out of the box. The following can be debugged without writing additional configuration: - Tasks from `package.json` -- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine) +- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node) Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks. +> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`. +> +> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+). + As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed. If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations.