bynk_lsp/
provider_stub.rs1use bynk_check::index::{ProjectIndex, SymbolKey, SymbolKind};
28use bynk_syntax::ast::{BaseType, CapabilityDecl, CommonsItem, SourceUnit, TypeRef};
29use bynk_syntax::lexer::tokenize;
30use bynk_syntax::parser::parse_unit_with_recovery;
31use bynk_syntax::span::Span;
32use tower_lsp::lsp_types::*;
33
34const DATA_TAG: &str = "bynk.generateProvider";
35const CATEGORIES: [&str; 1] = ["bynk.exports.capability_not_provided"];
36
37pub struct Plan {
39 pub capability: String,
40 pub provider: String,
41 pub edits: Vec<(Span, String)>,
42}
43
44pub struct Request {
47 pub capability: String,
48 pub quick_fix: bool,
49}
50
51pub fn requested(
53 text: &str,
54 diagnostics: &[bynk_ide::Diagnostic],
55 requested: Span,
56) -> Vec<Request> {
57 let mut out: Vec<Request> = Vec::new();
58 let touches = |s: Span| s.start <= requested.end && requested.start <= s.end;
59 for d in diagnostics {
60 if CATEGORIES.contains(&d.error.category) && touches(d.error.span) {
61 let name = text
62 .get(d.error.span.start..d.error.span.end)
63 .unwrap_or_default()
64 .trim()
65 .to_string();
66 if !name.is_empty() && !out.iter().any(|r| r.capability == name) {
67 out.push(Request {
68 capability: name,
69 quick_fix: true,
70 });
71 }
72 }
73 }
74 if let Some(unit) = parse(text) {
75 for cap in capabilities(&unit) {
76 if touches(cap.name.span) && !out.iter().any(|r| r.capability == cap.name.name) {
77 out.push(Request {
78 capability: cap.name.name.clone(),
79 quick_fix: false,
80 });
81 }
82 }
83 }
84 out
85}
86
87pub fn plan(text: &str, capability: &str, index: &ProjectIndex) -> Option<Plan> {
90 let unit = parse(text)?;
91 let SourceUnit::Context(ctx) = &unit else {
92 return None;
93 };
94 let cap = capabilities(&unit)
95 .into_iter()
96 .find(|c| c.name.name == capability)?;
97 if cap.derived.is_some() || cap.ops.is_empty() {
98 return None;
99 }
100 let key = SymbolKey {
101 unit: ctx.name.joined(),
102 kind: SymbolKind::Capability,
103 name: capability.to_string(),
104 };
105 if index.impls_of(&key).next().is_some() {
106 return None;
107 }
108 let unit = ctx.name.joined();
111 let declared: Vec<&str> = index
112 .symbols
113 .keys()
114 .filter(|k| k.unit == unit)
115 .map(|k| k.name.as_str())
116 .collect();
117 let provider = fresh_name(text, &declared, &format!("{capability}Impl"));
118
119 let mut ops = Vec::new();
120 for op in &cap.ops {
121 if !op.type_params.is_empty() {
122 return None;
123 }
124 let body = default_of(&op.return_type)?;
125 let signature = text.get(op.span.start..op.return_type.span().end)?;
126 if signature.contains("//") || !signature.trim_start().starts_with("fn") {
127 return None;
128 }
129 ops.push(format!(" {} {{\n {body}\n }}", signature.trim()));
130 }
131 let stub = format!(
132 "\n\nprovides {capability} = {provider} {{\n{}\n}}",
133 ops.join("\n\n")
134 );
135 let mut edits = vec![(Span::new(cap.span.end, cap.span.end), stub)];
136
137 let opts = bynk_fmt::FormatOptions::default();
138 if bynk_fmt::format_source(text, &opts).is_ok_and(|f| f == text) {
139 let stubbed = crate::fix_all::apply(text, &edits);
140 let formatted = bynk_fmt::format_source(&stubbed, &opts).ok()?;
141 edits = vec![crate::inline::changed_region(text, &formatted)];
142 }
143 Some(Plan {
144 capability: capability.to_string(),
145 provider,
146 edits,
147 })
148}
149
150fn parse(text: &str) -> Option<SourceUnit> {
151 let tokens = tokenize(text).ok()?;
152 match parse_unit_with_recovery(&tokens, text) {
153 (Some(unit), errs) if errs.is_empty() => Some(unit),
154 _ => None,
155 }
156}
157
158fn capabilities(unit: &SourceUnit) -> Vec<&CapabilityDecl> {
159 let SourceUnit::Context(ctx) = unit else {
160 return Vec::new();
161 };
162 ctx.items
163 .iter()
164 .filter_map(|i| match i {
165 CommonsItem::Capability(c) => Some(c),
166 _ => None,
167 })
168 .collect()
169}
170
171fn default_of(ty: &TypeRef) -> Option<&'static str> {
174 let ty = match ty {
175 TypeRef::Effect(inner, _) => inner,
176 other => other,
177 };
178 Some(match ty {
179 TypeRef::Base(BaseType::Int, _) => "0",
180 TypeRef::Base(BaseType::Float, _) => "0.0",
181 TypeRef::Base(BaseType::String, _) => "\"\"",
182 TypeRef::Base(BaseType::Bool, _) => "false",
183 TypeRef::Unit(_) => "()",
184 TypeRef::Option(..) => "None",
185 TypeRef::List(..) => "[]",
186 _ => return None,
187 })
188}
189
190fn fresh_name(text: &str, declared: &[&str], base: &str) -> String {
193 let taken = |name: &str| {
194 declared.contains(&name)
195 || text.match_indices(name).any(|(i, _)| {
196 let before = text[..i].chars().next_back();
197 let after = text[i + name.len()..].chars().next();
198 !before.is_some_and(|c| c.is_alphanumeric() || c == '_')
199 && !after.is_some_and(|c| c.is_alphanumeric() || c == '_')
200 })
201 };
202 if !taken(base) {
203 return base.to_string();
204 }
205 (2..)
206 .map(|n| format!("{base}{n}"))
207 .find(|name| !taken(name))
208 .expect("an unused name")
209}
210
211pub fn listed(
213 plan: &Plan,
214 quick_fix: bool,
215 uri: &Url,
216 version: Option<i32>,
217) -> CodeActionOrCommand {
218 CodeActionOrCommand::CodeAction(CodeAction {
219 title: format!(
220 "Generate provider `{}` for `{}`",
221 plan.provider, plan.capability
222 ),
223 kind: Some(if quick_fix {
224 CodeActionKind::QUICKFIX
225 } else {
226 CodeActionKind::REFACTOR
227 }),
228 data: Some(serde_json::json!({
229 "kind": DATA_TAG,
230 "uri": uri.as_str(),
231 "version": version,
232 "capability": plan.capability,
233 })),
234 ..Default::default()
235 })
236}
237
238pub struct Target {
240 pub uri: Url,
241 pub version: Option<i32>,
242 pub capability: String,
243}
244
245impl Target {
246 pub fn of(action: &CodeAction) -> Option<Self> {
247 let data = action.data.as_ref()?;
248 if data.get("kind")?.as_str()? != DATA_TAG {
249 return None;
250 }
251 Some(Target {
252 uri: Url::parse(data.get("uri")?.as_str()?).ok()?,
253 version: data
254 .get("version")
255 .and_then(serde_json::Value::as_i64)
256 .and_then(|v| i32::try_from(v).ok()),
257 capability: data.get("capability")?.as_str()?.to_string(),
258 })
259 }
260}