Skip to content

Commit f749b8e

Browse files
committed
Implement Documentation
1 parent f5b3979 commit f749b8e

File tree

5 files changed

+167
-23
lines changed

5 files changed

+167
-23
lines changed

crates/ark/src/lsp/diagnostics.rs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,11 +1657,7 @@ foo
16571657
depends: vec![],
16581658
fields: Dcf::new(),
16591659
};
1660-
let package = Package {
1661-
path: PathBuf::from("/mock/path"),
1662-
description,
1663-
namespace,
1664-
};
1660+
let package = Package::new(PathBuf::from("/mock/path"), description, namespace);
16651661

16661662
// Create a library with `mockpkg` installed
16671663
let library = Library::new(vec![]).insert("mockpkg", package);
@@ -1757,11 +1753,7 @@ foo
17571753
depends: vec![],
17581754
fields: Dcf::new(),
17591755
};
1760-
let package1 = Package {
1761-
path: PathBuf::from("/mock/path1"),
1762-
description: description1,
1763-
namespace: namespace1,
1764-
};
1756+
let package1 = Package::new(PathBuf::from("/mock/path1"), description1, namespace1);
17651757

17661758
// pkg2 exports `bar` and `baz`
17671759
let namespace2 = Namespace {
@@ -1775,11 +1767,7 @@ foo
17751767
depends: vec![],
17761768
fields: Dcf::new(),
17771769
};
1778-
let package2 = Package {
1779-
path: PathBuf::from("/mock/path2"),
1780-
description: description2,
1781-
namespace: namespace2,
1782-
};
1770+
let package2 = Package::new(PathBuf::from("/mock/path2"), description2, namespace2);
17831771

17841772
let library = Library::new(vec![])
17851773
.insert("pkg1", package1)
@@ -1835,11 +1823,7 @@ foo
18351823
depends: vec![],
18361824
fields: Dcf::new(),
18371825
};
1838-
let package = Package {
1839-
path: PathBuf::from("/mock/path"),
1840-
description,
1841-
namespace,
1842-
};
1826+
let package = Package::new(PathBuf::from("/mock/path"), description, namespace);
18431827

18441828
let library = Library::new(vec![]).insert("pkg", package);
18451829

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::path::Path;
2+
3+
use crate::lsp::inputs::documentation_rd_file::RdFile;
4+
5+
#[derive(Default, Clone, Debug)]
6+
pub struct Documentation {
7+
pub rd_files: Vec<RdFile>,
8+
}
9+
10+
impl Documentation {
11+
/// Load .Rd files from the man directory
12+
pub fn load_from_folder(path: &Path) -> anyhow::Result<Self> {
13+
if !path.is_dir() {
14+
return Ok(Documentation::default());
15+
}
16+
17+
let mut rd_files = Vec::new();
18+
19+
for entry in std::fs::read_dir(&path)? {
20+
let entry = entry?;
21+
let path = entry.path();
22+
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("Rd") {
23+
rd_files.push(RdFile::load_from_file(&path)?);
24+
}
25+
}
26+
27+
Ok(Documentation { rd_files })
28+
}
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use std::fs::File;
34+
use std::io::Write;
35+
36+
use tempfile::tempdir;
37+
38+
use super::*;
39+
use crate::lsp::inputs::documentation_rd_file::RdDocType;
40+
41+
fn create_rd_file(dir: &std::path::Path, name: &str, content: &str) {
42+
let file_path = dir.join(name);
43+
let mut file = File::create(&file_path).unwrap();
44+
file.write_all(content.as_bytes()).unwrap();
45+
}
46+
47+
#[test]
48+
fn test_load_from_folder_with_rd_files() {
49+
let dir = tempdir().unwrap();
50+
create_rd_file(dir.path(), "foo.Rd", "\\name{foo}\n\\docType{data}");
51+
create_rd_file(dir.path(), "bar.Rd", "\\name{bar}\n\\docType{package}");
52+
create_rd_file(dir.path(), "baz.Rd", "\\name{baz}\n% Some Rd file");
53+
create_rd_file(dir.path(), "qux.txt", "Not an Rd file");
54+
55+
let documentation = Documentation::load_from_folder(dir.path()).unwrap();
56+
assert_eq!(documentation.rd_files.len(), 3);
57+
58+
let doc_types: Vec<(Option<String>, Option<RdDocType>)> = documentation
59+
.rd_files
60+
.into_iter()
61+
.map(|rd| (rd.name, rd.doc_type))
62+
.collect();
63+
64+
assert!(doc_types.contains(&(Some(String::from("foo")), Some(RdDocType::Data))));
65+
assert!(doc_types.contains(&(Some(String::from("bar")), Some(RdDocType::Package))));
66+
assert!(doc_types.contains(&(Some(String::from("baz")), None)));
67+
}
68+
69+
#[test]
70+
fn test_load_from_folder_empty_or_nonexistent() {
71+
let dir = tempdir().unwrap();
72+
73+
// No files in directory
74+
let documentation = Documentation::load_from_folder(dir.path()).unwrap();
75+
assert_eq!(documentation.rd_files.len(), 0);
76+
77+
// Nonexistent directory
78+
let nonexistent = dir.path().join("does_not_exist");
79+
let documentation = Documentation::load_from_folder(&nonexistent).unwrap();
80+
assert_eq!(documentation.rd_files.len(), 0);
81+
}
82+
}

