diff --git a/Cargo.lock b/Cargo.lock index 3fbb80c90..2d9be36f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1557,6 +1557,7 @@ dependencies = [ "once_cell", "parking_lot", "rustix 1.0.3", + "quick-xml", "serde", "serde_json", "slotmap", @@ -2121,6 +2122,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quickcheck" version = "1.0.3" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index bcee1a0a7..bbc8929c7 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -51,6 +51,7 @@ log = "~0.4" parking_lot.workspace = true thiserror.workspace = true +quick-xml = { version = "0.31.0", features = ["serialize"] } [target.'cfg(windows)'.dependencies] clipboard-win = { version = "5.4", features = ["std"] } diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs new file mode 100644 index 000000000..cafca5d89 --- /dev/null +++ b/helix-view/src/coverage.rs @@ -0,0 +1,162 @@ +use quick_xml::de::from_reader; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; + +pub struct Coverage { + pub files: HashMap, +} + +pub struct FileCoverage { + pub lines: HashMap, +} + +#[derive(Deserialize, Debug)] +struct RawCoverage { + #[serde(rename = "@version")] + version: String, + sources: Sources, + packages: Packages, +} + +#[derive(Deserialize, Debug)] +struct Sources { + source: Vec, +} + +#[derive(Deserialize, Debug)] +struct Source { + #[serde(rename = "$value")] + name: String, +} + +#[derive(Deserialize, Debug)] +struct Packages { + package: Vec, +} + +#[derive(Deserialize, Debug)] +struct Package { + #[serde(rename = "@name")] + name: String, + classes: Classes, +} + +#[derive(Deserialize, Debug)] +struct Classes { + class: Vec, +} + +#[derive(Deserialize, Debug)] +struct Class { + #[serde(rename = "@name")] + name: String, + #[serde(rename = "@filename")] + filename: String, + lines: Lines, +} + +#[derive(Deserialize, Debug)] +struct Lines { + line: Vec, +} + +#[derive(Deserialize, Debug)] +struct Line { + #[serde(rename = "@number")] + number: u32, + #[serde(rename = "@hits")] + hits: u32, +} + +pub fn parse(path: std::path::PathBuf) -> Option { + let file = File::open(path).ok()?; + let reader = BufReader::new(file); + let tmp: RawCoverage = from_reader(reader).ok()?; + Some(tmp.into()) +} + +impl From for Coverage { + fn from(coverage: RawCoverage) -> Self { + let mut files = HashMap::new(); + for package in coverage.packages.package { + for class in package.classes.class { + let mut lines = HashMap::new(); + for line in class.lines.line { + lines.insert(line.number - 1, line.hits > 0); + } + for source in &coverage.sources.source { + let path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); + if path.exists() { + files.insert(path, FileCoverage { lines }); + break; + } + } + } + } + Coverage { files } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quick_xml::de::from_str; + use std::path::PathBuf; + static TEST_STRING: &str = r#" + + + a_src + + + + + + + + + + + + + + + + + + + +"#; + + #[test] + fn test_deserialize_raw_coverage_from_string() { + let result: RawCoverage = from_str(TEST_STRING).unwrap(); + println!("result is {:?}", result); + assert_eq!(result.version, "7.3.0"); + assert_eq!(result.sources.source[0].name, "a_src"); + assert_eq!(result.packages.package[0].name, "a package"); + let class = &result.packages.package[0].classes.class[0]; + assert_eq!(class.name, "a class"); + assert_eq!(class.filename, "file.ext"); + assert_eq!(class.lines.line[0].number, 3); + assert_eq!(class.lines.line[0].hits, 1); + assert_eq!(class.lines.line[1].number, 5); + assert_eq!(class.lines.line[1].hits, 0); + } + + #[test] + fn test_convert_raw_coverage_to_coverage() { + let tmp: RawCoverage = from_str(TEST_STRING).unwrap(); + let result: Coverage = tmp.into(); + assert_eq!(result.files.len(), 2); + let first = result.files.get(&PathBuf::from("a_src/file.ext")).unwrap(); + assert!(first.lines.get(&0).is_none()); + assert_eq!(first.lines.get(&3), Some(&true)); + assert_eq!(first.lines.get(&5), Some(&false)); + let second = result.files.get(&PathBuf::from("a_src/other.ext")).unwrap(); + assert!(second.lines.get(&3).is_none()); + assert_eq!(second.lines.get(&1), Some(&false)); + assert_eq!(second.lines.get(&7), Some(&true)); + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 41c9ee1ef..5c68c62ac 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -152,7 +152,7 @@ pub struct Document { /// update from the LSP pub inlay_hints_oudated: bool, - path: Option, + pub path: Option, relative_path: OnceCell>, encoding: &'static encoding::Encoding, has_bom: bool, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index be2218997..52e6ede09 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -420,7 +420,7 @@ pub fn get_terminal_provider() -> Option { }) } -#[cfg(not(any(windows, target_arch = "wasm32")))] +#[cfg(not(any(windows, target_os = "wasm32")))] pub fn get_terminal_provider() -> Option { use helix_stdx::env::{binary_exists, env_var_is_set}; @@ -711,6 +711,8 @@ pub enum GutterType { Spacer, /// Highlight local changes Diff, + /// Highlight local changes + Coverage, } impl std::str::FromStr for GutterType { @@ -722,8 +724,9 @@ impl std::str::FromStr for GutterType { "spacer" => Ok(Self::Spacer), "line-numbers" => Ok(Self::LineNumbers), "diff" => Ok(Self::Diff), + "coverage" => Ok(Self::Coverage), _ => anyhow::bail!( - "Gutter type can only be `diagnostics`, `spacer`, `line-numbers` or `diff`." + "Gutter type can only be `diagnostics`, `spacer`, `line-numbers`, `diff` or `coverage`." ), } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 665a78bcc..f47343207 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,8 +1,9 @@ -use std::fmt::Write; +use std::{fmt::Write, path::PathBuf}; use helix_core::syntax::LanguageServerFeature; use crate::{ + coverage, editor::GutterType, graphics::{Style, UnderlineStyle}, Document, Editor, Theme, View, @@ -32,6 +33,7 @@ impl GutterType { GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused), GutterType::Spacer => padding(editor, doc, view, theme, is_focused), GutterType::Diff => diff(editor, doc, view, theme, is_focused), + GutterType::Coverage => coverage(editor, doc, view, theme, is_focused), } } @@ -41,6 +43,7 @@ impl GutterType { GutterType::LineNumbers => line_numbers_width(view, doc), GutterType::Spacer => 1, GutterType::Diff => 1, + GutterType::Coverage => 1, } } } @@ -139,6 +142,50 @@ pub fn diff<'doc>( } } +pub fn coverage<'doc>( + _editor: &'doc Editor, + doc: &'doc Document, + _view: &View, + theme: &Theme, + _is_focused: bool, +) -> GutterFn<'doc> { + let covered = theme.get("diff.plus.gutter"); + let not_covered = theme.get("diff.minus.gutter"); + if let Some(cov) = coverage::parse(PathBuf::from("report/coverage.xml")) { + if let Some(mut path) = doc.path.clone() { + if let Ok(cwd) = std::env::current_dir() { + if let Ok(tmp) = path.strip_prefix(cwd) { + path = tmp.into(); + } + } + if let Some(file) = cov.files.get(&path) { + let this_file = coverage::FileCoverage { + lines: file.lines.clone(), + }; + return Box::new( + move |line: usize, + _selected: bool, + _first_visual_line: bool, + out: &mut String| { + if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) + } else { + None + } + }, + ); + } + } + } + return Box::new(move |_, _, _, _| None); +} + pub fn line_numbers<'doc>( editor: &'doc Editor, doc: &'doc Document, diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index e30a23381..1424d6e09 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -4,6 +4,7 @@ pub mod macros; pub mod annotations; pub mod base64; pub mod clipboard; +pub mod coverage; pub mod document; pub mod editor; pub mod events;