use bynk_check::firstparty::BYNK_SURFACE_CAPABILITIES;
use bynk_check::index::{ProjectIndex, SymbolKind};
use bynk_syntax::ast::{ConsumesDecl, SourceUnit, UsesDecl};
use bynk_syntax::lexer::tokenize;
use bynk_syntax::parser::parse_unit_with_recovery;
use bynk_syntax::span::Span;
use tower_lsp::lsp_types::*;
pub fn header_quick_fixes(
text: &str,
diagnostics: &[bynk_ide::Diagnostic],
requested: Span,
uri: &Url,
version: Option<i32>,
index: &ProjectIndex,
) -> Vec<CodeActionOrCommand> {
let Ok(tokens) = tokenize(text) else {
return Vec::new();
};
let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, text) else {
return Vec::new();
};
let Some(header) = Header::of(&unit) else {
return Vec::new();
};
let mut out = Vec::new();
for d in diagnostics {
if !intersects(d.error.span, requested) {
continue;
}
match d.error.category {
"bynk.resolve.unconsumed_context" => {
let chain = span_ident(text, d.error.span);
if let Some(action) = header.add_consumes_unit(&chain, text, uri, version) {
out.push(action);
}
}
"bynk.resolve.unknown_name" | "bynk.resolve.unknown_type" => {
let type_only = d.error.category == "bynk.resolve.unknown_type";
let name = span_ident(text, d.error.span);
out.extend(header.import_actions(&name, type_only, text, uri, version, index));
}
_ => {}
}
}
out
}
fn intersects(a: Span, b: Span) -> bool {
a.start <= b.end && b.start <= a.end
}
fn span_ident(text: &str, span: Span) -> String {
text.get(span.start..span.end)
.unwrap_or_default()
.split_whitespace()
.collect()
}
struct Header<'a> {
unit_name: String,
can_consume: bool,
name_span: Span,
uses: &'a [UsesDecl],
consumes: &'a [ConsumesDecl],
}
const NO_CONSUMES: &[ConsumesDecl] = &[];
impl<'a> Header<'a> {
fn of(unit: &'a SourceUnit) -> Option<Self> {
match unit {
SourceUnit::Commons(c) => Some(Header {
unit_name: c.name.joined(),
can_consume: false,
name_span: c.name.span,
uses: &c.uses,
consumes: NO_CONSUMES,
}),
SourceUnit::Context(c) => Some(Header {
unit_name: c.name.joined(),
can_consume: true,
name_span: c.name.span,
uses: &c.uses,
consumes: &c.consumes,
}),
SourceUnit::Adapter(a) => Some(Header {
unit_name: a.name.joined(),
can_consume: true,
name_span: a.name.span,
uses: &a.uses,
consumes: &a.consumes,
}),
SourceUnit::Suite(_) => None,
}
}
fn import_actions(
&self,
name: &str,
type_only: bool,
text: &str,
uri: &Url,
version: Option<i32>,
index: &ProjectIndex,
) -> Vec<CodeActionOrCommand> {
let mut targets: Vec<Candidate> = Vec::new();
if !type_only && BYNK_SURFACE_CAPABILITIES.contains(&name) {
targets.push(Candidate::ConsumesCapability {
unit: "bynk".to_string(),
});
}
for (key, entry) in &index.symbols {
if key.name != name || entry.def.is_none() || key.unit == self.unit_name {
continue;
}
match key.kind {
SymbolKind::Type if unit_is_commons(index, &key.unit) => {
targets.push(Candidate::Uses {
unit: key.unit.clone(),
});
}
SymbolKind::Fn if !type_only && unit_is_commons(index, &key.unit) => {
targets.push(Candidate::Uses {
unit: key.unit.clone(),
});
}
SymbolKind::Capability if !type_only => {
targets.push(Candidate::ConsumesCapability {
unit: key.unit.clone(),
});
}
_ => {}
}
}
targets.sort();
targets.dedup();
let mut out = Vec::new();
for cand in targets {
let action = match cand {
Candidate::Uses { unit } => self.add_uses(&unit, text, uri, version),
Candidate::ConsumesCapability { unit } => {
self.add_consumes_capability(&unit, name, text, uri, version)
}
};
if let Some(action) = action {
out.push(action);
}
}
out
}
fn add_consumes_unit(
&self,
target: &str,
text: &str,
uri: &Url,
version: Option<i32>,
) -> Option<CodeActionOrCommand> {
if !self.can_consume || self.consumes_target(target).is_some() {
return None;
}
let (at, insert) = self.new_consumes_edit(&format!("consumes {target}"));
Some(action(
format!("add `consumes {target}`"),
at,
insert,
text,
uri,
version,
))
}
fn add_consumes_capability(
&self,
unit: &str,
cap: &str,
text: &str,
uri: &Url,
version: Option<i32>,
) -> Option<CodeActionOrCommand> {
if !self.can_consume {
return None;
}
if let Some(dec) = self
.consumes
.iter()
.find(|c| c.target.joined() == unit && c.selected.is_some())
{
let selected = dec.selected.as_ref().unwrap();
if selected.iter().any(|c| c.name == cap) {
return None; }
let (at, insert) = match selected.last() {
Some(last) => (Span::new(last.span.end, last.span.end), format!(", {cap}")),
None => (dec.span, format!("consumes {unit} {{ {cap} }}")),
};
return Some(action(
format!("add `{cap}` to `consumes {unit}`"),
at,
insert,
text,
uri,
version,
));
}
if self.consumes_target(unit).is_some() {
return None;
}
let (at, insert) = self.new_consumes_edit(&format!("consumes {unit} {{ {cap} }}"));
Some(action(
format!("add `consumes {unit} {{ {cap} }}`"),
at,
insert,
text,
uri,
version,
))
}
fn add_uses(
&self,
target: &str,
text: &str,
uri: &Url,
version: Option<i32>,
) -> Option<CodeActionOrCommand> {
if self.uses.iter().any(|u| u.target.joined() == target) {
return None;
}
let (at, insert) = if let Some(last) = self.uses.last() {
(last.span.end, format!("\nuses {target}"))
} else if let Some(last) = self.consumes.last() {
(last.span.end, format!("\nuses {target}"))
} else {
(self.name_span.end, format!("\n\nuses {target}"))
};
Some(action(
format!("add `uses {target}`"),
Span::new(at, at),
insert,
text,
uri,
version,
))
}
fn new_consumes_edit(&self, clause: &str) -> (Span, String) {
if let Some(last) = self.consumes.last() {
let at = last.span.end;
(Span::new(at, at), format!("\n{clause}"))
} else {
let at = self.name_span.end;
(Span::new(at, at), format!("\n\n{clause}"))
}
}
fn consumes_target(&self, target: &str) -> Option<&ConsumesDecl> {
self.consumes
.iter()
.find(|c| c.target.joined() == target && c.selected.is_none())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Candidate {
Uses { unit: String },
ConsumesCapability { unit: String },
}
fn unit_is_commons(index: &ProjectIndex, unit: &str) -> bool {
!index.symbols.keys().any(|k| {
k.unit == unit
&& matches!(
k.kind,
SymbolKind::Service
| SymbolKind::Agent
| SymbolKind::Actor
| SymbolKind::Capability
| SymbolKind::Provider
| SymbolKind::Handler
| SymbolKind::CapabilityOp
)
})
}
fn action(
title: String,
at: Span,
new_text: String,
text: &str,
uri: &Url,
version: Option<i32>,
) -> CodeActionOrCommand {
let edit = OneOf::Left(TextEdit {
range: crate::position::span_to_range(text, at),
new_text,
});
CodeActionOrCommand::CodeAction(CodeAction {
title,
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit {
changes: None,
document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier {
uri: uri.clone(),
version,
},
edits: vec![edit],
}])),
change_annotations: None,
}),
..Default::default()
})
}