use std::{
collections::HashMap,
ops::{Deref, DerefMut},
};
use tree_sitter::{Node, Parser, Tree};
use mylsp::prelude::*;
use crate::{
cargo_values::{props, tables},
metadata,
toml::{TomlCursor, parsing::Toml},
};
#[derive(Clone, Debug)]
pub struct Document {
uri: String,
text: String,
tree: Tree,
tables: Vec<String>,
keys: Vec<String>,
key_values: Vec<(String, String)>,
diagnostics: Diagnostics,
dep_features: HashMap<String, Vec<String>>,
}
impl Document {
pub fn new(uri: &str, text: String) -> Self {
let mut parser = Parser::new();
parser
.set_language(tree_sitter_toml::language())
.expect("Error loading toml grammar");
let tree = parser
.parse(&text, None)
.expect("Error parsing with tree-sitter");
let diagnostics = make_diagnostics(&text, &tree);
let tables = get_tables(&text);
let toml: Toml = text.as_str().into();
let key_values = toml.extract_all();
let keys = key_values.iter().map(|v| v.0.clone()).collect::<Vec<_>>();
Self {
uri: uri.to_string(),
text,
tree,
tables,
keys,
key_values,
diagnostics,
dep_features: get_deps_features(uri),
}
}
pub fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
pub fn completions_at(&self, pos: Position) -> CompletionList {
let mut tt: TomlCursor = self.cursor();
let mut items = vec![];
let toml: Toml = self.text.as_str().into();
let prefix = toml.prefix(pos);
let mut list = if prefix.is_empty() {
tables(self.tables.as_slice())
} else {
props(&prefix, self.keys.as_slice())
};
if tt.blank_line(pos) {
let mut tables = tables(self.tables.as_slice());
tables.append(&mut list);
list = tables;
}
list.append(&mut prefix_to_dep_features(&prefix, &self.dep_features));
for prop in list {
items.push(CompletionItem {
label: prop,
kind: Some(CompletionItemKind::Keyword),
detail: None,
});
}
CompletionList {
is_incomplete: false,
items,
}
}
fn cursor(&self) -> TomlCursor<'_> {
(self.text.as_str(), self.tree.walk()).into()
}
}
fn get_deps_features(uri: &str) -> HashMap<String, Vec<String>> {
let mut res = HashMap::new();
let metadata = metadata::get_metadata(uri);
for p in &metadata.packages {
let features = p
.features
.keys()
.filter(|f| *f != "default")
.cloned()
.collect::<Vec<_>>();
res.insert(p.name.clone(), features);
}
res
}
fn prefix_to_dep_features(
prefix: &str,
dep_features: &HashMap<String, Vec<String>>,
) -> Vec<String> {
if !prefix.starts_with("dependencies.")
&& !prefix.starts_with("dev-dependencies.")
&& !prefix.starts_with("build-dependencies.")
&& !prefix.ends_with(".features")
{
return vec![];
}
let prefix = prefix.strip_prefix("dependencies.").unwrap_or(prefix);
let prefix = prefix.strip_prefix("dev-dependencies.").unwrap_or(prefix);
let prefix = prefix.strip_prefix("build-dependencies.").unwrap_or(prefix);
let dep = prefix.strip_suffix(".features").unwrap_or(prefix);
let Some(features) = dep_features.get(dep) else {
return vec![];
};
features.clone()
}
#[derive(Default, Clone, Debug)]
pub struct Diagnostics(Vec<Diagnostic>);
impl From<Vec<Diagnostic>> for Diagnostics {
fn from(value: Vec<Diagnostic>) -> Self {
Self(value)
}
}
impl Deref for Diagnostics {
type Target = Vec<Diagnostic>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Diagnostics {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<&Document> for Notification {
fn from(value: &Document) -> Self {
(
"textDocument/publishDiagnostics",
PublishDiagnosticsParams {
uri: value.uri.clone(),
version: None,
diagnostics: value.diagnostics().to_vec(),
},
)
.into()
}
}
impl From<&Document> for DocumentDiagnosticReport {
fn from(value: &Document) -> Self {
DocumentDiagnosticReport {
kind: "full".to_string(),
result_id: None,
items: value.diagnostics().to_vec(),
}
}
}
fn get_tables(text: &str) -> Vec<String> {
let mut res = vec![];
for l in text.lines() {
let l = l.trim();
if l.starts_with('[') && l.ends_with(']') {
res.push(l[1..l.len() - 1].trim().to_string());
}
}
res
}
fn make_diagnostics(text: &str, tree: &Tree) -> Diagnostics {
let mut res = vec![];
let mut tt: TomlCursor = (text, tree.walk()).into();
tt.visit_all(|n| {
if n.is_error() {
res.push(error_node_to_diagnostic(n));
}
});
res.into()
}
fn error_node_to_diagnostic(n: Node) -> Diagnostic {
Diagnostic {
range: node_to_range(&n),
message: n.to_sexp(),
severity: Some(DiagnosticSeverity::Error),
..Default::default()
}
}
fn node_to_range(n: &Node) -> Range {
let start = n.start_position();
let end = n.end_position();
Range {
start: Position {
line: start.row,
character: start.column,
},
end: Position {
line: end.row,
character: end.column,
},
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty() {
let doc = Document::new("", String::new());
assert_no_errors(&doc);
}
#[test]
fn test_error() {
let doc = Document::new("", "[a]b".to_string());
assert_errors(&doc, &[r(0, 0, 0, 4)]);
}
#[test]
fn test_error_nested() {
let doc = Document::new(
"",
r#"
[a]
b={c="d"}
[a]b
"#
.to_string(),
);
assert_errors(&doc, &[r(3, 3, 4, 0)]);
}
#[test]
fn test_completions_empty_doc() {
let doc = Document::new("", String::new());
assert_completions(
&doc,
(0, 0).into(),
&[
c("[package]", KW),
c("[badges]", KW),
c("[features]", KW),
c("[dependencies]", KW),
c("[dev-dependencies]", KW),
c("[build-dependencies]", KW),
c("[target]", KW),
c("[lints]", KW),
c("[lints.clippy]", KW),
c("[hints]", KW),
c("[patch]", KW),
c("[replace]", KW),
c("[profile]", KW),
c("[workspace]", KW),
],
);
}
#[test]
fn test_completions_doc_with_tables() {
let doc = Document::new("", "[package]\n[dependencies]\n".to_string());
assert_completions(
&doc,
(0, 0).into(),
&[
c("[badges]", KW),
c("[features]", KW),
c("[dev-dependencies]", KW),
c("[build-dependencies]", KW),
c("[target]", KW),
c("[lints]", KW),
c("[lints.clippy]", KW),
c("[hints]", KW),
c("[patch]", KW),
c("[replace]", KW),
c("[profile]", KW),
c("[workspace]", KW),
],
);
}
#[test]
fn test_completions_package_doc() {
let doc = Document::new(
"",
r"
[package]
na
"
.to_string(),
);
assert_completions(
&doc,
(2, 2).into(),
&[
c("name", KW),
c("version", KW),
c("authors", KW),
c("edition", KW),
c("rust-version", KW),
c("description", KW),
c("documentation", KW),
c("readme", KW),
c("homepage", KW),
c("repository", KW),
c("license", KW),
c("license-file", KW),
c("keywords", KW),
c("categories", KW),
c("workspace", KW),
c("build", KW),
c("links", KW),
c("exclude", KW),
c("include", KW),
c("publish", KW),
c("metadata", KW),
c("default-run", KW),
c("autolib", KW),
c("autobins", KW),
c("autoexamples", KW),
c("autotests", KW),
c("autobenches", KW),
c("resolver", KW),
],
);
}
#[test]
fn test_completions_package_doc_present_keys() {
let doc = Document::new(
"",
r#"
[package]
name = "name"
"#
.to_string(),
);
assert_completions(
&doc,
(2, 2).into(),
&[
c("version", KW),
c("authors", KW),
c("edition", KW),
c("rust-version", KW),
c("description", KW),
c("documentation", KW),
c("readme", KW),
c("homepage", KW),
c("repository", KW),
c("license", KW),
c("license-file", KW),
c("keywords", KW),
c("categories", KW),
c("workspace", KW),
c("build", KW),
c("links", KW),
c("exclude", KW),
c("include", KW),
c("publish", KW),
c("metadata", KW),
c("default-run", KW),
c("autolib", KW),
c("autobins", KW),
c("autoexamples", KW),
c("autotests", KW),
c("autobenches", KW),
c("resolver", KW),
],
);
}
#[test]
fn test_completions_package_blank_line_also_suggests_tables() {
let doc = Document::new(
"",
r"
[package]
"
.to_string(),
);
assert_completions(
&doc,
(2, 0).into(),
&[
c("[badges]", KW),
c("[features]", KW),
c("[dependencies]", KW),
c("[dev-dependencies]", KW),
c("[build-dependencies]", KW),
c("[target]", KW),
c("[lints]", KW),
c("[lints.clippy]", KW),
c("[hints]", KW),
c("[patch]", KW),
c("[replace]", KW),
c("[profile]", KW),
c("[workspace]", KW),
c("name", KW),
c("version", KW),
c("authors", KW),
c("edition", KW),
c("rust-version", KW),
c("description", KW),
c("documentation", KW),
c("readme", KW),
c("homepage", KW),
c("repository", KW),
c("license", KW),
c("license-file", KW),
c("keywords", KW),
c("categories", KW),
c("workspace", KW),
c("build", KW),
c("links", KW),
c("exclude", KW),
c("include", KW),
c("publish", KW),
c("metadata", KW),
c("default-run", KW),
c("autolib", KW),
c("autobins", KW),
c("autoexamples", KW),
c("autotests", KW),
c("autobenches", KW),
c("resolver", KW),
],
);
}
#[test]
fn test_completions_features_file() {
let doc = Document::new(
"test_fixtures/features/Cargo.toml",
r#"
[package]
name = "test_features"
version = "0.0.0"
[dependencies]
serde = { version = "1", default-features = false, features = [] }
"#
.to_string(),
);
assert_completions_contain(
&doc,
(6, 63).into(),
&[
c("derive", KW),
],
);
}
const KW: CompletionItemKind = CompletionItemKind::Keyword;
fn c(text: &str, kind: CompletionItemKind) -> CompletionItem {
CompletionItem {
label: text.to_string(),
kind: Some(kind),
detail: None,
}
}
fn assert_completions(doc: &Document, pos: Position, completions: &[CompletionItem]) {
let dcs_at = doc.completions_at(pos);
println!("{dcs_at:?}");
let mut dcs = dcs_at.items.iter();
for c in completions {
let dc = dcs
.next()
.expect("Assert failed, completions count not matching");
assert_eq!(dc, c);
}
assert!(dcs.next().is_none());
}
fn assert_completions_contain(doc: &Document, pos: Position, completions: &[CompletionItem]) {
let dcs_at = doc.completions_at(pos);
let mut found = 0;
for c in dcs_at.items {
if completions.contains(&c) {
found +=1;
}
}
assert_eq!(found, completions.len(), "completions not matching");
}
fn r(
start_line: usize,
start_character: usize,
end_line: usize,
end_character: usize,
) -> Range {
((start_line, start_character).into()..(end_line, end_character).into()).into()
}
fn assert_no_errors(doc: &Document) {
assert!(doc.diagnostics().is_empty());
let root = doc.tree.root_node();
assert!(!root.has_error());
}
fn assert_errors(doc: &Document, ranges: &[Range]) {
let mut dgs = doc.diagnostics().iter();
for range in ranges {
let dg = dgs
.next()
.expect("Assert failed, ranges and diagnostic count not matching");
assert_eq!(&dg.range, range);
}
assert!(dgs.next().is_none());
let root = doc.tree.root_node();
assert!(root.has_error());
}
}