1use draxl_ast::{Expr, File, Item, Pattern, Stmt};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum NodeKind {
5 File,
6 Mod,
7 Use,
8 Struct,
9 Enum,
10 Fn,
11 Field,
12 Variant,
13 Param,
14 LetStmt,
15 ExprStmt,
16 MatchArm,
17 PatternIdent,
18 PatternWild,
19 Type,
20 ExprPath,
21 ExprLit,
22 ExprGroup,
23 ExprBinary,
24 ExprUnary,
25 ExprCall,
26 ExprMatch,
27 ExprBlock,
28 Doc,
29 Comment,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum FragmentKind {
34 Item,
35 Field,
36 Variant,
37 Param,
38 Stmt,
39 MatchArm,
40 Expr,
41 Type,
42 Pattern,
43 Doc,
44 Comment,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ValueKind {
49 Ident,
50 Str,
51 Bool,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SlotArity {
56 Ranked,
57 Single,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum AttachmentContainerKind {
62 Items,
63 Stmts,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SlotSpec {
68 pub public_name: &'static str,
69 pub meta_slot_name: &'static str,
70 pub fragment_kind: FragmentKind,
71 pub arity: SlotArity,
72 pub occupant_removable: bool,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct PathSpec {
77 pub public_name: &'static str,
78 pub value_kind: ValueKind,
79 pub clearable: bool,
80}
81
82pub fn slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
83 match (owner, slot) {
84 (NodeKind::File, "items") => Some(SlotSpec {
85 public_name: "items",
86 meta_slot_name: "file_items",
87 fragment_kind: FragmentKind::Item,
88 arity: SlotArity::Ranked,
89 occupant_removable: true,
90 }),
91 (NodeKind::Mod, "items") => Some(SlotSpec {
92 public_name: "items",
93 meta_slot_name: "items",
94 fragment_kind: FragmentKind::Item,
95 arity: SlotArity::Ranked,
96 occupant_removable: true,
97 }),
98 (NodeKind::Struct, "fields") => Some(SlotSpec {
99 public_name: "fields",
100 meta_slot_name: "fields",
101 fragment_kind: FragmentKind::Field,
102 arity: SlotArity::Ranked,
103 occupant_removable: true,
104 }),
105 (NodeKind::Enum, "variants") => Some(SlotSpec {
106 public_name: "variants",
107 meta_slot_name: "variants",
108 fragment_kind: FragmentKind::Variant,
109 arity: SlotArity::Ranked,
110 occupant_removable: true,
111 }),
112 (NodeKind::Fn, "params") => Some(SlotSpec {
113 public_name: "params",
114 meta_slot_name: "params",
115 fragment_kind: FragmentKind::Param,
116 arity: SlotArity::Ranked,
117 occupant_removable: true,
118 }),
119 (NodeKind::Fn, "body") | (NodeKind::ExprBlock, "body") => Some(SlotSpec {
120 public_name: "body",
121 meta_slot_name: "body",
122 fragment_kind: FragmentKind::Stmt,
123 arity: SlotArity::Ranked,
124 occupant_removable: true,
125 }),
126 (NodeKind::ExprMatch, "arms") => Some(SlotSpec {
127 public_name: "arms",
128 meta_slot_name: "arms",
129 fragment_kind: FragmentKind::MatchArm,
130 arity: SlotArity::Ranked,
131 occupant_removable: true,
132 }),
133 (NodeKind::Fn, "ret") => Some(SlotSpec {
134 public_name: "ret",
135 meta_slot_name: "ret",
136 fragment_kind: FragmentKind::Type,
137 arity: SlotArity::Single,
138 occupant_removable: true,
139 }),
140 (NodeKind::Field, "ty") | (NodeKind::Param, "ty") => Some(SlotSpec {
141 public_name: "ty",
142 meta_slot_name: "ty",
143 fragment_kind: FragmentKind::Type,
144 arity: SlotArity::Single,
145 occupant_removable: false,
146 }),
147 (NodeKind::LetStmt, "pat") => Some(SlotSpec {
148 public_name: "pat",
149 meta_slot_name: "pat",
150 fragment_kind: FragmentKind::Pattern,
151 arity: SlotArity::Single,
152 occupant_removable: false,
153 }),
154 (NodeKind::LetStmt, "init") => Some(SlotSpec {
155 public_name: "init",
156 meta_slot_name: "init",
157 fragment_kind: FragmentKind::Expr,
158 arity: SlotArity::Single,
159 occupant_removable: false,
160 }),
161 (NodeKind::ExprStmt, "expr") => Some(SlotSpec {
162 public_name: "expr",
163 meta_slot_name: "expr",
164 fragment_kind: FragmentKind::Expr,
165 arity: SlotArity::Single,
166 occupant_removable: false,
167 }),
168 (NodeKind::ExprGroup, "expr") | (NodeKind::ExprUnary, "expr") => Some(SlotSpec {
169 public_name: "expr",
170 meta_slot_name: "expr",
171 fragment_kind: FragmentKind::Expr,
172 arity: SlotArity::Single,
173 occupant_removable: false,
174 }),
175 (NodeKind::ExprBinary, "lhs") => Some(SlotSpec {
176 public_name: "lhs",
177 meta_slot_name: "lhs",
178 fragment_kind: FragmentKind::Expr,
179 arity: SlotArity::Single,
180 occupant_removable: false,
181 }),
182 (NodeKind::ExprBinary, "rhs") => Some(SlotSpec {
183 public_name: "rhs",
184 meta_slot_name: "rhs",
185 fragment_kind: FragmentKind::Expr,
186 arity: SlotArity::Single,
187 occupant_removable: false,
188 }),
189 (NodeKind::ExprCall, "callee") => Some(SlotSpec {
190 public_name: "callee",
191 meta_slot_name: "callee",
192 fragment_kind: FragmentKind::Expr,
193 arity: SlotArity::Single,
194 occupant_removable: false,
195 }),
196 (NodeKind::ExprMatch, "scrutinee") => Some(SlotSpec {
197 public_name: "scrutinee",
198 meta_slot_name: "scrutinee",
199 fragment_kind: FragmentKind::Expr,
200 arity: SlotArity::Single,
201 occupant_removable: false,
202 }),
203 (NodeKind::MatchArm, "pat") => Some(SlotSpec {
204 public_name: "pat",
205 meta_slot_name: "pat",
206 fragment_kind: FragmentKind::Pattern,
207 arity: SlotArity::Single,
208 occupant_removable: false,
209 }),
210 (NodeKind::MatchArm, "guard") => Some(SlotSpec {
211 public_name: "guard",
212 meta_slot_name: "guard",
213 fragment_kind: FragmentKind::Expr,
214 arity: SlotArity::Single,
215 occupant_removable: true,
216 }),
217 (NodeKind::MatchArm, "body") => Some(SlotSpec {
218 public_name: "body",
219 meta_slot_name: "body",
220 fragment_kind: FragmentKind::Expr,
221 arity: SlotArity::Single,
222 occupant_removable: false,
223 }),
224 _ => None,
225 }
226}
227
228pub fn ranked_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
229 slot_spec(owner, slot).filter(|spec| spec.arity == SlotArity::Ranked)
230}
231
232pub fn single_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
233 slot_spec(owner, slot).filter(|spec| spec.arity == SlotArity::Single)
234}
235
236pub fn removable_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
237 slot_spec(owner, slot).filter(|spec| spec.occupant_removable)
238}
239
240pub fn path_spec(kind: NodeKind, path: &str) -> Option<PathSpec> {
241 match (kind, path) {
242 (NodeKind::Mod, "name")
243 | (NodeKind::Struct, "name")
244 | (NodeKind::Enum, "name")
245 | (NodeKind::Fn, "name")
246 | (NodeKind::Field, "name")
247 | (NodeKind::Variant, "name")
248 | (NodeKind::Param, "name")
249 | (NodeKind::PatternIdent, "name") => Some(PathSpec {
250 public_name: "name",
251 value_kind: ValueKind::Ident,
252 clearable: false,
253 }),
254 (NodeKind::Doc, "text") | (NodeKind::Comment, "text") => Some(PathSpec {
255 public_name: "text",
256 value_kind: ValueKind::Str,
257 clearable: true,
258 }),
259 (NodeKind::ExprBinary, "op") => Some(PathSpec {
260 public_name: "op",
261 value_kind: ValueKind::Ident,
262 clearable: false,
263 }),
264 (NodeKind::ExprUnary, "op") => Some(PathSpec {
265 public_name: "op",
266 value_kind: ValueKind::Ident,
267 clearable: true,
268 }),
269 (NodeKind::ExprStmt, "semi") => Some(PathSpec {
270 public_name: "semi",
271 value_kind: ValueKind::Bool,
272 clearable: true,
273 }),
274 _ => None,
275 }
276}
277
278pub fn clearable_path_spec(kind: NodeKind, path: &str) -> Option<PathSpec> {
279 path_spec(kind, path).filter(|spec| spec.clearable)
280}
281
282pub fn replace_fragment_kind(kind: NodeKind) -> FragmentKind {
283 match kind {
284 NodeKind::Mod | NodeKind::Use | NodeKind::Struct | NodeKind::Enum | NodeKind::Fn => {
285 FragmentKind::Item
286 }
287 NodeKind::Field => FragmentKind::Field,
288 NodeKind::Variant => FragmentKind::Variant,
289 NodeKind::Param => FragmentKind::Param,
290 NodeKind::LetStmt | NodeKind::ExprStmt => FragmentKind::Stmt,
291 NodeKind::MatchArm => FragmentKind::MatchArm,
292 NodeKind::PatternIdent | NodeKind::PatternWild => FragmentKind::Pattern,
293 NodeKind::Type => FragmentKind::Type,
294 NodeKind::ExprPath
295 | NodeKind::ExprLit
296 | NodeKind::ExprGroup
297 | NodeKind::ExprBinary
298 | NodeKind::ExprUnary
299 | NodeKind::ExprCall
300 | NodeKind::ExprMatch
301 | NodeKind::ExprBlock => FragmentKind::Expr,
302 NodeKind::Doc => FragmentKind::Doc,
303 NodeKind::Comment => FragmentKind::Comment,
304 NodeKind::File => FragmentKind::Item,
305 }
306}
307
308pub fn item_kind(item: &Item) -> NodeKind {
309 match item {
310 Item::Mod(_) => NodeKind::Mod,
311 Item::Use(_) => NodeKind::Use,
312 Item::Struct(_) => NodeKind::Struct,
313 Item::Enum(_) => NodeKind::Enum,
314 Item::Fn(_) => NodeKind::Fn,
315 Item::Doc(_) => NodeKind::Doc,
316 Item::Comment(_) => NodeKind::Comment,
317 }
318}
319
320pub fn stmt_kind(stmt: &Stmt) -> NodeKind {
321 match stmt {
322 Stmt::Let(_) => NodeKind::LetStmt,
323 Stmt::Expr(_) => NodeKind::ExprStmt,
324 Stmt::Item(item) => item_kind(item),
325 Stmt::Doc(_) => NodeKind::Doc,
326 Stmt::Comment(_) => NodeKind::Comment,
327 }
328}
329
330pub fn expr_kind(expr: &Expr) -> NodeKind {
331 match expr {
332 Expr::Path(_) => NodeKind::ExprPath,
333 Expr::Lit(_) => NodeKind::ExprLit,
334 Expr::Group(_) => NodeKind::ExprGroup,
335 Expr::Binary(_) => NodeKind::ExprBinary,
336 Expr::Unary(_) => NodeKind::ExprUnary,
337 Expr::Call(_) => NodeKind::ExprCall,
338 Expr::Match(_) => NodeKind::ExprMatch,
339 Expr::Block(_) => NodeKind::ExprBlock,
340 }
341}
342
343pub fn pattern_kind(pattern: &Pattern) -> NodeKind {
344 match pattern {
345 Pattern::Ident(_) => NodeKind::PatternIdent,
346 Pattern::Wild(_) => NodeKind::PatternWild,
347 }
348}
349
350pub fn node_kind_label(kind: NodeKind) -> &'static str {
351 match kind {
352 NodeKind::File => "file",
353 NodeKind::Mod => "module",
354 NodeKind::Use => "use item",
355 NodeKind::Struct => "struct",
356 NodeKind::Enum => "enum",
357 NodeKind::Fn => "function",
358 NodeKind::Field => "field",
359 NodeKind::Variant => "variant",
360 NodeKind::Param => "parameter",
361 NodeKind::LetStmt => "let statement",
362 NodeKind::ExprStmt => "expression statement",
363 NodeKind::MatchArm => "match arm",
364 NodeKind::PatternIdent => "identifier pattern",
365 NodeKind::PatternWild => "wildcard pattern",
366 NodeKind::Type => "type",
367 NodeKind::ExprPath => "path expression",
368 NodeKind::ExprLit => "literal expression",
369 NodeKind::ExprGroup => "grouped expression",
370 NodeKind::ExprBinary => "binary expression",
371 NodeKind::ExprUnary => "unary expression",
372 NodeKind::ExprCall => "call expression",
373 NodeKind::ExprMatch => "match expression",
374 NodeKind::ExprBlock => "block expression",
375 NodeKind::Doc => "doc comment",
376 NodeKind::Comment => "line comment",
377 }
378}
379
380pub fn value_kind_label(value_kind: ValueKind) -> &'static str {
381 match value_kind {
382 ValueKind::Ident => "an identifier value",
383 ValueKind::Str => "a string value",
384 ValueKind::Bool => "a boolean value",
385 }
386}
387
388pub fn attachment_container_kind_for_owner(kind: NodeKind) -> Option<AttachmentContainerKind> {
389 match kind {
390 NodeKind::File | NodeKind::Mod => Some(AttachmentContainerKind::Items),
391 NodeKind::Fn | NodeKind::ExprBlock => Some(AttachmentContainerKind::Stmts),
392 _ => None,
393 }
394}
395
396pub fn attachment_closure_allowed(
397 owner_kind: NodeKind,
398 slot: &str,
399 closure_kind: AttachmentContainerKind,
400) -> bool {
401 let Some(spec) = ranked_slot_spec(owner_kind, slot) else {
402 return false;
403 };
404 matches!(
405 (
406 closure_kind,
407 attachment_container_kind_for_owner(owner_kind),
408 spec.fragment_kind
409 ),
410 (
411 AttachmentContainerKind::Items,
412 Some(AttachmentContainerKind::Items),
413 FragmentKind::Item
414 ) | (
415 AttachmentContainerKind::Stmts,
416 Some(AttachmentContainerKind::Stmts),
417 FragmentKind::Stmt
418 )
419 )
420}
421
422pub fn is_attachable_kind(kind: NodeKind) -> bool {
423 matches!(kind, NodeKind::Doc | NodeKind::Comment)
424}
425
426pub fn invalid_ranked_slot_message(owner_label: &str, slot: &str) -> String {
427 format!("slot `{owner_label}.{slot}` is not available for ranked insertion")
428}
429
430pub fn invalid_single_slot_message(owner_label: &str, slot: &str) -> String {
431 format!("slot `{owner_label}.{slot}` is not available for `put`")
432}
433
434pub fn invalid_set_path_message(node_id: &str, path: &str, kind: NodeKind) -> String {
435 format!(
436 "path `@{node_id}.{path}` is not settable on {}",
437 node_kind_label(kind)
438 )
439}
440
441pub fn invalid_clear_path_message(node_id: &str, path: &str, kind: NodeKind) -> String {
442 format!(
443 "path `@{node_id}.{path}` is not clearable on {}",
444 node_kind_label(kind)
445 )
446}
447
448pub fn required_slot_error_message(action: &str, target_id: &str, slot: &str) -> String {
449 format!(
450 "{} target `{}` cannot be removed from required slot `{}`",
451 action, target_id, slot
452 )
453}
454
455pub fn unsupported_slot_error_message(action: &str, target_id: &str, slot: &str) -> String {
456 format!(
457 "{} target `{}` is in unsupported slot `{}`",
458 action, target_id, slot
459 )
460}
461
462pub fn trivia_move_target_message() -> &'static str {
463 "move does not support doc or comment targets; use attach, detach, replace, or delete"
464}
465
466pub fn single_slot_attachment_closure_message() -> &'static str {
467 "cannot move a node with attached docs/comments into a single-child slot"
468}
469
470pub fn invalid_attachment_closure_destination_message(
471 closure_kind: AttachmentContainerKind,
472) -> &'static str {
473 match closure_kind {
474 AttachmentContainerKind::Items => {
475 "cannot move item attachments into a non-item ranked slot"
476 }
477 AttachmentContainerKind::Stmts => {
478 "cannot move statement attachments into a non-body ranked slot"
479 }
480 }
481}
482
483pub fn invalid_attachment_container_owner_message(
484 owner_label: &str,
485 closure_kind: AttachmentContainerKind,
486) -> String {
487 match closure_kind {
488 AttachmentContainerKind::Items => {
489 format!("owner `{owner_label}` does not expose an item attachment container")
490 }
491 AttachmentContainerKind::Stmts => {
492 format!("owner `{owner_label}` does not expose a statement body slot")
493 }
494 }
495}
496
497pub fn attach_target_not_sibling_message(target_id: &str, node_id: &str) -> String {
498 format!("attach target `{target_id}` is not a sibling semantic node for `{node_id}`")
499}
500
501pub fn detach_requires_following_sibling_message(node_id: &str) -> String {
502 format!("detach source `{node_id}` needs a following sibling semantic node")
503}
504
505pub fn find_node_kind(file: &File, node_id: &str) -> Option<NodeKind> {
506 for item in &file.items {
507 if let Some(kind) = find_in_item(item, node_id) {
508 return Some(kind);
509 }
510 }
511 None
512}
513
514fn find_in_item(item: &Item, node_id: &str) -> Option<NodeKind> {
515 if item.meta().id == node_id {
516 return Some(item_kind(item));
517 }
518
519 match item {
520 Item::Mod(module) => {
521 for child in &module.items {
522 if let Some(kind) = find_in_item(child, node_id) {
523 return Some(kind);
524 }
525 }
526 None
527 }
528 Item::Struct(strukt) => {
529 for field in &strukt.fields {
530 if field.meta.id == node_id {
531 return Some(NodeKind::Field);
532 }
533 if field.ty.meta().id == node_id {
534 return Some(NodeKind::Type);
535 }
536 }
537 None
538 }
539 Item::Enum(enm) => {
540 for variant in &enm.variants {
541 if variant.meta.id == node_id {
542 return Some(NodeKind::Variant);
543 }
544 }
545 None
546 }
547 Item::Fn(function) => {
548 for param in &function.params {
549 if param.meta.id == node_id {
550 return Some(NodeKind::Param);
551 }
552 if param.ty.meta().id == node_id {
553 return Some(NodeKind::Type);
554 }
555 }
556 if function
557 .ret_ty
558 .as_ref()
559 .is_some_and(|ret_ty| ret_ty.meta().id == node_id)
560 {
561 return Some(NodeKind::Type);
562 }
563 find_in_block(&function.body, node_id)
564 }
565 Item::Use(_) | Item::Doc(_) | Item::Comment(_) => None,
566 }
567}
568
569fn find_in_block(block: &draxl_ast::Block, node_id: &str) -> Option<NodeKind> {
570 if block.meta.as_ref().is_some_and(|meta| meta.id == node_id) {
571 return Some(NodeKind::ExprBlock);
572 }
573
574 for stmt in &block.stmts {
575 if let Some(kind) = find_in_stmt(stmt, node_id) {
576 return Some(kind);
577 }
578 }
579 None
580}
581
582fn find_in_stmt(stmt: &Stmt, node_id: &str) -> Option<NodeKind> {
583 match stmt {
584 Stmt::Let(node) => {
585 if node.meta.id == node_id {
586 return Some(NodeKind::LetStmt);
587 }
588 find_in_pattern(&node.pat, node_id).or_else(|| find_in_expr(&node.value, node_id))
589 }
590 Stmt::Expr(node) => {
591 if node.meta.id == node_id {
592 return Some(NodeKind::ExprStmt);
593 }
594 find_in_expr(&node.expr, node_id)
595 }
596 Stmt::Item(item) => find_in_item(item, node_id),
597 Stmt::Doc(node) => (node.meta.id == node_id).then_some(NodeKind::Doc),
598 Stmt::Comment(node) => (node.meta.id == node_id).then_some(NodeKind::Comment),
599 }
600}
601
602fn find_in_expr(expr: &Expr, node_id: &str) -> Option<NodeKind> {
603 if expr.meta().is_some_and(|meta| meta.id == node_id) {
604 return Some(expr_kind(expr));
605 }
606
607 match expr {
608 Expr::Group(group) => find_in_expr(&group.expr, node_id),
609 Expr::Binary(binary) => {
610 find_in_expr(&binary.lhs, node_id).or_else(|| find_in_expr(&binary.rhs, node_id))
611 }
612 Expr::Unary(unary) => find_in_expr(&unary.expr, node_id),
613 Expr::Call(call) => {
614 if let Some(kind) = find_in_expr(&call.callee, node_id) {
615 return Some(kind);
616 }
617 for arg in &call.args {
618 if let Some(kind) = find_in_expr(arg, node_id) {
619 return Some(kind);
620 }
621 }
622 None
623 }
624 Expr::Match(match_expr) => {
625 if let Some(kind) = find_in_expr(&match_expr.scrutinee, node_id) {
626 return Some(kind);
627 }
628 for arm in &match_expr.arms {
629 if arm.meta.id == node_id {
630 return Some(NodeKind::MatchArm);
631 }
632 if let Some(kind) = find_in_pattern(&arm.pat, node_id) {
633 return Some(kind);
634 }
635 if let Some(guard) = &arm.guard {
636 if let Some(kind) = find_in_expr(guard, node_id) {
637 return Some(kind);
638 }
639 }
640 if let Some(kind) = find_in_expr(&arm.body, node_id) {
641 return Some(kind);
642 }
643 }
644 None
645 }
646 Expr::Block(block) => find_in_block(block, node_id),
647 Expr::Path(_) | Expr::Lit(_) => None,
648 }
649}
650
651fn find_in_pattern(pattern: &Pattern, node_id: &str) -> Option<NodeKind> {
652 if pattern.meta().is_some_and(|meta| meta.id == node_id) {
653 return Some(pattern_kind(pattern));
654 }
655 None
656}