1use std::fmt;
40
41use serde_json::Value as JsonValue;
42
43use crate::catalog::{Catalog, Locale};
44use crate::core::error::WorkshopError;
45use crate::core::source::{Position, Span};
46use crate::frontend::keywords::structural_keyword_prefix;
47use crate::frontend::lexer::{Token, tokenize};
48use crate::program::{Action, Program};
49
50mod emit;
51mod layout;
52
53use crate::rules::statement::{Statement, StatementContentError, statement_from_content};
54use emit::{emit_section_keyword, emit_statement_rows};
55use layout::{
56 RuleLayout, StatementChunk, StatementSection, byte_is_whitespace, indent_unit, is_whitespace,
57 line_prefix, newline_of,
58};
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum StatementList {
64 Conditions,
66 Actions,
68}
69
70impl StatementList {
71 fn section_id(self) -> &'static str {
72 match self {
73 Self::Conditions => "conditions",
74 Self::Actions => "actions",
75 }
76 }
77
78 fn list_len(self, rule: &crate::Rule) -> usize {
79 match self {
80 Self::Conditions => rule.conditions.len(),
81 Self::Actions => rule.actions.len(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[non_exhaustive]
90pub struct StatementAddress {
91 pub rule: usize,
93 pub list: StatementList,
95 pub index: usize,
98}
99
100impl StatementAddress {
101 pub fn new(rule: usize, list: StatementList, index: usize) -> Self {
103 Self { rule, list, index }
104 }
105}
106
107#[derive(Debug, Clone, Copy)]
113#[non_exhaustive]
114pub enum StatementEdit<'a> {
115 Insert(&'a [JsonValue]),
118 Replace(&'a JsonValue),
121 Delete,
123}
124
125#[derive(Debug)]
128#[non_exhaustive]
129pub enum StatementEditError {
130 Content(StatementContentError),
132 UnknownRule {
134 rule: usize,
136 },
137 IndexOutOfRange {
141 rule: usize,
143 list: StatementList,
145 index: usize,
147 },
148 StatementKind {
151 rule: usize,
153 list: StatementList,
155 },
156 EmptyInsert {
158 rule: usize,
160 list: StatementList,
162 },
163 SourceUnavailable {
166 rule: usize,
168 },
169 StaleAddress {
174 rule: usize,
176 list: StatementList,
178 },
179 Emission(WorkshopError),
182 Edit(crate::source::SourceEditError),
184}
185
186impl fmt::Display for StatementEditError {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::Content(error) => write!(f, "{error}"),
190 Self::UnknownRule { rule } => write!(f, "no rule at index {rule}"),
191 Self::IndexOutOfRange { rule, list, index } => {
192 write!(f, "no {list:?} statement at index {index} in rule {rule}")
193 }
194 Self::StatementKind { rule, list } => write!(
195 f,
196 "statement kind does not match the {list:?} list of rule {rule}"
197 ),
198 Self::EmptyInsert { rule, list } => {
199 write!(f, "insert into {list:?} of rule {rule} needs a statement")
200 }
201 Self::SourceUnavailable { rule } => {
202 write!(f, "rule {rule} has no editable source")
203 }
204 Self::StaleAddress { rule, list } => write!(
205 f,
206 "the {list:?} of rule {rule} no longer matches its retained source"
207 ),
208 Self::Emission(error) => write!(f, "statement emission failed: {error}"),
209 Self::Edit(error) => write!(f, "source edit failed: {error}"),
210 }
211 }
212}
213
214impl std::error::Error for StatementEditError {}
215
216impl From<StatementContentError> for StatementEditError {
217 fn from(error: StatementContentError) -> Self {
218 Self::Content(error)
219 }
220}
221
222impl From<WorkshopError> for StatementEditError {
223 fn from(error: WorkshopError) -> Self {
224 Self::Emission(error)
225 }
226}
227
228impl From<crate::source::SourceEditError> for StatementEditError {
229 fn from(error: crate::source::SourceEditError) -> Self {
230 Self::Edit(error)
231 }
232}
233
234type Result<T> = std::result::Result<T, StatementEditError>;
235
236const ACTION_KEYWORD_IDS: &[&str] = &[
240 "disabled",
241 "if",
242 "elseIf",
243 "else",
244 "while",
245 "forGlobalVariable",
246 "forPlayerVariable",
247 "end",
248];
249
250impl Program {
251 pub fn edit_statements(
286 &self,
287 catalog: &Catalog,
288 locale: &Locale,
289 address: StatementAddress,
290 edit: StatementEdit<'_>,
291 ) -> Result<crate::source::SourceEdit> {
292 let rule = self
293 .rules
294 .get(address.rule)
295 .ok_or(StatementEditError::UnknownRule { rule: address.rule })?;
296
297 let statements = convert_statements(catalog, address, &edit)?;
298 let list_len = address.list.list_len(rule);
299 match edit {
300 StatementEdit::Insert(_) => {
301 if address.index > list_len {
302 return Err(StatementEditError::IndexOutOfRange {
303 rule: address.rule,
304 list: address.list,
305 index: address.index,
306 });
307 }
308 }
309 StatementEdit::Replace(_) | StatementEdit::Delete => {
310 if address.index >= list_len {
311 return Err(StatementEditError::IndexOutOfRange {
312 rule: address.rule,
313 list: address.list,
314 index: address.index,
315 });
316 }
317 }
318 }
319
320 let rows = emit_statement_rows(&statements, self, catalog, locale)?;
321
322 let rule_span = self
323 .rule_span(address.rule)
324 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
325 let document = self
326 .source(rule_span.file)
327 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
328 let text = document.text();
329 let tokens = tokenize(text)
330 .map_err(|_| StatementEditError::SourceUnavailable { rule: address.rule })?;
331 let layout = RuleLayout::scan(&tokens, rule_span, catalog, locale)
332 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
333
334 let section = layout
335 .sections
336 .iter()
337 .find(|section| section.id == address.list.section_id());
338 let Some(section) = section else {
339 let unit = indent_unit(text, &tokens, &layout, rule_span);
340 return self.insert_created_section(
341 catalog, locale, address, list_len, &rows, &unit, &tokens, &layout, text, rule_span,
342 );
343 };
344
345 let synthesized_tail = match address.list {
346 StatementList::Conditions => section.chunks.len() != list_len,
347 StatementList::Actions => {
348 section.chunks.len() > list_len
349 || !rule.actions[section.chunks.len()..]
350 .iter()
351 .all(|action| matches!(action, Action::End))
352 }
353 };
354 if synthesized_tail {
355 return Err(StatementEditError::StaleAddress {
356 rule: address.rule,
357 list: address.list,
358 });
359 }
360
361 match edit {
362 StatementEdit::Insert(_) => {
363 if let Some(chunk) = section.chunks.get(address.index) {
367 self.verify_statement(
368 address,
369 *chunk,
370 &tokens,
371 catalog,
372 locale,
373 rule_span.file,
374 )?;
375 self.insert_before_chunk(address, &rows, *chunk, &tokens, text, rule_span)
376 } else {
377 let unit = indent_unit(text, &tokens, &layout, rule_span);
378 self.insert_at_close(address, &rows, &unit, section, &tokens, text, rule_span)
379 }
380 }
381 StatementEdit::Replace(_) => {
382 let chunk =
383 section
384 .chunks
385 .get(address.index)
386 .ok_or(StatementEditError::StaleAddress {
387 rule: address.rule,
388 list: address.list,
389 })?;
390 self.verify_statement(address, *chunk, &tokens, catalog, locale, rule_span.file)?;
391 let row = rows.first().expect("replace emits one row");
392 self.replace_chunk(address, row, *chunk, &tokens, rule_span)
393 }
394 StatementEdit::Delete => {
395 let chunk =
396 section
397 .chunks
398 .get(address.index)
399 .ok_or(StatementEditError::StaleAddress {
400 rule: address.rule,
401 list: address.list,
402 })?;
403 self.verify_statement(address, *chunk, &tokens, catalog, locale, rule_span.file)?;
404 self.delete_chunk(address, *chunk, &tokens, text, rule_span)
405 }
406 }
407 }
408
409 fn verify_statement(
413 &self,
414 address: StatementAddress,
415 chunk: StatementChunk,
416 tokens: &[Token],
417 catalog: &Catalog,
418 locale: &Locale,
419 file: crate::source::FileId,
420 ) -> Result<()> {
421 let chunk_start = tokens[chunk.first_content].start;
422 let chunk_end = tokens[chunk.semi].end;
423 let within = |start: Position| {
424 (start.line, start.col) >= (chunk_start.line, chunk_start.col)
425 && (start.line, start.col) <= (chunk_end.line, chunk_end.col)
426 };
427 let verified = match address.list {
428 StatementList::Conditions => self
429 .condition_span(address.rule, address.index)
430 .is_some_and(|span| span.file == file && within(span.start)),
431 StatementList::Actions => match &self.rules[address.rule].actions[address.index] {
432 Action::Else | Action::End => {
433 let expected = if matches!(
434 &self.rules[address.rule].actions[address.index],
435 Action::Else
436 ) {
437 "else"
438 } else {
439 "end"
440 };
441 structural_keyword_prefix(
442 tokens,
443 chunk.first_content,
444 catalog,
445 locale,
446 ACTION_KEYWORD_IDS,
447 )
448 .is_some_and(|(id, _)| id == expected)
449 }
450 Action::ElseIf { .. } => self
451 .action_argument_span(address.rule, address.index, 0)
452 .is_some_and(|span| span.file == file && within(span.start)),
453 _ => self
454 .action_span(address.rule, address.index)
455 .is_some_and(|span| span.file == file && within(span.start)),
456 },
457 };
458 if verified {
459 Ok(())
460 } else {
461 Err(StatementEditError::StaleAddress {
462 rule: address.rule,
463 list: address.list,
464 })
465 }
466 }
467
468 fn insert_before_chunk(
471 &self,
472 address: StatementAddress,
473 rows: &[String],
474 chunk: StatementChunk,
475 tokens: &[Token],
476 text: &str,
477 rule_span: Span,
478 ) -> Result<crate::source::SourceEdit> {
479 let anchor = tokens[chunk.first].start;
480 let newline = newline_of(text, anchor);
481 let line_start = Position::new(anchor.line, 1);
482 let indent = line_prefix(text, anchor);
483 let (position, replacement) = if is_whitespace(indent) {
484 let mut replacement = String::new();
485 for row in rows {
486 replacement.push_str(indent);
487 replacement.push_str(row);
488 replacement.push_str(newline);
489 }
490 (line_start, replacement)
491 } else {
492 let left = if byte_is_whitespace(text, anchor) {
493 ""
494 } else {
495 " "
496 };
497 (anchor, format!("{left}{} ", rows.join(" ")))
498 };
499 self.checked_edit(rule_span, position, position, replacement)
500 .map_err(|error| wrap_edit_error(address, error))
501 }
502
503 #[allow(clippy::too_many_arguments)]
506 fn insert_at_close(
507 &self,
508 address: StatementAddress,
509 rows: &[String],
510 unit: &str,
511 section: &StatementSection,
512 tokens: &[Token],
513 text: &str,
514 rule_span: Span,
515 ) -> Result<crate::source::SourceEdit> {
516 let close = tokens[section.dangling.unwrap_or(section.close)].start;
519 let newline = newline_of(text, close);
520 let close_indent = line_prefix(text, close);
521 let (position, replacement) = if is_whitespace(close_indent) {
522 let indent = section
523 .chunks
524 .last()
525 .map(|chunk| line_prefix(text, tokens[chunk.first].start))
526 .filter(|prefix| is_whitespace(prefix))
527 .map(str::to_string)
528 .unwrap_or_else(|| format!("{close_indent}{unit}"));
529 let mut replacement = String::new();
530 for row in rows {
531 replacement.push_str(&indent);
532 replacement.push_str(row);
533 replacement.push_str(newline);
534 }
535 (Position::new(close.line, 1), replacement)
536 } else {
537 let left = if byte_is_whitespace(text, close) {
538 ""
539 } else {
540 " "
541 };
542 (close, format!("{left}{} ", rows.join(" ")))
543 };
544 self.checked_edit(rule_span, position, position, replacement)
545 .map_err(|error| wrap_edit_error(address, error))
546 }
547
548 #[allow(clippy::too_many_arguments)]
553 fn insert_created_section(
554 &self,
555 catalog: &Catalog,
556 locale: &Locale,
557 address: StatementAddress,
558 list_len: usize,
559 rows: &[String],
560 unit: &str,
561 tokens: &[Token],
562 layout: &RuleLayout,
563 text: &str,
564 rule_span: Span,
565 ) -> Result<crate::source::SourceEdit> {
566 if list_len != 0 || address.index != 0 || rows.is_empty() {
570 return Err(if list_len == 0 {
571 StatementEditError::IndexOutOfRange {
572 rule: address.rule,
573 list: address.list,
574 index: address.index,
575 }
576 } else {
577 StatementEditError::StaleAddress {
578 rule: address.rule,
579 list: address.list,
580 }
581 });
582 }
583 let keyword = emit_section_keyword(address.list, catalog, locale)?;
584 let anchor = layout
585 .sections
586 .iter()
587 .find(|section| section.id == "actions")
588 .map(|section| section.name_start)
589 .unwrap_or_else(|| tokens[layout.close].start);
590 let newline = newline_of(text, anchor);
591 let section_indent = layout
594 .sections
595 .last()
596 .map(|section| line_prefix(text, section.name_start))
597 .filter(|prefix| is_whitespace(prefix))
598 .map(str::to_string);
599 let anchor_indent = line_prefix(text, anchor);
600 let (position, replacement) = if is_whitespace(anchor_indent) {
601 let indent = section_indent.unwrap_or_else(|| format!("{anchor_indent}{unit}"));
602 let statement_indent = format!("{indent}{unit}");
603 let mut replacement = format!("{indent}{keyword} {{{newline}");
604 for row in rows {
605 replacement.push_str(&statement_indent);
606 replacement.push_str(row);
607 replacement.push_str(newline);
608 }
609 replacement.push_str(&indent);
610 replacement.push('}');
611 replacement.push_str(newline);
612 (Position::new(anchor.line, 1), replacement)
613 } else {
614 let left = if byte_is_whitespace(text, anchor) {
615 ""
616 } else {
617 " "
618 };
619 (anchor, format!("{left}{keyword} {{ {} }} ", rows.join(" ")))
620 };
621 self.checked_edit(rule_span, position, position, replacement)
622 .map_err(|error| wrap_edit_error(address, error))
623 }
624
625 fn replace_chunk(
628 &self,
629 address: StatementAddress,
630 row: &str,
631 chunk: StatementChunk,
632 tokens: &[Token],
633 rule_span: Span,
634 ) -> Result<crate::source::SourceEdit> {
635 let start = tokens[chunk.first_content].start;
636 let end = tokens[chunk.semi].end;
637 self.checked_edit(rule_span, start, end, row)
638 .map_err(|error| wrap_edit_error(address, error))
639 }
640
641 fn delete_chunk(
647 &self,
648 address: StatementAddress,
649 chunk: StatementChunk,
650 tokens: &[Token],
651 text: &str,
652 rule_span: Span,
653 ) -> Result<crate::source::SourceEdit> {
654 let first = tokens[chunk.first].start;
655 let semi_end = tokens[chunk.semi].end;
656 let starts_line = is_whitespace(line_prefix(text, first));
657 let next = tokens.get(chunk.semi + 1).map(|token| token.start);
660 let content_follows = next.is_some_and(|next| next.line == semi_end.line);
661 let (start, end) = match (starts_line, content_follows) {
662 (true, false) => (
663 Position::new(first.line, 1),
664 Position::new(semi_end.line + 1, 1),
665 ),
666 (_, true) => (first, next.expect("content follows")),
667 (false, false) => {
668 let blanks = line_prefix(text, first)
669 .chars()
670 .rev()
671 .take_while(|character| matches!(character, ' ' | '\t'))
672 .count();
673 (
674 Position::new(first.line, first.col - blanks as u32),
675 semi_end,
676 )
677 }
678 };
679 self.checked_edit(rule_span, start, end, "")
680 .map_err(|error| wrap_edit_error(address, error))
681 }
682
683 fn checked_edit(
684 &self,
685 rule_span: Span,
686 start: Position,
687 end: Position,
688 replacement: impl Into<String>,
689 ) -> std::result::Result<crate::source::SourceEdit, crate::source::SourceEditError> {
690 self.edit_source(Span::new(rule_span.file, start, end), replacement)
691 }
692}
693
694fn wrap_edit_error(
695 address: StatementAddress,
696 error: crate::source::SourceEditError,
697) -> StatementEditError {
698 match error {
699 crate::source::SourceEditError::InvalidRange => {
700 StatementEditError::SourceUnavailable { rule: address.rule }
701 }
702 other => StatementEditError::Edit(other),
703 }
704}
705
706fn convert_statements(
707 catalog: &Catalog,
708 address: StatementAddress,
709 edit: &StatementEdit<'_>,
710) -> Result<Vec<Statement>> {
711 let nodes: &[JsonValue] = match edit {
712 StatementEdit::Insert(nodes) => {
713 if nodes.is_empty() {
714 return Err(StatementEditError::EmptyInsert {
715 rule: address.rule,
716 list: address.list,
717 });
718 }
719 nodes
720 }
721 StatementEdit::Replace(node) => std::slice::from_ref(node),
722 StatementEdit::Delete => &[],
723 };
724 let mut statements = Vec::with_capacity(nodes.len());
725 for node in nodes {
726 let statement = statement_from_content(catalog, node)?;
727 let fits = matches!(
728 (address.list, &statement),
729 (StatementList::Conditions, Statement::Condition(_))
730 | (StatementList::Actions, Statement::Action(_))
731 );
732 if !fits {
733 return Err(StatementEditError::StatementKind {
734 rule: address.rule,
735 list: address.list,
736 });
737 }
738 statements.push(statement);
739 }
740 Ok(statements)
741}