crates/ark/src/lsp/inputs/documentation_rd_file.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
use std::fs;
22
use std::path::Path;
33

4+
#[derive(Clone, Debug)]
45
pub struct RdFile {
6+
/// `name` and `title` are mandatory in a properly written documentation
7+
/// file but we make them optional here in case they are missing. We
8+
/// currently don't load `title` because it's possible it contains escaped
9+
/// `\` characters and we don't have a proper Rd parser yet.
10+
pub name: Option<String>,
511
pub doc_type: Option<RdDocType>,
612
}
713

8-
#[derive(Debug, PartialEq)]
14+
#[derive(Clone, Debug, PartialEq)]
915
pub enum RdDocType {
1016
Data,
1117
Package,
@@ -20,11 +26,23 @@ impl RdFile {
2026
)
2127
})?;
2228

29+
let name = parse_name(&content);
2330
let doc_type = parse_doc_type(&content);
24-
Ok(RdFile { doc_type })
31+
32+
Ok(RdFile { name, doc_type })
2533
}
2634
}
2735

36+
fn parse_name(content: &str) -> Option<String> {
37+
static RE: std::sync::LazyLock<regex::Regex> =
38+
std::sync::LazyLock::new(|| regex::Regex::new(r"\\name\{([a-zA-Z0-9_]+)\}").unwrap());
39+
40+
let captures = RE.captures(content)?;
41+
let name = captures.get(1)?.as_str().to_string();
42+
43+
Some(name)
44+
}
45+
2846
fn parse_doc_type(content: &str) -> Option<RdDocType> {
2947
static RE: std::sync::LazyLock<regex::Regex> =
3048
std::sync::LazyLock::new(|| regex::Regex::new(r"\\docType\{(data|package)\}").unwrap());
@@ -42,6 +60,44 @@ fn parse_doc_type(content: &str) -> Option<RdDocType> {
4260
mod tests {
4361
use super::*;
4462

63+
#[test]
64+
fn test_parse_name_basic() {
65+
let input = r#"\name{foobar}"#;
66+
assert_eq!(parse_name(input), Some("foobar".to_string()));
67+
68+
let input = r#"
69+
% Some Rd file
70+
\name{mydata}
71+
\docType{data}
72+
"#;
73+
assert_eq!(parse_name(input), Some("mydata".to_string()));
74+
}
75+
76+
#[test]
77+
fn test_parse_name_escaped_brace() {
78+
let input = r#"\name{foo\}bar}"#;
79+
assert_eq!(parse_name(input), None);
80+
}
81+
82+
#[test]
83+
fn test_parse_name_missing() {
84+
let input = r#"
85+
% typo
86+
\namee{data}
87+
"#;
88+
assert_eq!(parse_name(input), None);
89+
}
90+
91+
#[test]
92+
fn test_parse_name_multiple_names() {
93+
let input = r#"
94+
\name{first}
95+
\name{second}
96+
"#;
97+
// Should match the first occurrence
98+
assert_eq!(parse_name(input), Some("first".to_string()));
99+
}
100+
45101
#[test]
46102
fn test_parse_doc_type_data() {
47103
let input = r#"\docType{data}"#;

crates/ark/src/lsp/inputs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//
66
//
77

8+
pub mod documentation;
89
pub mod documentation_rd_file;
910
pub mod library;
1011
pub mod package;

crates/ark/src/lsp/inputs/package.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use std::fs;
99
use std::path::PathBuf;
1010

11+
use crate::lsp::inputs::documentation::Documentation;
1112
use crate::lsp::inputs::package_description::Description;
1213
use crate::lsp::inputs::package_namespace::Namespace;
1314

@@ -20,13 +21,22 @@ pub struct Package {
2021

2122
pub description: Description,
2223
pub namespace: Namespace,
24+
pub documentation: Documentation,
2325
}
2426

2527
impl Package {
28+
pub fn new(path: PathBuf, description: Description, namespace: Namespace) -> Self {
29+
Self {
30+
path,
31+
description,
32+
namespace,
33+
documentation: Default::default(),
34+
}
35+
}
36+
2637
/// Load a package from a given path.
2738
pub fn load_from_folder(package_path: &std::path::Path) -> anyhow::Result<Option<Self>> {
2839
let description_path = package_path.join("DESCRIPTION");
29-
let namespace_path = package_path.join("NAMESPACE");
3040

3141
// Only consider directories that contain a description file
3242
if !description_path.is_file() {
@@ -38,6 +48,7 @@ impl Package {
3848
let description_contents = fs::read_to_string(&description_path)?;
3949
let description = Description::parse(&description_contents)?;
4050

51+
let namespace_path = package_path.join("NAMESPACE");
4152
let namespace = if namespace_path.is_file() {
4253
let namespace_contents = fs::read_to_string(&namespace_path)?;
4354
Namespace::parse(&namespace_contents)?
@@ -49,10 +60,20 @@ impl Package {
4960
Namespace::default()
5061
};
5162

63+
let documentation_path = package_path.join("man");
64+
let documentation = match Documentation::load_from_folder(&documentation_path) {
65+
Ok(documentation) => documentation,
66+
Err(err) => {
67+
tracing::warn!("Can't load package documentation: {err:?}");
68+
Documentation::default()
69+
},
70+
};
71+
5272
Ok(Some(Package {
5373
path: package_path.to_path_buf(),
5474
description,
5575
namespace,
76+
documentation,
5677
}))
5778
}
5879

0 commit comments

Comments
 (0)