1use bynk_check::locals::{LocalBinding, locals_at};
30use bynk_syntax::ast::{
31 Block, CommonsItem, Expr, ExprKind, Ident, LetStmt, MatchBody, SourceUnit, Statement,
32 expr_children, statement_exprs,
33};
34use bynk_syntax::lexer::tokenize;
35use bynk_syntax::parser::parse_unit_with_recovery;
36use bynk_syntax::span::Span;
37use tower_lsp::lsp_types::*;
38
39const DATA_TAG: &str = "bynk.inlineVariable";
40
41pub struct Plan {
43 pub name: String,
44 pub def: Span,
45 pub edits: Vec<(Span, String)>,
46}
47
48pub struct Offer {
50 pub name: String,
51 pub def: Span,
52}
53
54pub fn offerable(text: &str, offset: usize, locals: &[LocalBinding]) -> Option<Offer> {
58 analyse(text, offset, locals).map(|p| Offer {
59 name: p.name,
60 def: p.def,
61 })
62}
63
64pub fn plan(text: &str, offset: usize, locals: &[LocalBinding]) -> Option<Plan> {
68 let mut plan = analyse(text, offset, locals)?;
69 let opts = bynk_fmt::FormatOptions::default();
73 if bynk_fmt::format_source(text, &opts).is_ok_and(|f| f == text) {
74 let inlined = crate::fix_all::apply(text, &plan.edits);
75 let formatted = bynk_fmt::format_source(&inlined, &opts).ok()?;
76 plan.edits = vec![changed_region(text, &formatted)];
77 }
78 Some(plan)
79}
80
81fn analyse(text: &str, offset: usize, locals: &[LocalBinding]) -> Option<Plan> {
83 let sites = crate::locals_nav::local_sites_at(locals, text, offset)?;
84 let (def, uses) = sites.split_first()?;
85 let tokens = tokenize(text).ok()?;
86 let (Some(unit), errs) = parse_unit_with_recovery(&tokens, text) else {
87 return None;
88 };
89 if !errs.is_empty() {
90 return None;
91 }
92 let mut found = Found::default();
93 walk_unit(&unit, def.start, &mut found);
94 let let_stmt = found.let_stmt?;
95 if let_stmt.name.name == "_" || let_stmt.let_else.is_some() || found.effectful {
96 return None;
97 }
98 if let_stmt.type_annot.is_some() {
103 return None;
104 }
105 if !let_stmt.trivia.leading.is_empty() || let_stmt.trivia.trailing.is_some() {
106 return None;
107 }
108 let value = &let_stmt.value;
109 let value_text = text.get(value.span.start..value.span.end)?;
110 let replacement = if atomic(value) {
111 value_text.to_string()
112 } else {
113 format!("({value_text})")
114 };
115
116 let mut idents = Vec::new();
118 collect_idents(value, &mut idents);
119 let resolve = |name: &str, at: usize| -> Option<Span> {
120 locals_at(locals, at)
121 .into_iter()
122 .find(|b| b.name == name)
123 .map(|b| b.def_span)
124 };
125 let same = |a: Option<Span>, b: Option<Span>| {
126 a.map(|s| (s.start, s.end)) == b.map(|s| (s.start, s.end))
127 };
128
129 let mut edits = Vec::new();
130 for u in uses {
131 let shorthand = found
132 .shorthands
133 .iter()
134 .any(|s| (s.start, s.end) == (u.start, u.end));
135 let expr_use = found
136 .idents
137 .iter()
138 .any(|s| (s.start, s.end) == (u.start, u.end));
139 if !shorthand && !expr_use {
140 if found
143 .labels
144 .iter()
145 .any(|s| (s.start, s.end) == (u.start, u.end))
146 {
147 continue;
148 }
149 return None;
150 }
151 for id in &idents {
152 if !same(
153 resolve(&id.name, value.span.start),
154 resolve(&id.name, u.start),
155 ) {
156 return None;
157 }
158 }
159 let new_text = if shorthand {
160 format!("{}: {value_text}", let_stmt.name.name)
161 } else {
162 replacement.clone()
163 };
164 edits.push((Span::new(u.start, u.end), new_text));
165 }
166 if edits.is_empty() {
167 return None;
168 }
169
170 let s = let_stmt.span;
172 let line_start = text[..s.start].rfind('\n').map_or(0, |i| i + 1);
173 if !text[line_start..s.start].trim().is_empty() {
174 return None;
175 }
176 let line_end = text[s.end..].find('\n').map_or(text.len(), |i| s.end + i);
177 if !text[s.end..line_end].trim().is_empty() {
178 return None;
179 }
180 let delete_end = (line_end + 1).min(text.len());
181 edits.push((Span::new(line_start, delete_end), String::new()));
182 edits.sort_by_key(|(span, _)| (span.start, span.end));
183 Some(Plan {
184 name: let_stmt.name.name.clone(),
185 def: *def,
186 edits,
187 })
188}
189
190pub(crate) fn changed_region(old: &str, new: &str) -> (Span, String) {
193 let prefix: usize = old
194 .chars()
195 .zip(new.chars())
196 .take_while(|(a, b)| a == b)
197 .map(|(a, _)| a.len_utf8())
198 .sum();
199 let suffix: usize = old[prefix..]
200 .chars()
201 .rev()
202 .zip(new[prefix..].chars().rev())
203 .take_while(|(a, b)| a == b)
204 .map(|(a, _)| a.len_utf8())
205 .sum();
206 (
207 Span::new(prefix, old.len() - suffix),
208 new[prefix..new.len() - suffix].to_string(),
209 )
210}
211
212fn atomic(e: &Expr) -> bool {
214 matches!(
215 e.kind,
216 ExprKind::IntLit { .. }
217 | ExprKind::FloatLit { .. }
218 | ExprKind::DurationLit { .. }
219 | ExprKind::StrLit(_)
220 | ExprKind::InterpStr(_)
221 | ExprKind::BoolLit(_)
222 | ExprKind::Ident(_)
223 | ExprKind::Call { .. }
224 | ExprKind::Paren(_)
225 | ExprKind::Ok(_)
226 | ExprKind::Err(_)
227 | ExprKind::RecordConstruction { .. }
228 | ExprKind::FieldAccess { .. }
229 | ExprKind::MethodCall { .. }
230 | ExprKind::Some(_)
231 | ExprKind::None
232 | ExprKind::UnitLit
233 | ExprKind::ListLit(_)
234 )
235}
236
237fn collect_idents<'a>(e: &'a Expr, out: &mut Vec<&'a Ident>) {
240 match &e.kind {
241 ExprKind::Ident(id) => out.push(id),
242 ExprKind::Call { name, .. } => out.push(name),
243 _ => {}
244 }
245 for c in expr_children(e) {
246 collect_idents(c, out);
247 }
248}
249
250#[derive(Default)]
254struct Found<'a> {
255 let_stmt: Option<&'a LetStmt>,
256 effectful: bool,
257 idents: Vec<Span>,
258 shorthands: Vec<Span>,
259 labels: Vec<Span>,
261}
262
263fn walk_unit<'a>(unit: &'a SourceUnit, def: usize, found: &mut Found<'a>) {
264 let items: &[CommonsItem] = match unit {
265 SourceUnit::Commons(c) => &c.items,
266 SourceUnit::Context(c) => &c.items,
267 SourceUnit::Adapter(_) => &[],
268 SourceUnit::Suite(t) => {
269 for case in &t.cases {
270 walk_block(&case.body, def, found);
271 }
272 &[]
273 }
274 };
275 for item in items {
276 match item {
277 CommonsItem::Fn(f) => walk_block(&f.body, def, found),
278 CommonsItem::Provider(p) => {
279 p.ops.iter().for_each(|op| walk_block(&op.body, def, found))
280 }
281 CommonsItem::Service(s) => s
282 .handlers
283 .iter()
284 .for_each(|h| walk_block(&h.body, def, found)),
285 CommonsItem::Agent(a) => a
286 .handlers
287 .iter()
288 .for_each(|h| walk_block(&h.body, def, found)),
289 _ => {}
290 }
291 }
292}
293
294fn walk_block<'a>(b: &'a Block, def: usize, found: &mut Found<'a>) {
295 for s in &b.statements {
296 if let Statement::Let(l) | Statement::EffectLet(l) = s
297 && l.name.span.start == def
298 {
299 found.let_stmt = Some(l);
300 found.effectful = matches!(s, Statement::EffectLet(_));
301 }
302 let mut exprs = Vec::new();
303 statement_exprs(s, &mut exprs);
304 for e in exprs {
305 walk_expr(e, def, found);
306 }
307 }
308 walk_expr(&b.tail, def, found);
309}
310
311fn walk_expr<'a>(e: &'a Expr, def: usize, found: &mut Found<'a>) {
312 match &e.kind {
313 ExprKind::Ident(id) => found.idents.push(id.span),
314 ExprKind::RecordConstruction { fields, .. } => {
315 for f in fields {
316 match f.value {
317 None => found.shorthands.push(f.name.span),
318 Some(_) => found.labels.push(f.name.span),
319 }
320 }
321 }
322 _ => {}
323 }
324 match &e.kind {
325 ExprKind::Block(b) => walk_block(b, def, found),
326 ExprKind::If {
327 cond,
328 then_block,
329 else_block,
330 } => {
331 walk_expr(cond, def, found);
332 walk_block(then_block, def, found);
333 walk_block(else_block, def, found);
334 }
335 ExprKind::Match { discriminant, arms } => {
336 walk_expr(discriminant, def, found);
337 for arm in arms {
338 if let Some(g) = &arm.guard {
339 walk_expr(g, def, found);
340 }
341 match &arm.body {
342 MatchBody::Expr(e) => walk_expr(e, def, found),
343 MatchBody::Block(b) => walk_block(b, def, found),
344 }
345 }
346 }
347 _ => {
348 for c in expr_children(e) {
349 walk_expr(c, def, found);
350 }
351 }
352 }
353}
354
355pub fn listed(plan: &Offer, uri: &Url, version: Option<i32>) -> CodeActionOrCommand {
357 CodeActionOrCommand::CodeAction(CodeAction {
358 title: format!("Inline `{}`", plan.name),
359 kind: Some(CodeActionKind::REFACTOR_INLINE),
360 data: Some(serde_json::json!({
361 "kind": DATA_TAG,
362 "uri": uri.as_str(),
363 "version": version,
364 "def": plan.def.start,
365 })),
366 ..Default::default()
367 })
368}
369
370pub struct Target {
372 pub uri: Url,
373 pub version: Option<i32>,
374 pub def: usize,
375}
376
377impl Target {
378 pub fn of(action: &CodeAction) -> Option<Self> {
379 let data = action.data.as_ref()?;
380 if data.get("kind")?.as_str()? != DATA_TAG {
381 return None;
382 }
383 Some(Target {
384 uri: Url::parse(data.get("uri")?.as_str()?).ok()?,
385 version: data
386 .get("version")
387 .and_then(serde_json::Value::as_i64)
388 .and_then(|v| i32::try_from(v).ok()),
389 def: usize::try_from(data.get("def")?.as_u64()?).ok()?,
390 })
391 }
392}