mod expression;
mod frontmatter;
mod invocation;
pub use expression::{FunctionHint, NameHint};
use crate::components;
use crate::diagnostic::{Diagnostic, Location, Range};
use crate::parse::CODE;
use crate::resolve::{Component, Document, Resolved};
use crate::settings::Input;
use crate::tree::{
Code, Declaration, DeclaredType, DefaultValue, Node, ParamValue, Signature, SlotDeclaration,
StringPart, TypeName,
};
use crate::validate::params::binds;
use crate::validate::universal::Universal;
use crate::values::{Form, Kind};
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct PositionalHint {
pub range: Range,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<Subject>,
pub completions: Vec<Completion>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum Subject {
Component {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
component: Option<ComponentHint>,
},
Params {
component: String,
params: Vec<ParameterHint>,
},
Param {
component: String,
param: ParameterHint,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
},
Slot {
component: String,
slot: String,
slots: Vec<SlotDeclaration>,
},
Path {
path: String,
name: NameHint,
},
Function {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
hint: Option<FunctionHint>,
},
Frontmatter {
path: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
about: Option<String>,
},
Type {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
takes: Option<KindHint>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ComponentHint {
#[serde(skip_serializing_if = "Option::is_none")]
pub defined_as: Option<String>,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<Signature>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
pub provided: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub defined: Option<Range>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ParamOrigin {
Declared,
Common,
Attribute,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ParameterHint {
pub name: String,
pub origin: ParamOrigin,
pub takes: KindHint,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
pub breakpoints: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KindHint {
pub tokens: Vec<String>,
pub forms: Vec<Form>,
pub examples: Vec<String>,
pub up_to: usize,
pub flag: bool,
pub quoted: bool,
pub description: String,
}
impl KindHint {
fn of(kind: &Kind) -> Self {
Self {
tokens: kind.tokens.iter().map(ToString::to_string).collect(),
forms: kind.forms.to_vec(),
examples: kind.examples.iter().map(ToString::to_string).collect(),
up_to: kind.up_to,
flag: kind.flag,
quoted: kind.quoted,
description: kind.describe(),
}
}
fn declared(declared: &DeclaredType, inputs: &[Input]) -> Self {
let DeclaredType::Input { input } = declared else {
return Self::of(&Kind::declared(declared));
};
let bound: Vec<&str> = inputs
.iter()
.filter(|known| binds(input, &known.declaration.declared))
.map(|known| known.declaration.name.as_str())
.collect();
Self {
forms: vec![Form::Name],
quoted: false,
description: format!("the name of {} under `md.inputs`", declared.written()),
..Self::of(&Kind::one_of(bound))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Completion {
pub label: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl Completion {
fn new(text: &str, detail: Option<String>) -> Self {
Self {
label: text.to_string(),
text: text.to_string(),
detail,
}
}
}
pub(crate) fn at(
resolved: &Resolved,
diagnostics: &[Diagnostic],
offset: usize,
) -> Option<PositionalHint> {
let document = resolved.document(resolved.main);
let caret = document.line_starts.location(offset);
let lines = document.lines();
let text: Vec<char> = lines.line(caret.line).unwrap_or("").chars().collect();
let place = Place {
resolved,
document,
offset,
line: caret.line,
at: (caret.column - 1).min(text.len()),
text,
};
let found = match surrounding(&document.root.children, offset) {
Surrounding::Text => expression::interpolation(&place).or_else(|| invocation::hint(&place)),
Surrounding::Frontmatter => frontmatter::hint(&place),
Surrounding::Code(code) => invocation::code_block(&place, code),
Surrounding::Opaque => None,
};
let caret = Location::new(place.line, place.at + 1);
let diagnostics: Vec<Diagnostic> = diagnostics
.iter()
.filter(|diagnostic| diagnostic.file.is_none() && covers(&diagnostic.range, caret))
.cloned()
.collect();
let hint = match found {
Some(found) => PositionalHint {
range: place.range(found.start, found.end),
subject: Some(found.subject),
completions: found.completions,
diagnostics,
},
None if diagnostics.is_empty() => return None,
None => PositionalHint {
range: place.range(place.at, place.at),
subject: None,
completions: Vec::new(),
diagnostics,
},
};
Some(hint)
}
fn covers(range: &Range, at: Location) -> bool {
let end = range
.end
.unwrap_or(Location::new(range.start.line, usize::MAX));
range.start <= at && at <= end
}
struct Place<'r> {
resolved: &'r Resolved,
document: &'r Document,
offset: usize,
line: usize,
text: Vec<char>,
at: usize,
}
impl Place<'_> {
fn range(&self, start: usize, end: usize) -> Range {
Range {
start: Location::new(self.line, start + 1),
end: Some(Location::new(self.line, end + 1)),
}
}
fn component(&self, name: &str) -> Option<&Component> {
match self.document.names.get(name)? {
crate::resolve::Binding::Component(id, _) => Some(self.resolved.component(*id)),
_ => None,
}
}
fn component_hint(&self, name: &str) -> Option<ComponentHint> {
let component = self.component(name)?;
let origin = components::origin(self.resolved, component);
Some(ComponentHint {
defined_as: (component.name != name).then(|| component.name.clone()),
kind: components::kind(component),
signature: component.signature.clone(),
file: (origin != self.document.origin).then(|| origin.to_string()),
provided: self.resolved.document(component.document).provided,
defined: component.position.map(Range::from),
})
}
fn component_names(&self) -> Vec<Completion> {
self.document
.names
.components()
.map(|(name, id)| {
Completion::new(name, Some(components::kind(self.resolved.component(id))))
})
.collect()
}
fn params(&self, name: &str) -> Vec<ParameterHint> {
let component = self.component(name);
let inputs = &self.document.settings.inputs;
let declared = component
.and_then(|component| Some((component, component.signature.as_ref()?)))
.into_iter()
.flat_map(|(component, signature)| {
signature
.params
.iter()
.map(move |declaration| declared(declaration, component, inputs))
});
let mut params: Vec<ParameterHint> = declared.collect();
let universal: Vec<ParameterHint> = Universal::ALL
.iter()
.filter(|universal| params.iter().all(|param| param.name != universal.name()))
.map(|universal| ParameterHint {
name: universal.name().to_string(),
origin: if universal.is_common() {
ParamOrigin::Common
} else {
ParamOrigin::Attribute
},
takes: KindHint::of(&universal.kind()),
required: false,
default: None,
doc: None,
breakpoints: universal.takes_breakpoint(),
})
.collect();
params.extend(universal);
params
}
fn block_params(&self, name: &str) -> Vec<ParameterHint> {
let mut params: Vec<ParameterHint> = self
.params(name)
.into_iter()
.filter(|param| param.name != CODE)
.collect();
params.push(ParameterHint {
name: "alt".to_string(),
origin: ParamOrigin::Attribute,
takes: KindHint::of(&Kind::declared(&DeclaredType::Name(TypeName::String))),
required: false,
default: None,
doc: Some("The text alternative for screen readers.".to_string()),
breakpoints: false,
});
params
}
}
fn declared(declaration: &Declaration, component: &Component, inputs: &[Input]) -> ParameterHint {
ParameterHint {
name: declaration.name.clone(),
origin: ParamOrigin::Declared,
takes: KindHint::declared(&declaration.declared, inputs),
required: !declaration.optional && declaration.default.is_none(),
default: declaration.default.as_ref().map(written),
doc: declaration.doc.clone(),
breakpoints: component.takes_breakpoints(),
}
}
fn written(default: &DefaultValue) -> String {
match default {
DefaultValue::Param(value) => match value {
ParamValue::Bare(text)
| ParamValue::Json(text)
| ParamValue::Class(text)
| ParamValue::Id(text) => text.clone(),
ParamValue::Expr(source) => format!("({source})"),
ParamValue::Flag => "true".to_string(),
ParamValue::String(parts) => {
let text: String = parts
.iter()
.map(|part| match part {
StringPart::Text(text) => text.value.clone(),
StringPart::Interpolation(interpolation) => {
format!("{{{{{}}}}}", interpolation.path)
}
})
.collect();
format!("\"{text}\"")
}
},
DefaultValue::Yaml(serde_json::Value::String(text)) => text.clone(),
DefaultValue::Yaml(value) => value.to_string(),
}
}
struct Found {
start: usize,
end: usize,
subject: Subject,
completions: Vec<Completion>,
}
#[derive(Debug, PartialEq)]
enum Surrounding<'a> {
Text,
Frontmatter,
Code(&'a Code),
Opaque,
}
fn surrounding(nodes: &[Node], offset: usize) -> Surrounding<'_> {
let starts_before = nodes.partition_point(|node| {
node.position()
.is_none_or(|position| position.start.offset <= offset)
});
let Some(block) = starts_before.checked_sub(1).map(|index| &nodes[index]) else {
return Surrounding::Text;
};
let inside = block
.position()
.is_some_and(|position| position.start.offset <= offset && offset <= position.end.offset);
match block {
Node::Yaml(_) if inside => Surrounding::Frontmatter,
Node::Code(code) if inside => Surrounding::Code(code),
Node::Math(_)
| Node::Html(_)
| Node::InlineCode(_)
| Node::InlineMath(_)
| Node::ComponentSignature(_)
if inside =>
{
Surrounding::Opaque
}
Node::ComponentDefinition(definition) if inside => match &definition.body {
crate::tree::DefinitionBody::Compmd { children }
if children
.first()
.and_then(Node::position)
.is_some_and(|first| first.start.offset <= offset) =>
{
surrounding(children, offset)
}
_ => Surrounding::Opaque,
},
Node::ComponentContainer(container) => {
let in_slot = container.slots.iter().rev().find(|slot| {
slot.position
.is_some_and(|position| position.start.offset <= offset)
});
match in_slot {
Some(slot) => surrounding(&slot.children, offset),
None => surrounding(&container.children, offset),
}
}
_ => surrounding(block.children(), offset),
}
}
#[cfg(test)]
mod tests;