Skip to content

Commit 099a6ab

Browse files
committed
Implement Index
1 parent b2e0d29 commit 099a6ab

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
pub mod library;
99
pub mod package;
1010
pub mod package_description;
11+
pub mod package_index;
1112
pub mod package_namespace;
1213
pub mod source_root;
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//
2+
// package_index.rs
3+
//
4+
// Copyright (C) 2025 by Posit Software, PBC
5+
//
6+
//
7+
8+
use std::path::Path;
9+
10+
pub struct Index {
11+
pub names: Vec<String>,
12+
}
13+
14+
impl Index {
15+
pub fn load_from_folder(path: &Path) -> anyhow::Result<Self> {
16+
if !path.is_dir() {
17+
return Err(anyhow::anyhow!(
18+
"Can't load index as '{path}' is not a folder",
19+
path = path.to_string_lossy()
20+
));
21+
}
22+
23+
let index_path = path.join("INDEX");
24+
if !index_path.is_file() {
25+
return Err(anyhow::anyhow!(
26+
"Can't load index as '{path}' does not contain an INDEX file",
27+
path = path.to_string_lossy()
28+
));
29+
}
30+
31+
let contents = std::fs::read_to_string(&index_path)?;
32+
Ok(Index::parse(&contents))
33+
}
34+
35+
/// Parses a package index text, extracting valid R symbol names from the first column.
36+
/// Only names starting at the beginning of a line and consisting of letters, digits, dots, or underscores are included.
37+
pub fn parse(input: &str) -> Self {
38+
let valid_name = regex::Regex::new(r"^[A-Za-z.][A-Za-z0-9._]*$").unwrap();
39+
let mut names = Vec::new();
40+
41+
for line in input.lines() {
42+
// Only consider lines that start at column 0 (no leading whitespace)
43+
if line.starts_with(char::is_whitespace) {
44+
continue;
45+
}
46+
if let Some(first_col) = line.split_whitespace().next() {
47+
if valid_name.is_match(first_col) {
48+
names.push(first_col.to_string());
49+
}
50+
}
51+
}
52+
53+
Index { names }
54+
}
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use std::fs;
60+
61+
use tempfile::tempdir;
62+
63+
use super::*;
64+
65+
#[test]
66+
fn test_index_parses_simple_names() {
67+
let input = "\
68+
foo Description of foo
69+
bar Description of bar
70+
baz Description of baz
71+
";
72+
let idx = Index::parse(input);
73+
assert_eq!(idx.names, vec!["foo", "bar", "baz"]);
74+
}
75+
76+
#[test]
77+
fn test_index_ignores_continuation_lines() {
78+
let input = "\
79+
foo Description of foo
80+
Continuation of description
81+
bar Description of bar
82+
";
83+
let idx = Index::parse(input);
84+
assert_eq!(idx.names, vec!["foo", "bar"]);
85+
}
86+
87+
#[test]
88+
fn test_index_parses_names_with_dots_and_underscores() {
89+
let input = "\
90+
foo.bar Description
91+
foo_bar Description
92+
.foo Description
93+
";
94+
let idx = Index::parse(input);
95+
assert_eq!(idx.names, vec!["foo.bar", "foo_bar", ".foo"]);
96+
}
97+
98+
#[test]
99+
fn test_index_skips_names_with_dashes() {
100+
let input = "\
101+
foo-bar Description
102+
baz Description
103+
foo Description
104+
";
105+
let idx = Index::parse(input);
106+
assert_eq!(idx.names, vec!["baz", "foo"]);
107+
}
108+
109+
#[test]
110+
fn test_index_skips_lines_without_valid_names() {
111+
let input = "\
112+
123foo Not a valid name
113+
_bar Not a valid name
114+
.foo Valid name
115+
";
116+
let idx = Index::parse(input);
117+
assert_eq!(idx.names, vec![".foo"]);
118+
}
119+
120+
#[test]
121+
fn test_index_parses_realistic_package_index() {
122+
let input = "\
123+
.prt.methTit Print and Summary Method Utilities for Mixed
124+
Effects
125+
Arabidopsis Arabidopsis clipping/fertilization data
126+
Dyestuff Yield of dyestuff by batch
127+
GHrule Univariate Gauss-Hermite quadrature rule
128+
NelderMead-class Class '\"NelderMead\"' of Nelder-Mead optimizers
129+
and its Generator
130+
Nelder_Mead Nelder-Mead Optimization of Parameters,
131+
Possibly (Box) Constrained
132+
";
133+
let idx = Index::parse(input);
134+
assert_eq!(idx.names, vec![
135+
".prt.methTit",
136+
"Arabidopsis",
137+
"Dyestuff",
138+
"GHrule",
139+
"Nelder_Mead"
140+
]);
141+
}
142+
143+
#[test]
144+
fn load_from_folder_returns_errors() {
145+
// From a file
146+
let file = tempfile::NamedTempFile::new().unwrap();
147+
let result = Index::load_from_folder(file.path());
148+
assert!(result.is_err());
149+
150+
// From a dir without an INDEX file
151+
let dir = tempdir().unwrap();
152+
let result = Index::load_from_folder(dir.path());
153+
assert!(result.is_err());
154+
}
155+
156+
#[test]
157+
fn load_from_folder_reads_and_parses_index() {
158+
let dir = tempdir().unwrap();
159+
let index_path = dir.path().join("INDEX");
160+
let content = "\
161+
foo Description of foo
162+
bar Description of bar
163+
";
164+
fs::write(&index_path, content).unwrap();
165+
166+
let idx = Index::load_from_folder(dir.path()).unwrap();
167+
assert_eq!(idx.names, vec!["foo", "bar"]);
168+
}
169+
}

0 commit comments

Comments
 (0)