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 { rule: usize },
134 IndexOutOfRange {
138 rule: usize,
139 list: StatementList,
140 index: usize,
141 },
142 StatementKind { rule: usize, list: StatementList },
145 EmptyInsert { rule: usize, list: StatementList },
147 SourceUnavailable { rule: usize },
150 StaleAddress { rule: usize, list: StatementList },
155 Emission(WorkshopError),
158 Edit(crate::source::SourceEditError),
160}
161
162impl fmt::Display for StatementEditError {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 match self {
165 Self::Content(error) => write!(f, "{error}"),
166 Self::UnknownRule { rule } => write!(f, "no rule at index {rule}"),
167 Self::IndexOutOfRange { rule, list, index } => {
168 write!(f, "no {list:?} statement at index {index} in rule {rule}")
169 }
170 Self::StatementKind { rule, list } => write!(
171 f,
172 "statement kind does not match the {list:?} list of rule {rule}"
173 ),
174 Self::EmptyInsert { rule, list } => {
175 write!(f, "insert into {list:?} of rule {rule} needs a statement")
176 }
177 Self::SourceUnavailable { rule } => {
178 write!(f, "rule {rule} has no editable source")
179 }
180 Self::StaleAddress { rule, list } => write!(
181 f,
182 "the {list:?} of rule {rule} no longer matches its retained source"
183 ),
184 Self::Emission(error) => write!(f, "statement emission failed: {error}"),
185 Self::Edit(error) => write!(f, "source edit failed: {error}"),
186 }
187 }
188}
189
190impl std::error::Error for StatementEditError {}
191
192impl From<StatementContentError> for StatementEditError {
193 fn from(error: StatementContentError) -> Self {
194 Self::Content(error)
195 }
196}
197
198impl From<WorkshopError> for StatementEditError {
199 fn from(error: WorkshopError) -> Self {
200 Self::Emission(error)
201 }
202}
203
204impl From<crate::source::SourceEditError> for StatementEditError {
205 fn from(error: crate::source::SourceEditError) -> Self {
206 Self::Edit(error)
207 }
208}
209
210type Result<T> = std::result::Result<T, StatementEditError>;
211
212const ACTION_KEYWORD_IDS: &[&str] = &[
213 "disabled",
214 "if",
215 "elseIf",
216 "else",
217 "while",
218 "forGlobalVariable",
219 "forPlayerVariable",
220 "end",
221];
222
223impl Program {
224 pub fn edit_statements(
259 &self,
260 catalog: &Catalog,
261 locale: &Locale,
262 address: StatementAddress,
263 edit: StatementEdit<'_>,
264 ) -> Result<crate::source::SourceEdit> {
265 let rule = self
266 .rules
267 .get(address.rule)
268 .ok_or(StatementEditError::UnknownRule { rule: address.rule })?;
269
270 let statements = convert_statements(catalog, address, &edit)?;
271 let list_len = address.list.list_len(rule);
272 match edit {
273 StatementEdit::Insert(_) => {
274 if address.index > list_len {
275 return Err(StatementEditError::IndexOutOfRange {
276 rule: address.rule,
277 list: address.list,
278 index: address.index,
279 });
280 }
281 }
282 StatementEdit::Replace(_) | StatementEdit::Delete => {
283 if address.index >= list_len {
284 return Err(StatementEditError::IndexOutOfRange {
285 rule: address.rule,
286 list: address.list,
287 index: address.index,
288 });
289 }
290 }
291 }
292
293 let rows = emit_statement_rows(&statements, self, catalog, locale)?;
294
295 let rule_span = self
296 .rule_span(address.rule)
297 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
298 let document = self
299 .source(rule_span.file)
300 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
301 let text = document.text();
302 let tokens = tokenize(text)
303 .map_err(|_| StatementEditError::SourceUnavailable { rule: address.rule })?;
304 let layout = RuleLayout::scan(&tokens, rule_span, catalog, locale)
305 .ok_or(StatementEditError::SourceUnavailable { rule: address.rule })?;
306
307 let section = layout
308 .sections
309 .iter()
310 .find(|section| section.id == address.list.section_id());
311 let Some(section) = section else {
312 let unit = indent_unit(text, &tokens, &layout, rule_span);
313 return self.insert_created_section(
314 catalog, locale, address, list_len, &rows, &unit, &tokens, &layout, text, rule_span,
315 );
316 };
317
318 let synthesized_tail = match address.list {
319 StatementList::Conditions => section.chunks.len() != list_len,
320 StatementList::Actions => {
321 section.chunks.len() > list_len
322 || !rule.actions[section.chunks.len()..]
323 .iter()
324 .all(|action| matches!(action, Action::End))
325 }
326 };
327 if synthesized_tail {
328 return Err(StatementEditError::StaleAddress {
329 rule: address.rule,
330 list: address.list,
331 });
332 }
333
334 match edit {
335 StatementEdit::Insert(_) => {
336 if let Some(chunk) = section.chunks.get(address.index) {
337 self.verify_statement(
338 address,
339 *chunk,
340 &tokens,
341 catalog,
342 locale,
343 rule_span.file,
344 )?;
345 self.insert_before_chunk(address, &rows, *chunk, &tokens, text, rule_span)
346 } else {
347 let unit = indent_unit(text, &tokens, &layout, rule_span);
348 self.insert_at_close(address, &rows, &unit, section, &tokens, text, rule_span)
349 }
350 }
351 StatementEdit::Replace(_) => {
352 let chunk =
353 section
354 .chunks
355 .get(address.index)
356 .ok_or(StatementEditError::StaleAddress {
357 rule: address.rule,
358 list: address.list,
359 })?;
360 self.verify_statement(address, *chunk, &tokens, catalog, locale, rule_span.file)?;
361 let row = rows.first().expect("replace emits one row");
362 self.replace_chunk(address, row, *chunk, &tokens, rule_span)
363 }
364 StatementEdit::Delete => {
365 let chunk =
366 section
367 .chunks
368 .get(address.index)
369 .ok_or(StatementEditError::StaleAddress {
370 rule: address.rule,
371 list: address.list,
372 })?;
373 self.verify_statement(address, *chunk, &tokens, catalog, locale, rule_span.file)?;
374 self.delete_chunk(address, *chunk, &tokens, text, rule_span)
375 }
376 }
377 }
378
379 fn verify_statement(
380 &self,
381 address: StatementAddress,
382 chunk: StatementChunk,
383 tokens: &[Token],
384 catalog: &Catalog,
385 locale: &Locale,
386 file: crate::source::FileId,
387 ) -> Result<()> {
388 let chunk_start = tokens[chunk.first_content].start;
389 let chunk_end = tokens[chunk.semi].end;
390 let within = |start: Position| {
391 (start.line, start.col) >= (chunk_start.line, chunk_start.col)
392 && (start.line, start.col) <= (chunk_end.line, chunk_end.col)
393 };
394 let verified = match address.list {
395 StatementList::Conditions => self
396 .condition_span(address.rule, address.index)
397 .is_some_and(|span| span.file == file && within(span.start)),
398 StatementList::Actions => match &self.rules[address.rule].actions[address.index] {
399 Action::Else | Action::End => {
400 let expected = if matches!(
401 &self.rules[address.rule].actions[address.index],
402 Action::Else
403 ) {
404 "else"
405 } else {
406 "end"
407 };
408 structural_keyword_prefix(
409 tokens,
410 chunk.first_content,
411 catalog,
412 locale,
413 ACTION_KEYWORD_IDS,
414 )
415 .is_some_and(|(id, _)| id == expected)
416 }
417 Action::ElseIf { .. } => self
418 .action_argument_span(address.rule, address.index, 0)
419 .is_some_and(|span| span.file == file && within(span.start)),
420 _ => self
421 .action_span(address.rule, address.index)
422 .is_some_and(|span| span.file == file && within(span.start)),
423 },
424 };
425 if verified {
426 Ok(())
427 } else {
428 Err(StatementEditError::StaleAddress {
429 rule: address.rule,
430 list: address.list,
431 })
432 }
433 }
434
435 fn insert_before_chunk(
436 &self,
437 address: StatementAddress,
438 rows: &[String],
439 chunk: StatementChunk,
440 tokens: &[Token],
441 text: &str,
442 rule_span: Span,
443 ) -> Result<crate::source::SourceEdit> {
444 let anchor = tokens[chunk.first].start;
445 let newline = newline_of(text, anchor);
446 let line_start = Position::new(anchor.line, 1);
447 let indent = line_prefix(text, anchor);
448 let (position, replacement) = if is_whitespace(indent) {
449 let mut replacement = String::new();
450 for row in rows {
451 replacement.push_str(indent);
452 replacement.push_str(row);
453 replacement.push_str(newline);
454 }
455 (line_start, replacement)
456 } else {
457 let left = if byte_is_whitespace(text, anchor) {
458 ""
459 } else {
460 " "
461 };
462 (anchor, format!("{left}{} ", rows.join(" ")))
463 };
464 self.checked_edit(rule_span, position, position, replacement)
465 .map_err(|error| wrap_edit_error(address, error))
466 }
467
468 #[allow(clippy::too_many_arguments)]
469 fn insert_at_close(
470 &self,
471 address: StatementAddress,
472 rows: &[String],
473 unit: &str,
474 section: &StatementSection,
475 tokens: &[Token],
476 text: &str,
477 rule_span: Span,
478 ) -> Result<crate::source::SourceEdit> {
479 let close = tokens[section.dangling.unwrap_or(section.close)].start;
480 let newline = newline_of(text, close);
481 let close_indent = line_prefix(text, close);
482 let (position, replacement) = if is_whitespace(close_indent) {
483 let indent = section
484 .chunks
485 .last()
486 .map(|chunk| line_prefix(text, tokens[chunk.first].start))
487 .filter(|prefix| is_whitespace(prefix))
488 .map(str::to_string)
489 .unwrap_or_else(|| format!("{close_indent}{unit}"));
490 let mut replacement = String::new();
491 for row in rows {
492 replacement.push_str(&indent);
493 replacement.push_str(row);
494 replacement.push_str(newline);
495 }
496 (Position::new(close.line, 1), replacement)
497 } else {
498 let left = if byte_is_whitespace(text, close) {
499 ""
500 } else {
501 " "
502 };
503 (close, format!("{left}{} ", rows.join(" ")))
504 };
505 self.checked_edit(rule_span, position, position, replacement)
506 .map_err(|error| wrap_edit_error(address, error))
507 }
508
509 #[allow(clippy::too_many_arguments)]
510 fn insert_created_section(
511 &self,
512 catalog: &Catalog,
513 locale: &Locale,
514 address: StatementAddress,
515 list_len: usize,
516 rows: &[String],
517 unit: &str,
518 tokens: &[Token],
519 layout: &RuleLayout,
520 text: &str,
521 rule_span: Span,
522 ) -> Result<crate::source::SourceEdit> {
523 if list_len != 0 || address.index != 0 || rows.is_empty() {
524 return Err(if list_len == 0 {
525 StatementEditError::IndexOutOfRange {
526 rule: address.rule,
527 list: address.list,
528 index: address.index,
529 }
530 } else {
531 StatementEditError::StaleAddress {
532 rule: address.rule,
533 list: address.list,
534 }
535 });
536 }
537 let keyword = emit_section_keyword(address.list, catalog, locale)?;
538 let anchor = layout
539 .sections
540 .iter()
541 .find(|section| section.id == "actions")
542 .map(|section| section.name_start)
543 .unwrap_or_else(|| tokens[layout.close].start);
544 let newline = newline_of(text, anchor);
545 let section_indent = layout
546 .sections
547 .last()
548 .map(|section| line_prefix(text, section.name_start))
549 .filter(|prefix| is_whitespace(prefix))
550 .map(str::to_string);
551 let anchor_indent = line_prefix(text, anchor);
552 let (position, replacement) = if is_whitespace(anchor_indent) {
553 let indent = section_indent.unwrap_or_else(|| format!("{anchor_indent}{unit}"));
554 let statement_indent = format!("{indent}{unit}");
555 let mut replacement = format!("{indent}{keyword} {{{newline}");
556 for row in rows {
557 replacement.push_str(&statement_indent);
558 replacement.push_str(row);
559 replacement.push_str(newline);
560 }
561 replacement.push_str(&indent);
562 replacement.push('}');
563 replacement.push_str(newline);
564 (Position::new(anchor.line, 1), replacement)
565 } else {
566 let left = if byte_is_whitespace(text, anchor) {
567 ""
568 } else {
569 " "
570 };
571 (anchor, format!("{left}{keyword} {{ {} }} ", rows.join(" ")))
572 };
573 self.checked_edit(rule_span, position, position, replacement)
574 .map_err(|error| wrap_edit_error(address, error))
575 }
576
577 fn replace_chunk(
578 &self,
579 address: StatementAddress,
580 row: &str,
581 chunk: StatementChunk,
582 tokens: &[Token],
583 rule_span: Span,
584 ) -> Result<crate::source::SourceEdit> {
585 let start = tokens[chunk.first_content].start;
586 let end = tokens[chunk.semi].end;
587 self.checked_edit(rule_span, start, end, row)
588 .map_err(|error| wrap_edit_error(address, error))
589 }
590
591 fn delete_chunk(
592 &self,
593 address: StatementAddress,
594 chunk: StatementChunk,
595 tokens: &[Token],
596 text: &str,
597 rule_span: Span,
598 ) -> Result<crate::source::SourceEdit> {
599 let first = tokens[chunk.first].start;
600 let semi_end = tokens[chunk.semi].end;
601 let starts_line = is_whitespace(line_prefix(text, first));
602 let next = tokens.get(chunk.semi + 1).map(|token| token.start);
603 let content_follows = next.is_some_and(|next| next.line == semi_end.line);
604 let (start, end) = match (starts_line, content_follows) {
605 (true, false) => (
606 Position::new(first.line, 1),
607 Position::new(semi_end.line + 1, 1),
608 ),
609 (_, true) => (first, next.expect("content follows")),
610 (false, false) => {
611 let blanks = line_prefix(text, first)
612 .chars()
613 .rev()
614 .take_while(|character| matches!(character, ' ' | '\t'))
615 .count();
616 (
617 Position::new(first.line, first.col - blanks as u32),
618 semi_end,
619 )
620 }
621 };
622 self.checked_edit(rule_span, start, end, "")
623 .map_err(|error| wrap_edit_error(address, error))
624 }
625
626 fn checked_edit(
627 &self,
628 rule_span: Span,
629 start: Position,
630 end: Position,
631 replacement: impl Into<String>,
632 ) -> std::result::Result<crate::source::SourceEdit, crate::source::SourceEditError> {
633 self.edit_source(Span::new(rule_span.file, start, end), replacement)
634 }
635}
636
637fn wrap_edit_error(
638 address: StatementAddress,
639 error: crate::source::SourceEditError,
640) -> StatementEditError {
641 match error {
642 crate::source::SourceEditError::InvalidRange => {
643 StatementEditError::SourceUnavailable { rule: address.rule }
644 }
645 other => StatementEditError::Edit(other),
646 }
647}
648
649fn convert_statements(
650 catalog: &Catalog,
651 address: StatementAddress,
652 edit: &StatementEdit<'_>,
653) -> Result<Vec<Statement>> {
654 let nodes: &[JsonValue] = match edit {
655 StatementEdit::Insert(nodes) => {
656 if nodes.is_empty() {
657 return Err(StatementEditError::EmptyInsert {
658 rule: address.rule,
659 list: address.list,
660 });
661 }
662 nodes
663 }
664 StatementEdit::Replace(node) => std::slice::from_ref(node),
665 StatementEdit::Delete => &[],
666 };
667 let mut statements = Vec::with_capacity(nodes.len());
668 for node in nodes {
669 let statement = statement_from_content(catalog, node)?;
670 let fits = matches!(
671 (address.list, &statement),
672 (StatementList::Conditions, Statement::Condition(_))
673 | (StatementList::Actions, Statement::Action(_))
674 );
675 if !fits {
676 return Err(StatementEditError::StatementKind {
677 rule: address.rule,
678 list: address.list,
679 });
680 }
681 statements.push(statement);
682 }
683 Ok(statements)
684}