1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5#[cfg(any(feature = "diagram-packet", feature = "diagram-radar"))]
6use crate::error::Error;
7pub use crate::generated::editor_rename_policy::EditorRenamePolicy;
8use crate::{OperationControl, OperationControlResult};
9
10#[cfg(any(
11 feature = "diagram-class",
12 feature = "diagram-er",
13 feature = "diagram-flowchart",
14 feature = "diagram-swimlane",
15 feature = "diagram-sequence",
16 feature = "diagram-state"
17))]
18use crate::error::{ParseDiagnostic, ParseDiagnosticSpanKind, ParseErrorSourceSpan};
19
20pub fn flowchart_shape_names() -> impl Iterator<Item = &'static str> {
26 #[cfg(any(
27 feature = "diagram-flowchart",
28 feature = "diagram-swimlane",
29 feature = "diagram-agentflow"
30 ))]
31 {
32 crate::diagrams::flowchart::flowchart_public_shape_names()
33 }
34 #[cfg(not(any(
35 feature = "diagram-flowchart",
36 feature = "diagram-swimlane",
37 feature = "diagram-agentflow"
38 )))]
39 {
40 std::iter::empty()
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46pub struct SourceSpan {
47 pub start: usize,
48 pub end: usize,
49}
50
51pub(crate) fn line_content_end(source: &str, end: usize) -> usize {
52 let end = end.min(source.len());
53 end.checked_sub(1)
54 .filter(|index| source.as_bytes().get(*index) == Some(&b'\r'))
55 .unwrap_or(end)
56}
57
58#[allow(
59 dead_code,
60 reason = "Shared parser facilities have different consumers in each family selection."
61)]
62pub(crate) fn has_ascii_separator(source: &str, start: usize, end: usize) -> bool {
63 source.get(start..end).is_some_and(|slice| {
64 !slice.is_empty() && slice.as_bytes().iter().all(u8::is_ascii_whitespace)
65 })
66}
67
68#[allow(
69 dead_code,
70 reason = "Shared parser facilities have different consumers in each family selection."
71)]
72pub(crate) fn trailing_ascii_whitespace_slot(
73 source: &str,
74 start: usize,
75 end: usize,
76) -> Option<SourceSpan> {
77 let end = line_content_end(source, end);
78 let slice = source.get(start..end)?;
79 (!slice.is_empty() && slice.as_bytes().last().is_some_and(u8::is_ascii_whitespace))
80 .then_some(SourceSpan::new(end, end))
81}
82
83#[allow(
84 dead_code,
85 reason = "Shared parser facilities have different consumers in each family selection."
86)]
87pub(crate) fn source_value_span(source: &str, span: SourceSpan, value: &str) -> Option<SourceSpan> {
88 let slice = source.get(span.start..span.end)?;
89 let relative_start = slice.find(value)?;
90 Some(SourceSpan::new(
91 span.start + relative_start,
92 span.start + relative_start + value.len(),
93 ))
94}
95
96impl SourceSpan {
97 pub fn new(start: usize, end: usize) -> Self {
98 Self { start, end }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
104pub enum EditorSemanticKind {
105 Class,
106 Event,
107 Function,
108 Module,
109 Namespace,
110 Object,
111 Package,
112 Property,
113 String,
114 Struct,
115 Variable,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct EditorFamilySemantics {
124 outline_kind: EditorSemanticKind,
125}
126
127impl Default for EditorFamilySemantics {
128 fn default() -> Self {
129 Self {
130 outline_kind: EditorSemanticKind::Variable,
131 }
132 }
133}
134
135impl EditorFamilySemantics {
136 pub(crate) const fn new(outline_kind: EditorSemanticKind) -> Self {
137 Self { outline_kind }
138 }
139
140 pub const fn outline_kind(self) -> EditorSemanticKind {
141 self.outline_kind
142 }
143}
144
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
147pub enum EditorSemanticRole {
148 #[default]
150 Entity,
151 ClassDefinition,
154 Reference,
157 Outline,
159 Payload,
162}
163
164impl EditorRenamePolicy {
165 pub fn is_renameable(self) -> bool {
166 !matches!(self, Self::None)
167 }
168
169 pub fn accepts(self, candidate: &str) -> bool {
170 match self {
171 Self::None => false,
172 Self::Identifier => {
173 !candidate.is_empty()
174 && candidate
175 .chars()
176 .all(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '-'))
177 }
178 Self::QualifiedIdentifier => {
179 !candidate.is_empty() && candidate.split('.').all(is_ascii_identifier)
180 }
181 Self::EventModelingId => is_ascii_identifier(candidate),
182 Self::EventModelingFrameId => {
183 (1..=3).contains(&candidate.len())
184 && candidate.bytes().all(|byte| byte.is_ascii_digit())
185 }
186 #[cfg(not(any(
187 feature = "diagram-flowchart",
188 feature = "diagram-swimlane",
189 feature = "diagram-agentflow"
190 )))]
191 Self::FlowchartNodeId => false,
192 #[cfg(any(
193 feature = "diagram-flowchart",
194 feature = "diagram-swimlane",
195 feature = "diagram-agentflow"
196 ))]
197 Self::FlowchartNodeId => crate::diagrams::flowchart::is_valid_editor_node_id(candidate),
198 #[cfg(not(feature = "diagram-git-graph"))]
199 Self::GitGraphReference => false,
200 #[cfg(feature = "diagram-git-graph")]
201 Self::GitGraphReference => {
202 crate::diagrams::git_graph::is_valid_editor_reference(candidate)
203 }
204 #[cfg(not(feature = "diagram-architecture"))]
205 Self::ArchitectureIdentifier => false,
206 #[cfg(feature = "diagram-architecture")]
207 Self::ArchitectureIdentifier => {
208 crate::diagrams::architecture::is_valid_editor_identifier(candidate)
209 }
210 #[cfg(not(feature = "diagram-railroad"))]
211 Self::RailroadIrRule => false,
212 #[cfg(feature = "diagram-railroad")]
213 Self::RailroadIrRule => {
214 crate::diagrams::railroad::is_valid_editor_ir_rule_identifier(candidate)
215 }
216 #[cfg(not(feature = "diagram-railroad"))]
217 Self::RailroadEbnfRule => false,
218 #[cfg(feature = "diagram-railroad")]
219 Self::RailroadEbnfRule => {
220 crate::diagrams::railroad::is_valid_editor_ebnf_rule_identifier(candidate)
221 }
222 #[cfg(not(feature = "diagram-railroad"))]
223 Self::RailroadPegRule => false,
224 #[cfg(feature = "diagram-railroad")]
225 Self::RailroadPegRule => {
226 crate::diagrams::railroad::is_valid_editor_peg_rule_identifier(candidate)
227 }
228 #[cfg(not(feature = "diagram-railroad"))]
229 Self::RailroadAbnfRule => false,
230 #[cfg(feature = "diagram-railroad")]
231 Self::RailroadAbnfRule => {
232 crate::diagrams::railroad::is_valid_editor_abnf_rule_identifier(candidate)
233 }
234 #[cfg(not(feature = "diagram-agentflow"))]
235 Self::AgentflowNodeId => false,
236 #[cfg(feature = "diagram-agentflow")]
237 Self::AgentflowNodeId => crate::diagrams::agentflow::is_valid_editor_node_id(candidate),
238 #[cfg(not(feature = "diagram-usecase"))]
239 Self::UsecaseIdentifier => false,
240 #[cfg(feature = "diagram-usecase")]
241 Self::UsecaseIdentifier => {
242 crate::diagrams::usecase::is_valid_editor_identifier(candidate)
243 }
244 }
245 }
246}
247
248fn is_ascii_identifier(value: &str) -> bool {
249 let mut bytes = value.bytes();
250 bytes
251 .next()
252 .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic())
253 && bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric())
254}
255
256impl EditorSemanticRole {
257 pub fn contributes_completion(self) -> bool {
258 matches!(self, Self::Entity | Self::ClassDefinition)
259 }
260
261 pub fn contributes_references(self) -> bool {
262 matches!(self, Self::Entity | Self::Reference)
263 }
264
265 pub fn contributes_outline(self) -> bool {
266 matches!(self, Self::Entity | Self::ClassDefinition | Self::Outline)
267 }
268
269 pub const fn is_class_definition(self) -> bool {
270 matches!(self, Self::ClassDefinition)
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
276#[non_exhaustive]
277pub struct EditorSemanticSymbol {
278 pub name: String,
279 pub detail: Option<String>,
280 pub kind: EditorSemanticKind,
281 pub role: EditorSemanticRole,
282 pub rename_policy: EditorRenamePolicy,
283 pub span: SourceSpan,
284 pub selection: SourceSpan,
285}
286
287impl EditorSemanticSymbol {
288 pub fn new(
289 name: impl Into<String>,
290 detail: Option<String>,
291 kind: EditorSemanticKind,
292 span: SourceSpan,
293 selection: SourceSpan,
294 ) -> Self {
295 Self::with_role(
296 name,
297 detail,
298 kind,
299 EditorSemanticRole::Entity,
300 span,
301 selection,
302 )
303 }
304
305 pub fn outline(
306 name: impl Into<String>,
307 detail: Option<String>,
308 kind: EditorSemanticKind,
309 span: SourceSpan,
310 selection: SourceSpan,
311 ) -> Self {
312 Self::with_role(
313 name,
314 detail,
315 kind,
316 EditorSemanticRole::Outline,
317 span,
318 selection,
319 )
320 }
321
322 pub fn class_definition(
323 name: impl Into<String>,
324 detail: Option<String>,
325 kind: EditorSemanticKind,
326 span: SourceSpan,
327 selection: SourceSpan,
328 ) -> Self {
329 Self::with_role(
330 name,
331 detail,
332 kind,
333 EditorSemanticRole::ClassDefinition,
334 span,
335 selection,
336 )
337 }
338
339 pub fn reference(
340 name: impl Into<String>,
341 detail: Option<String>,
342 kind: EditorSemanticKind,
343 span: SourceSpan,
344 selection: SourceSpan,
345 ) -> Self {
346 Self::with_role(
347 name,
348 detail,
349 kind,
350 EditorSemanticRole::Reference,
351 span,
352 selection,
353 )
354 }
355
356 pub fn payload(
357 name: impl Into<String>,
358 detail: Option<String>,
359 kind: EditorSemanticKind,
360 span: SourceSpan,
361 selection: SourceSpan,
362 ) -> Self {
363 Self::with_role(
364 name,
365 detail,
366 kind,
367 EditorSemanticRole::Payload,
368 span,
369 selection,
370 )
371 }
372
373 pub fn with_role(
374 name: impl Into<String>,
375 detail: Option<String>,
376 kind: EditorSemanticKind,
377 role: EditorSemanticRole,
378 span: SourceSpan,
379 selection: SourceSpan,
380 ) -> Self {
381 let rename_policy = if matches!(
382 role,
383 EditorSemanticRole::Entity | EditorSemanticRole::Reference
384 ) {
385 EditorRenamePolicy::Identifier
386 } else {
387 EditorRenamePolicy::None
388 };
389 Self {
390 name: name.into(),
391 detail,
392 kind,
393 role,
394 rename_policy,
395 span,
396 selection,
397 }
398 }
399
400 pub fn with_rename_policy(mut self, rename_policy: EditorRenamePolicy) -> Self {
401 self.rename_policy = rename_policy;
402 self
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
408pub struct EditorSemanticDiagnostic {
409 pub message: String,
410 pub span: Option<SourceSpan>,
411 pub kind: EditorSemanticDiagnosticKind,
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum EditorSemanticDiagnosticKind {
416 ParserRecovery,
417 ParserWarning,
418}
419
420impl EditorSemanticDiagnostic {
421 pub fn new(message: impl Into<String>, span: Option<SourceSpan>) -> Self {
422 Self {
423 message: message.into(),
424 span,
425 kind: EditorSemanticDiagnosticKind::ParserWarning,
426 }
427 }
428
429 pub fn parser_recovery(message: impl Into<String>, span: Option<SourceSpan>) -> Self {
430 Self {
431 message: message.into(),
432 span,
433 kind: EditorSemanticDiagnosticKind::ParserRecovery,
434 }
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
440pub enum EditorExpectedSyntaxKind {
441 Directive,
442 Frontmatter,
443 IdList,
444 NodeIdentifier,
445 ClassName,
446 FlowchartOperator,
447 ShapeValue,
448 ShapeTrigger,
449 FlowchartDirectionValue,
450 CardinalDirectionValue,
451 BlockDirectionValue,
452 StyleValue,
453 InteractionAction,
454 Payload,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub struct EditorExpectedSyntax {
460 pub kind: EditorExpectedSyntaxKind,
461 pub span: SourceSpan,
462}
463
464impl EditorExpectedSyntax {
465 pub fn new(kind: EditorExpectedSyntaxKind, span: SourceSpan) -> Self {
466 Self { kind, span }
467 }
468}
469
470#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
472pub enum EditorSemanticCompleteness {
473 #[default]
474 Complete,
475 Recovered,
476}
477
478#[derive(Debug, Clone, Default, PartialEq, Eq)]
480#[non_exhaustive]
481pub struct EditorSemanticFacts {
482 pub completeness: EditorSemanticCompleteness,
483 pub family_semantics: EditorFamilySemantics,
484 pub symbols: Vec<EditorSemanticSymbol>,
485 pub directive_prefixes: Vec<String>,
486 pub diagnostics: Vec<EditorSemanticDiagnostic>,
487 pub expected_syntax: Vec<EditorExpectedSyntax>,
488}
489
490impl EditorSemanticFacts {
491 pub fn new() -> Self {
492 Self::default()
493 }
494
495 pub fn push_symbol(&mut self, symbol: EditorSemanticSymbol) {
496 self.symbols.push(symbol);
497 }
498
499 pub fn mark_recovered(&mut self) {
500 self.completeness = EditorSemanticCompleteness::Recovered;
501 }
502
503 pub fn mark_recovered_with_diagnostic(
504 &mut self,
505 message: impl Into<String>,
506 span: Option<SourceSpan>,
507 ) {
508 self.mark_recovered();
509 self.push_diagnostic(message, span);
510 }
511
512 pub fn mark_recovered_from_parse_error(
513 &mut self,
514 message: impl Into<String>,
515 span: Option<SourceSpan>,
516 ) {
517 self.mark_recovered();
518 self.diagnostics
519 .push(EditorSemanticDiagnostic::parser_recovery(message, span));
520 }
521
522 pub fn push_diagnostic(&mut self, message: impl Into<String>, span: Option<SourceSpan>) {
523 self.diagnostics
524 .push(EditorSemanticDiagnostic::new(message, span));
525 }
526
527 pub fn push_directive_prefix(&mut self, prefix: impl Into<String>) {
528 let prefix = prefix.into();
529 if !self.directive_prefixes.contains(&prefix) {
530 self.directive_prefixes.push(prefix);
531 }
532 }
533
534 pub fn push_expected_syntax(&mut self, expected: EditorExpectedSyntax) {
535 self.expected_syntax.push(expected);
536 }
537
538 pub(crate) fn finalize_expected_syntax_controlled(
539 &mut self,
540 control: &OperationControl,
541 ) -> OperationControlResult<()> {
542 let mut seen = BTreeSet::new();
543 let mut unique = Vec::with_capacity(self.expected_syntax.len());
544 for (index, expected) in std::mem::take(&mut self.expected_syntax)
545 .into_iter()
546 .enumerate()
547 {
548 if index.is_multiple_of(128) {
549 control.checkpoint()?;
550 }
551 if seen.insert((expected.kind, expected.span.start, expected.span.end)) {
552 unique.push(expected);
553 }
554 }
555 self.expected_syntax = unique;
556 control.checkpoint()
557 }
558}
559
560#[allow(
561 dead_code,
562 reason = "Shared parser facilities have different consumers in each family selection."
563)]
564pub(crate) fn editor_keyword_value_span(
565 source: &str,
566 statement_start: usize,
567 statement_end: usize,
568 keyword: &str,
569) -> Option<SourceSpan> {
570 let raw = source.get(statement_start..statement_end)?;
571 let trimmed = raw.trim_start();
572 let leading = raw.len().saturating_sub(trimmed.len());
573 let keyword_source = trimmed.get(..keyword.len())?;
574 if !keyword_source.eq_ignore_ascii_case(keyword) {
575 return None;
576 }
577 let after_keyword = trimmed.get(keyword.len()..)?;
578 if after_keyword
579 .chars()
580 .next()
581 .is_some_and(|ch| !ch.is_whitespace())
582 {
583 return None;
584 }
585 let whitespace = after_keyword
586 .chars()
587 .take_while(|ch| ch.is_whitespace())
588 .map(char::len_utf8)
589 .sum::<usize>();
590 let value_start = statement_start + leading + keyword.len() + whitespace;
591 let value_len = after_keyword[whitespace..]
592 .chars()
593 .take_while(|ch| !ch.is_whitespace())
594 .map(char::len_utf8)
595 .sum::<usize>();
596 Some(SourceSpan::new(value_start, value_start + value_len))
597}
598
599#[cfg(any(feature = "diagram-packet", feature = "diagram-radar"))]
600pub(crate) fn editor_recovery_fallback_span(source: &str) -> SourceSpan {
601 let mut line_start = 0;
602 for segment in source.split_inclusive('\n') {
603 let line = segment.strip_suffix('\n').unwrap_or(segment);
604 let line = line.strip_suffix('\r').unwrap_or(line);
605 let trimmed = line.trim();
606 if !trimmed.is_empty() {
607 let start = line_start + line.find(trimmed).unwrap_or_default();
608 return SourceSpan::new(start, start + trimmed.len());
609 }
610 line_start += segment.len();
611 }
612 SourceSpan::new(source.len(), source.len())
613}
614
615#[cfg(any(feature = "diagram-packet", feature = "diagram-radar"))]
616pub(crate) fn ensure_editor_recovery_from_error(
617 mut facts: EditorSemanticFacts,
618 error: &Error,
619 fallback_span: SourceSpan,
620) -> EditorSemanticFacts {
621 let (message, span) = match error {
622 Error::DiagramParse { diagnostic, .. } => (
623 diagnostic.message().to_string(),
624 diagnostic.span().unwrap_or(fallback_span),
625 ),
626 other => (other.to_string(), fallback_span),
627 };
628 let already_reported = facts.diagnostics.iter().any(|diagnostic| {
629 diagnostic.kind == EditorSemanticDiagnosticKind::ParserRecovery
630 && diagnostic.message == message
631 && diagnostic.span == Some(span)
632 });
633 if already_reported {
634 facts.mark_recovered();
635 return facts;
636 }
637
638 facts.mark_recovered_from_parse_error(message, Some(span));
639 facts
640}
641
642#[cfg(any(
643 feature = "diagram-class",
644 feature = "diagram-er",
645 feature = "diagram-flowchart",
646 feature = "diagram-swimlane",
647 feature = "diagram-sequence",
648 feature = "diagram-state"
649))]
650pub(crate) fn lalrpop_recovery_span<T, E>(
651 error: &lalrpop_util::ParseError<usize, T, E>,
652 fallback_offset: usize,
653) -> SourceSpan {
654 match error {
655 lalrpop_util::ParseError::InvalidToken { location } => {
656 SourceSpan::new(*location, *location)
657 }
658 lalrpop_util::ParseError::UnrecognizedEof { location, .. } => {
659 SourceSpan::new(*location, *location)
660 }
661 lalrpop_util::ParseError::UnrecognizedToken { token, .. }
662 | lalrpop_util::ParseError::ExtraToken { token } => SourceSpan::new(token.0, token.2),
663 lalrpop_util::ParseError::User { .. } => SourceSpan::new(fallback_offset, fallback_offset),
664 }
665}
666
667#[cfg(any(
668 feature = "diagram-class",
669 feature = "diagram-er",
670 feature = "diagram-flowchart",
671 feature = "diagram-swimlane",
672 feature = "diagram-sequence",
673 feature = "diagram-state"
674))]
675pub(crate) fn lalrpop_parse_diagnostic<T, E>(
676 error: &lalrpop_util::ParseError<usize, T, E>,
677 fallback_offset: usize,
678) -> ParseDiagnostic
679where
680 T: std::fmt::Debug,
681 E: std::fmt::Display + ParseErrorSourceSpan,
682{
683 let message = format_lalrpop_parse_error(error);
684 match error {
685 lalrpop_util::ParseError::InvalidToken { location }
686 | lalrpop_util::ParseError::UnrecognizedEof { location, .. } => {
687 ParseDiagnostic::new(message).with_span(
688 SourceSpan::new(*location, *location),
689 ParseDiagnosticSpanKind::InsertionPoint,
690 )
691 }
692 lalrpop_util::ParseError::UnrecognizedToken { token, .. }
693 | lalrpop_util::ParseError::ExtraToken { token } => ParseDiagnostic::new(message)
694 .with_span(
695 SourceSpan::new(token.0, token.2),
696 ParseDiagnosticSpanKind::Exact,
697 ),
698 lalrpop_util::ParseError::User { error } => {
699 if let Some(span) = error.source_span() {
700 ParseDiagnostic::new(message).with_span(span, ParseDiagnosticSpanKind::Exact)
701 } else {
702 ParseDiagnostic::new(message).with_span(
703 SourceSpan::new(fallback_offset, fallback_offset),
704 ParseDiagnosticSpanKind::Fallback,
705 )
706 }
707 }
708 }
709}
710
711#[cfg(any(
712 feature = "diagram-class",
713 feature = "diagram-er",
714 feature = "diagram-flowchart",
715 feature = "diagram-swimlane",
716 feature = "diagram-sequence",
717 feature = "diagram-state"
718))]
719pub(crate) fn format_lalrpop_parse_error<T, E>(
720 error: &lalrpop_util::ParseError<usize, T, E>,
721) -> String
722where
723 T: std::fmt::Debug,
724 E: std::fmt::Display,
725{
726 match error {
727 lalrpop_util::ParseError::InvalidToken { .. } => "unexpected token".to_string(),
728 lalrpop_util::ParseError::UnrecognizedEof { expected, .. } => {
729 let expected = format_expected_tokens(expected);
730 if expected.is_empty() {
731 "unexpected end of input".to_string()
732 } else {
733 format!("unexpected end of input; expected {expected}")
734 }
735 }
736 lalrpop_util::ParseError::UnrecognizedToken { token, expected } => {
737 let expected = format_expected_tokens(expected);
738 let found = format_found_token(&token.1);
739 if expected.is_empty() {
740 format!("unexpected {found}")
741 } else {
742 format!("unexpected {found}; expected {expected}")
743 }
744 }
745 lalrpop_util::ParseError::ExtraToken { token } => {
746 format!("unexpected extra {}", format_found_token(&token.1))
747 }
748 lalrpop_util::ParseError::User { error } => error.to_string(),
749 }
750}
751
752#[cfg(any(
753 feature = "diagram-class",
754 feature = "diagram-er",
755 feature = "diagram-flowchart",
756 feature = "diagram-swimlane",
757 feature = "diagram-sequence",
758 feature = "diagram-state"
759))]
760fn format_expected_tokens(expected: &[String]) -> String {
761 expected
762 .iter()
763 .map(|token| humanize_expected_token(token))
764 .collect::<Vec<_>>()
765 .join(", ")
766}
767
768#[cfg(any(
769 feature = "diagram-class",
770 feature = "diagram-er",
771 feature = "diagram-flowchart",
772 feature = "diagram-swimlane",
773 feature = "diagram-sequence",
774 feature = "diagram-state"
775))]
776fn humanize_expected_token(token: &str) -> String {
777 match token {
778 "Id" => "node identifier".to_string(),
779 "EdgeLabel" => "edge label".to_string(),
780 "Direction" => "diagram direction".to_string(),
781 "AlphaNumToken" => "identifier".to_string(),
782 "Text" | "NoteText" | "Descr" | "RestOfLine" => "text".to_string(),
783 "StringLit" => "string literal".to_string(),
784 other => humanize_token_name(other),
785 }
786}
787
788#[cfg(any(
789 feature = "diagram-class",
790 feature = "diagram-er",
791 feature = "diagram-flowchart",
792 feature = "diagram-swimlane",
793 feature = "diagram-sequence",
794 feature = "diagram-state"
795))]
796fn format_found_token<T>(token: &T) -> String
797where
798 T: std::fmt::Debug,
799{
800 let debug = format!("{token:?}");
801 let variant = debug
802 .split_once('(')
803 .map(|(name, _)| name)
804 .unwrap_or(debug.as_str());
805
806 match variant {
807 "Sep" | "Newline" => "statement separator".to_string(),
808 "StyleSep" => "style separator".to_string(),
809 "Amp" => "`&`".to_string(),
810 "Comma" => "`,`".to_string(),
811 "Plus" => "`+`".to_string(),
812 "Minus" => "`-`".to_string(),
813 "Arrow" => "edge operator".to_string(),
814 "SignalType" => "message operator".to_string(),
815 "Id" | "Actor" | "StyledId" => "identifier".to_string(),
816 "Direction" | "DirectionStmt" => "diagram direction".to_string(),
817 "EdgeLabel" | "Text" | "NoteText" | "Descr" | "RestOfLine" => "text".to_string(),
818 "NodeLabel" | "StateDescr" | "CompositState" => "node label".to_string(),
819 "StringLit" => "string literal".to_string(),
820 "Num" => "number".to_string(),
821 other => humanize_token_name(other),
822 }
823}
824
825#[cfg(any(
826 feature = "diagram-class",
827 feature = "diagram-er",
828 feature = "diagram-flowchart",
829 feature = "diagram-swimlane",
830 feature = "diagram-sequence",
831 feature = "diagram-state"
832))]
833fn humanize_token_name(token: &str) -> String {
834 let token = token.strip_prefix("Kw").unwrap_or(token);
835 let mut out = String::new();
836 let mut previous_is_lowercase = false;
837
838 for ch in token.chars() {
839 if ch == '_' || ch == '-' {
840 if !out.ends_with(' ') {
841 out.push(' ');
842 }
843 previous_is_lowercase = false;
844 continue;
845 }
846
847 if ch.is_ascii_uppercase() && previous_is_lowercase && !out.ends_with(' ') {
848 out.push(' ');
849 }
850
851 if ch.is_ascii_digit() && !out.ends_with(' ') && !out.is_empty() {
852 out.push(' ');
853 }
854
855 out.push(ch.to_ascii_lowercase());
856 previous_is_lowercase = ch.is_ascii_lowercase();
857 }
858
859 out.trim().to_string()
860}
861
862#[cfg(test)]
863mod tests {
864 #[cfg(any(
865 feature = "diagram-class",
866 feature = "diagram-er",
867 feature = "diagram-flowchart",
868 feature = "diagram-swimlane",
869 feature = "diagram-sequence",
870 feature = "diagram-state"
871 ))]
872 use super::lalrpop_parse_diagnostic;
873 use super::{EditorRenamePolicy, EditorSemanticKind, EditorSemanticRole, EditorSemanticSymbol};
874 #[cfg(any(
875 feature = "diagram-class",
876 feature = "diagram-er",
877 feature = "diagram-flowchart",
878 feature = "diagram-swimlane",
879 feature = "diagram-sequence",
880 feature = "diagram-state"
881 ))]
882 use crate::ParseDiagnosticSpanKind;
883
884 #[test]
885 #[cfg(all(
886 feature = "diagram-flowchart",
887 feature = "diagram-git-graph",
888 feature = "diagram-architecture",
889 feature = "diagram-railroad"
890 ))]
891 fn rename_policies_follow_family_identifier_grammars() {
892 assert!(EditorRenamePolicy::FlowchartNodeId.accepts("foo.bar"));
893 assert!(EditorRenamePolicy::FlowchartNodeId.accepts("foo-bar"));
894 assert!(!EditorRenamePolicy::FlowchartNodeId.accepts("foo--bar"));
895 assert!(!EditorRenamePolicy::FlowchartNodeId.accepts("foo.->bar"));
896 assert!(!EditorRenamePolicy::FlowchartNodeId.accepts("end.foo"));
897 assert!(!EditorRenamePolicy::FlowchartNodeId.accepts("graph.foo"));
898 assert!(!EditorRenamePolicy::FlowchartNodeId.accepts("subgraph.foo"));
899
900 assert!(EditorRenamePolicy::GitGraphReference.accepts("release/v1.2"));
901 assert!(EditorRenamePolicy::GitGraphReference.accepts("release-"));
902 assert!(EditorRenamePolicy::GitGraphReference.accepts("_private"));
903 assert!(!EditorRenamePolicy::GitGraphReference.accepts("release/"));
904 assert!(!EditorRenamePolicy::GitGraphReference.accepts("release branch"));
905
906 assert!(EditorRenamePolicy::ArchitectureIdentifier.accepts("rowspan"));
907 assert!(EditorRenamePolicy::ArchitectureIdentifier.accepts("service-2"));
908 assert!(!EditorRenamePolicy::ArchitectureIdentifier.accepts("service-"));
909 assert!(!EditorRenamePolicy::ArchitectureIdentifier.accepts("align"));
910
911 assert!(EditorRenamePolicy::RailroadIrRule.accepts("rule_name"));
912 assert!(!EditorRenamePolicy::RailroadIrRule.accepts("1rule"));
913 assert!(!EditorRenamePolicy::RailroadIrRule.accepts("terminal"));
914 assert!(EditorRenamePolicy::RailroadEbnfRule.accepts("terminal"));
915 assert!(EditorRenamePolicy::RailroadPegRule.accepts("terminal"));
916 assert!(EditorRenamePolicy::RailroadAbnfRule.accepts("rule-name"));
917 assert!(!EditorRenamePolicy::RailroadAbnfRule.accepts("rule_name"));
918 }
919
920 #[test]
921 #[cfg(feature = "diagram-agentflow")]
922 fn agentflow_rename_policy_uses_its_own_keyword_boundaries() {
923 for candidate in ["flow", "connector", "global", "flow-guide", "global注文"] {
924 assert!(
925 !EditorRenamePolicy::AgentflowNodeId.accepts(candidate),
926 "{candidate}"
927 );
928 }
929 for candidate in ["flow_user", "flowUser", "Connector", "friend-end"] {
930 assert!(
931 EditorRenamePolicy::AgentflowNodeId.accepts(candidate),
932 "{candidate}"
933 );
934 }
935 }
936
937 #[test]
938 #[cfg(feature = "diagram-usecase")]
939 fn usecase_rename_policy_rejects_statement_only_keywords() {
940 for candidate in [
941 "package",
942 "PACKAGE",
943 "rectangle",
944 "allowmixing",
945 "newpage",
946 "skinparam",
947 ] {
948 assert!(
949 !EditorRenamePolicy::UsecaseIdentifier.accepts(candidate),
950 "{candidate}"
951 );
952 }
953 for candidate in ["package_user", "packageUser", "1User"] {
954 assert!(
955 EditorRenamePolicy::UsecaseIdentifier.accepts(candidate),
956 "{candidate}"
957 );
958 }
959 }
960
961 #[test]
962 fn generated_rename_policy_ids_match_serde_exactly() {
963 assert_eq!(
964 EditorRenamePolicy::ALL.map(EditorRenamePolicy::as_str),
965 EditorRenamePolicy::IDS
966 );
967 assert_eq!(
968 EditorRenamePolicy::default(),
969 EditorRenamePolicy::Identifier
970 );
971
972 for (policy, id) in EditorRenamePolicy::ALL
973 .into_iter()
974 .zip(EditorRenamePolicy::IDS)
975 {
976 assert_eq!(serde_json::to_string(&policy).unwrap(), format!("\"{id}\""));
977 assert_eq!(
978 serde_json::from_str::<EditorRenamePolicy>(&format!("\"{id}\""))
979 .expect("generated rename policy id must deserialize"),
980 policy
981 );
982 }
983 assert!(serde_json::from_str::<EditorRenamePolicy>("\"unknown\"").is_err());
984 }
985
986 #[test]
987 fn class_definition_role_is_typed_completion_without_reference_identity() {
988 let role = EditorSemanticRole::ClassDefinition;
989 assert!(role.contributes_completion());
990 assert!(!role.contributes_references());
991 assert!(role.contributes_outline());
992 assert!(role.is_class_definition());
993
994 let span = crate::SourceSpan::new(0, 3);
995 let symbol = EditorSemanticSymbol::class_definition(
996 "hot",
997 Some("display text may change".to_string()),
998 EditorSemanticKind::Class,
999 span,
1000 span,
1001 );
1002 assert_eq!(symbol.role, role);
1003 assert_eq!(symbol.rename_policy, EditorRenamePolicy::None);
1004 }
1005
1006 #[test]
1007 fn reference_role_is_navigation_only() {
1008 let role = EditorSemanticRole::Reference;
1009 assert!(!role.contributes_completion());
1010 assert!(role.contributes_references());
1011 assert!(!role.contributes_outline());
1012 assert!(!role.is_class_definition());
1013
1014 let span = crate::SourceSpan::new(0, 3);
1015 let symbol = EditorSemanticSymbol::reference(
1016 "ref",
1017 Some("display text may change".to_string()),
1018 EditorSemanticKind::Class,
1019 span,
1020 span,
1021 );
1022 assert_eq!(symbol.role, role);
1023 assert_eq!(symbol.rename_policy, EditorRenamePolicy::Identifier);
1024 }
1025
1026 #[test]
1027 #[cfg(any(
1028 feature = "diagram-class",
1029 feature = "diagram-er",
1030 feature = "diagram-flowchart",
1031 feature = "diagram-swimlane",
1032 feature = "diagram-sequence",
1033 feature = "diagram-state"
1034 ))]
1035 fn lalrpop_parse_diagnostic_preserves_unrecognized_token_span() {
1036 let error = lalrpop_util::ParseError::<usize, &str, String>::UnrecognizedToken {
1037 token: (3, "bad", 6),
1038 expected: vec!["ID".to_string()],
1039 };
1040
1041 let diagnostic = lalrpop_parse_diagnostic(&error, 10);
1042
1043 let span = diagnostic.span().expect("diagnostic span");
1044 assert_eq!(span.start, 3);
1045 assert_eq!(span.end, 6);
1046 assert_eq!(diagnostic.span_kind(), ParseDiagnosticSpanKind::Exact);
1047 assert!(diagnostic.message().contains("\"bad\""));
1048 }
1049
1050 #[test]
1051 #[cfg(any(
1052 feature = "diagram-class",
1053 feature = "diagram-er",
1054 feature = "diagram-flowchart",
1055 feature = "diagram-swimlane",
1056 feature = "diagram-sequence",
1057 feature = "diagram-state"
1058 ))]
1059 fn lalrpop_parse_diagnostic_preserves_eof_insertion_point() {
1060 let error = lalrpop_util::ParseError::<usize, &str, String>::UnrecognizedEof {
1061 location: 12,
1062 expected: vec!["]".to_string()],
1063 };
1064
1065 let diagnostic = lalrpop_parse_diagnostic(&error, 99);
1066
1067 let span = diagnostic.span().expect("diagnostic span");
1068 assert_eq!(span.start, 12);
1069 assert_eq!(span.end, 12);
1070 assert_eq!(
1071 diagnostic.span_kind(),
1072 ParseDiagnosticSpanKind::InsertionPoint
1073 );
1074 assert!(diagnostic.message().contains("unexpected end of input"));
1075 }
1076
1077 #[test]
1078 #[cfg(any(
1079 feature = "diagram-class",
1080 feature = "diagram-er",
1081 feature = "diagram-flowchart",
1082 feature = "diagram-swimlane",
1083 feature = "diagram-sequence",
1084 feature = "diagram-state"
1085 ))]
1086 fn lalrpop_parse_diagnostic_marks_user_errors_as_fallback() {
1087 let error = lalrpop_util::ParseError::<usize, &str, String>::User {
1088 error: "custom parse failure".to_string(),
1089 };
1090
1091 let diagnostic = lalrpop_parse_diagnostic(&error, 8);
1092
1093 let span = diagnostic.span().expect("diagnostic span");
1094 assert_eq!(span.start, 8);
1095 assert_eq!(span.end, 8);
1096 assert_eq!(diagnostic.span_kind(), ParseDiagnosticSpanKind::Fallback);
1097 assert_eq!(diagnostic.message(), "custom parse failure");
1098 }
1099}