diff --git a/Cargo.lock b/Cargo.lock index 8ad9039..6ba8eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "auditable-serde" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "cfg-if" @@ -179,19 +179,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "heck" version = "0.5.0" @@ -282,9 +276,9 @@ dependencies = [ [[package]] name = "id-arena" -version = "2.3.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "idna" @@ -309,21 +303,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown", "serde", - "serde_core", ] [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "leb128fmt" @@ -339,15 +332,15 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.29" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" @@ -398,30 +391,29 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] [[package]] name = "semver" -version = "1.0.28" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", - "serde_core", ] [[package]] @@ -456,9 +448,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -481,15 +473,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "spdx" -version = "0.10.9" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" dependencies = [ "smallvec", ] @@ -540,9 +532,9 @@ checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" @@ -604,7 +596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ "bitflags", - "hashbrown 0.15.5", + "hashbrown", "indexmap", "semver", ] @@ -753,6 +745,7 @@ dependencies = [ name = "zed_haskell" version = "0.3.2" dependencies = [ + "serde_json", "zed_extension_api", ] diff --git a/Cargo.toml b/Cargo.toml index 2b4f363..8cddda7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ crate-type = ["cdylib"] [dependencies] zed_extension_api = "0.7.0" + +[dev-dependencies] +serde_json = "1.0.150" diff --git a/languages/haskell/runnables.scm b/languages/haskell/runnables.scm new file mode 100644 index 0000000..7b9959d --- /dev/null +++ b/languages/haskell/runnables.scm @@ -0,0 +1,11 @@ +; Detect the main function +(declarations + [ + (function + name: (variable) @run) + (bind + name: (variable) @run) + ] + (#eq? @run "main") + (#set! tag haskell-build) + (#set! tag haskell-run)) diff --git a/languages/haskell/tasks.json b/languages/haskell/tasks.json new file mode 100644 index 0000000..58d86e7 --- /dev/null +++ b/languages/haskell/tasks.json @@ -0,0 +1,26 @@ +[ + { + "label": "Run", + "command": "if [ -f stack.yaml ]; then stack run; elif [ -f cabal.project ] || ls *.cabal 2>/dev/null | grep -q .; then cabal run; else echo 'No Stack or Cabal found to run the code' >&2; exit 1; fi", + "tags": ["haskell-run"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + }, + "show_command": false + }, + { + "label": "Build", + "command": "if [ -f stack.yaml ]; then stack build; elif [ -f cabal.project ] || ls *.cabal 2>/dev/null | grep -q .; then cabal build; else echo 'No Stack or Cabal found to build the code' >&2; exit 1; fi", + "tags": ["haskell-build"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + }, + "show_command": false + } +] diff --git a/tests/task_verification_test.rs b/tests/task_verification_test.rs new file mode 100644 index 0000000..c62815e --- /dev/null +++ b/tests/task_verification_test.rs @@ -0,0 +1,169 @@ +// Heavily inspired by the Java extension's task verification tests: +// https://github.com/zed-extensions/java/blob/b6b8f9a5396b7f794d12a91ee4c703078ecc3e96/tests/task_verification_test.rs + +use serde_json::Value; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn get_task_command_by_tag(tag: &str) -> String { + let tasks_json = + fs::read_to_string("languages/haskell/tasks.json").expect("Failed to read tasks.json"); + let tasks: Value = serde_json::from_str(&tasks_json).expect("Failed to parse tasks.json"); + for task in tasks.as_array().expect("tasks.json is not an array") { + if let Some(tags) = task["tags"].as_array() { + if tags.iter().any(|t| t.as_str() == Some(tag)) { + return task["command"] + .as_str() + .expect("command is not a string") + .to_string(); + } + } + } + panic!("Task with tag '{tag}' not found"); +} + +struct TestProject { + temp_dir: PathBuf, + bin_dir: PathBuf, + new_path: String, +} + +impl TestProject { + fn new(name: &str) -> Self { + let temp_dir = std::env::temp_dir().join(format!("haskell_task_test_{name}")); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir).unwrap(); + } + fs::create_dir_all(&temp_dir).unwrap(); + let bin_dir = temp_dir.join("bin"); + fs::create_dir_all(&bin_dir).unwrap(); + let new_path = format!( + "{}:{}", + bin_dir.to_string_lossy(), + std::env::var("PATH").unwrap_or_default() + ); + Self { + temp_dir, + bin_dir, + new_path, + } + } + + fn with_stack(&self) { + fs::File::create(self.temp_dir.join("stack.yaml")).unwrap(); + fs::File::create(self.temp_dir.join("myproject.cabal")).unwrap(); + self.mock_bin("stack", "#!/bin/sh\necho \"STACK_CALLED: $@\""); + } + + fn with_cabal(&self) { + fs::File::create(self.temp_dir.join("myproject.cabal")).unwrap(); + self.mock_bin("cabal", "#!/bin/sh\necho \"CABAL_CALLED: $@\""); + } + + fn with_cabal_project(&self) { + fs::File::create(self.temp_dir.join("cabal.project")).unwrap(); + self.mock_bin("cabal", "#!/bin/sh\necho \"CABAL_CALLED: $@\""); + } + + fn mock_bin(&self, name: &str, content: &str) { + let bin_path = self.bin_dir.join(name); + fs::write(&bin_path, content).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&bin_path, fs::Permissions::from_mode(0o755)).unwrap(); + } + } + + fn run_task(&self, tag: &str) -> String { + let command = get_task_command_by_tag(tag); + let output = Command::new("sh") + .arg("-c") + .arg(&command) + .env("PATH", &self.new_path) + .current_dir(&self.temp_dir) + .output() + .expect("Failed to execute shell command"); + String::from_utf8_lossy(&output.stdout).into_owned() + + &String::from_utf8_lossy(&output.stderr) + } +} + +impl Drop for TestProject { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.temp_dir); + } +} + +#[test] +fn test_stack_build() { + let project = TestProject::new("stack_build"); + project.with_stack(); + let output = project.run_task("haskell-build"); + assert!(output.contains("STACK_CALLED: build"), "Got: {}", output); +} + +#[test] +fn test_stack_run() { + let project = TestProject::new("stack_run"); + project.with_stack(); + let output = project.run_task("haskell-run"); + assert!(output.contains("STACK_CALLED: run"), "Got: {}", output); +} + +#[test] +fn test_cabal_build() { + let project = TestProject::new("cabal_build"); + project.with_cabal(); + let output = project.run_task("haskell-build"); + assert!(output.contains("CABAL_CALLED: build"), "Got: {}", output); +} + +#[test] +fn test_cabal_run() { + let project = TestProject::new("cabal_run"); + project.with_cabal(); + let output = project.run_task("haskell-run"); + assert!(output.contains("CABAL_CALLED: run"), "Got: {}", output); +} + +#[test] +fn test_cabal_project_build() { + let project = TestProject::new("cabal_project_build"); + project.with_cabal_project(); + let output = project.run_task("haskell-build"); + assert!(output.contains("CABAL_CALLED: build"), "Got: {}", output); +} + +#[test] +fn test_cabal_project_run() { + let project = TestProject::new("cabal_project_run"); + project.with_cabal_project(); + let output = project.run_task("haskell-run"); + assert!(output.contains("CABAL_CALLED: run"), "Got: {}", output); +} + +#[test] +fn test_stack_preferred_over_cabal() { + let project = TestProject::new("stack_preferred"); + project.with_stack(); + project.with_cabal(); + let output = project.run_task("haskell-run"); + assert!( + output.contains("STACK_CALLED: run"), + "Stack should be preferred when stack.yaml present. Got: {}", + output + ); +} + +#[test] +fn test_no_build_tool() { + let project = TestProject::new("no_tool"); + let output = project.run_task("haskell-build"); + assert!( + output.contains("No Stack or Cabal found"), + "Got: {}", + output + ); +}