1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use crate::decorators::{DecoratorLevel, DecoratorSet, decorator_record, record_field};
5use crate::language_profile::LanguageProfile;
6use crate::link_network::{Link, LinkId, LinkNetwork, LinkType};
7use crate::query::{LinkQuery, QueryCaptures, QueryMatch, QueryPredicate, QueryPredicateHost};
8use crate::source::{ByteRange, SourceSpan};
9use crate::substitution::{SubstitutionReport, SubstitutionRule, VariableSubstitutionRule};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct ReplacementRule {
14 kind: ReplacementKind,
15}
16
17impl ReplacementRule {
18 #[must_use]
24 pub fn captured_text(capture_name: impl Into<String>, replacement: impl Into<String>) -> Self {
25 Self {
26 kind: ReplacementKind::CapturedText {
27 capture_name: normalize_capture_name(capture_name),
28 replacement: replacement.into(),
29 },
30 }
31 }
32
33 #[must_use]
35 pub const fn substitution(rule: SubstitutionRule) -> Self {
36 Self {
37 kind: ReplacementKind::Substitution(rule),
38 }
39 }
40
41 #[must_use]
43 pub const fn variable_substitution(rule: VariableSubstitutionRule) -> Self {
44 Self {
45 kind: ReplacementKind::VariableSubstitution(rule),
46 }
47 }
48
49 #[must_use]
54 pub fn quasiquote(capture_name: impl Into<String>, template: QuasiquoteTemplate) -> Self {
55 Self {
56 kind: ReplacementKind::Quasiquote {
57 capture_name: normalize_capture_name(capture_name),
58 template,
59 },
60 }
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65enum ReplacementKind {
66 CapturedText {
67 capture_name: String,
68 replacement: String,
69 },
70 Quasiquote {
71 capture_name: String,
72 template: QuasiquoteTemplate,
73 },
74 Substitution(SubstitutionRule),
75 VariableSubstitution(VariableSubstitutionRule),
76}
77
78#[derive(Clone, Debug, Default, PartialEq, Eq)]
80pub struct ReplacementReport {
81 text_replacements: Vec<TextReplacement>,
82 template_errors: Vec<QuasiquoteError>,
83 substitution: SubstitutionReport,
84 profile_diagnostics: Vec<LinkId>,
85}
86
87impl ReplacementReport {
88 pub(crate) const fn from_substitution(substitution: SubstitutionReport) -> Self {
89 Self {
90 text_replacements: Vec::new(),
91 template_errors: Vec::new(),
92 substitution,
93 profile_diagnostics: Vec::new(),
94 }
95 }
96
97 #[must_use]
99 pub fn text_replacements(&self) -> &[TextReplacement] {
100 &self.text_replacements
101 }
102
103 #[must_use]
105 pub fn template_errors(&self) -> &[QuasiquoteError] {
106 &self.template_errors
107 }
108
109 #[must_use]
111 pub const fn substitution(&self) -> &SubstitutionReport {
112 &self.substitution
113 }
114
115 #[must_use]
117 pub fn profile_diagnostics(&self) -> &[LinkId] {
118 &self.profile_diagnostics
119 }
120
121 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.text_replacements.is_empty()
125 && self.template_errors.is_empty()
126 && self.substitution.created().is_empty()
127 && self.substitution.updated().is_empty()
128 && self.substitution.deleted().is_empty()
129 && self.profile_diagnostics.is_empty()
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
135pub struct TextReplacement {
136 capture_name: String,
137 link_id: LinkId,
138 token_ids: Vec<LinkId>,
139 span: Option<SourceSpan>,
140 old_text: String,
141 new_text: String,
142}
143
144impl TextReplacement {
145 fn new(
146 capture_name: &str,
147 link_id: LinkId,
148 token_ids: Vec<LinkId>,
149 span: Option<SourceSpan>,
150 old_text: String,
151 new_text: &str,
152 ) -> Self {
153 Self {
154 capture_name: capture_name.to_string(),
155 link_id,
156 token_ids,
157 span,
158 old_text,
159 new_text: new_text.to_string(),
160 }
161 }
162
163 #[must_use]
165 pub fn capture_name(&self) -> &str {
166 &self.capture_name
167 }
168
169 #[must_use]
171 pub const fn link_id(&self) -> LinkId {
172 self.link_id
173 }
174
175 #[must_use]
177 pub fn token_ids(&self) -> &[LinkId] {
178 &self.token_ids
179 }
180
181 #[must_use]
183 pub const fn span(&self) -> Option<SourceSpan> {
184 self.span
185 }
186
187 #[must_use]
189 pub fn old_text(&self) -> &str {
190 &self.old_text
191 }
192
193 #[must_use]
195 pub fn new_text(&self) -> &str {
196 &self.new_text
197 }
198}
199
200#[derive(Clone, Copy, Debug, Default)]
202pub struct SourceTextPredicateHost;
203
204impl QueryPredicateHost for SourceTextPredicateHost {
205 fn evaluate(
206 &self,
207 predicate: &QueryPredicate,
208 captures: &QueryCaptures,
209 network: &LinkNetwork,
210 ) -> bool {
211 let Some((capture_name, literal)) = capture_literal_arguments(predicate) else {
212 return false;
213 };
214 let Some(captured_text) = captured_text(network, captures.first(capture_name)) else {
215 return false;
216 };
217
218 match predicate.name() {
219 "eq?" => captured_text == literal,
220 "not-eq?" => captured_text != literal,
221 _ => false,
222 }
223 }
224}
225
226#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct QuasiquoteTemplate {
229 parts: Vec<TemplatePart>,
230}
231
232impl QuasiquoteTemplate {
233 pub fn parse(source: impl Into<String>) -> Result<Self, QuasiquoteError> {
235 let source = source.into();
236 let mut parts = Vec::new();
237 let mut rest = source.as_str();
238 while let Some(start) = rest.find("{{") {
239 if start > 0 {
240 parts.push(TemplatePart::Literal(rest[..start].to_string()));
241 }
242 let after_open = &rest[start + 2..];
243 let Some(end) = after_open.find("}}") else {
244 return Err(QuasiquoteError::Parse(
245 "unterminated quasiquote placeholder".to_string(),
246 ));
247 };
248 let name = normalize_capture_name(after_open[..end].trim());
249 if name.is_empty() {
250 return Err(QuasiquoteError::Parse(
251 "quasiquote placeholder is empty".to_string(),
252 ));
253 }
254 parts.push(TemplatePart::Placeholder(name));
255 rest = &after_open[end + 2..];
256 }
257 if !rest.is_empty() {
258 parts.push(TemplatePart::Literal(rest.to_string()));
259 }
260 if parts.is_empty() {
261 parts.push(TemplatePart::Literal(source));
262 }
263 Ok(Self { parts })
264 }
265
266 fn render(
267 &self,
268 network: &LinkNetwork,
269 query_match: &QueryMatch,
270 old_text: &str,
271 ) -> Result<String, QuasiquoteError> {
272 let mut values = BTreeMap::<String, String>::new();
273 for part in &self.parts {
274 if let TemplatePart::Placeholder(name) = part {
275 if values.contains_key(name) {
276 continue;
277 }
278 let Some(text) = captured_text(network, query_match.captures().first(name)) else {
279 return Err(QuasiquoteError::MissingPlaceholder(name.clone()));
280 };
281 values.insert(name.clone(), text);
282 }
283 }
284
285 let mut rendered = String::new();
286 for part in &self.parts {
287 match part {
288 TemplatePart::Literal(literal) => rendered.push_str(literal),
289 TemplatePart::Placeholder(name) => {
290 let Some(value) = values.get(name) else {
291 return Err(QuasiquoteError::MissingPlaceholder(name.clone()));
292 };
293 rendered.push_str(value);
294 }
295 }
296 }
297 Ok(preserve_parentheses(old_text, rendered))
298 }
299}
300
301#[derive(Clone, Debug, PartialEq, Eq)]
302enum TemplatePart {
303 Literal(String),
304 Placeholder(String),
305}
306
307#[derive(Clone, Debug, PartialEq, Eq)]
309pub enum QuasiquoteError {
310 Parse(String),
312 MissingPlaceholder(String),
314}
315
316impl fmt::Display for QuasiquoteError {
317 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
318 match self {
319 Self::Parse(message) => formatter.write_str(message),
320 Self::MissingPlaceholder(name) => {
321 write!(formatter, "quasiquote placeholder `{name}` is not captured")
322 }
323 }
324 }
325}
326
327impl std::error::Error for QuasiquoteError {}
328
329impl LinkNetwork {
330 #[must_use]
336 pub fn find(&self, query: &LinkQuery) -> Vec<QueryMatch> {
337 self.query_matches_with(query, &SourceTextPredicateHost)
338 }
339
340 pub fn replace(&mut self, matches: &[QueryMatch], rule: &ReplacementRule) -> ReplacementReport {
342 self.replace_decorated(matches, rule, &DecoratorSet::empty())
343 }
344
345 pub fn replace_decorated(
350 &mut self,
351 matches: &[QueryMatch],
352 rule: &ReplacementRule,
353 decorators: &DecoratorSet,
354 ) -> ReplacementReport {
355 match &rule.kind {
356 ReplacementKind::CapturedText {
357 capture_name,
358 replacement,
359 } => ReplacementReport {
360 text_replacements: self.replace_captured_text(
361 matches,
362 capture_name,
363 replacement,
364 decorators,
365 ),
366 template_errors: Vec::new(),
367 substitution: SubstitutionReport::default(),
368 profile_diagnostics: Vec::new(),
369 },
370 ReplacementKind::Quasiquote {
371 capture_name,
372 template,
373 } => {
374 let (text_replacements, template_errors) =
375 self.replace_captured_quasiquote(matches, capture_name, template);
376 ReplacementReport {
377 text_replacements,
378 template_errors,
379 substitution: SubstitutionReport::default(),
380 profile_diagnostics: Vec::new(),
381 }
382 }
383 ReplacementKind::Substitution(rule) => {
384 if matches.is_empty() {
385 ReplacementReport::default()
386 } else {
387 ReplacementReport {
388 text_replacements: Vec::new(),
389 template_errors: Vec::new(),
390 substitution: self.apply_substitution(rule),
391 profile_diagnostics: Vec::new(),
392 }
393 }
394 }
395 ReplacementKind::VariableSubstitution(rule) => {
396 if matches.is_empty() {
397 ReplacementReport::default()
398 } else {
399 ReplacementReport {
400 text_replacements: Vec::new(),
401 template_errors: Vec::new(),
402 substitution: self.apply_variable_substitution(rule),
403 profile_diagnostics: Vec::new(),
404 }
405 }
406 }
407 }
408 }
409
410 pub fn replace_with_profile(
417 &mut self,
418 matches: &[QueryMatch],
419 rule: &ReplacementRule,
420 profile: &LanguageProfile,
421 ) -> ReplacementReport {
422 let mut candidate = self.clone();
423 let report = candidate.replace(matches, rule);
424 if report.is_empty() {
425 return report;
426 }
427
428 match profile.validate_transform_result(&candidate) {
429 Ok(()) => {
430 *self = candidate;
431 report
432 }
433 Err(violation) => {
434 let diagnostic = profile.insert_diagnostic(
435 self,
436 &violation,
437 matches.first().map(QueryMatch::link_id),
438 );
439 ReplacementReport {
440 profile_diagnostics: vec![diagnostic],
441 ..ReplacementReport::default()
442 }
443 }
444 }
445 }
446
447 fn replace_captured_text(
448 &mut self,
449 matches: &[QueryMatch],
450 capture_name: &str,
451 replacement: &str,
452 decorators: &DecoratorSet,
453 ) -> Vec<TextReplacement> {
454 let mut touched_tokens = BTreeSet::new();
455 let mut replacements = Vec::new();
456
457 for query_match in matches {
458 for capture in query_match
459 .captures()
460 .iter()
461 .filter(|capture| capture.name() == capture_name)
462 {
463 let token_ids = source_token_ids(self, capture.link_id());
464 if token_ids.is_empty()
465 || token_ids
466 .iter()
467 .any(|token_id| touched_tokens.contains(token_id))
468 {
469 continue;
470 }
471
472 let old_text = text_for_tokens(self, &token_ids);
473 let record = decorator_record([
474 ("capture", capture_name),
475 ("old", old_text.as_str()),
476 ("new", replacement),
477 ]);
478 let Some(decorated) = decorators.decorate(DecoratorLevel::Transformation, &record)
479 else {
480 continue;
481 };
482 let replacement = record_field(&decorated, "new");
483 if old_text == replacement {
484 continue;
485 }
486
487 let span = span_for_tokens(self, &token_ids);
488 let first_token = token_ids[0];
489 if !self.set_term(first_token, replacement.to_string()) {
490 continue;
491 }
492 for token_id in token_ids.iter().skip(1) {
493 let _ = self.set_term(*token_id, String::new());
494 }
495
496 touched_tokens.extend(token_ids.iter().copied());
497 replacements.push(TextReplacement::new(
498 capture_name,
499 capture.link_id(),
500 token_ids,
501 span,
502 old_text,
503 replacement,
504 ));
505 }
506 }
507
508 replacements
509 }
510
511 fn replace_captured_quasiquote(
512 &mut self,
513 matches: &[QueryMatch],
514 capture_name: &str,
515 template: &QuasiquoteTemplate,
516 ) -> (Vec<TextReplacement>, Vec<QuasiquoteError>) {
517 let mut touched_tokens = BTreeSet::new();
518 let mut replacements = Vec::new();
519 let mut errors = Vec::new();
520
521 for query_match in matches {
522 for capture in query_match
523 .captures()
524 .iter()
525 .filter(|capture| capture.name() == capture_name)
526 {
527 let token_ids = source_token_ids(self, capture.link_id());
528 if token_ids.is_empty()
529 || token_ids
530 .iter()
531 .any(|token_id| touched_tokens.contains(token_id))
532 {
533 continue;
534 }
535
536 let old_text = text_for_tokens(self, &token_ids);
537 let replacement = match template.render(self, query_match, &old_text) {
538 Ok(replacement) => replacement,
539 Err(error) => {
540 errors.push(error);
541 continue;
542 }
543 };
544 if old_text == replacement {
545 continue;
546 }
547
548 let span = span_for_tokens(self, &token_ids);
549 let first_token = token_ids[0];
550 if !self.set_term(first_token, replacement.clone()) {
551 continue;
552 }
553 for token_id in token_ids.iter().skip(1) {
554 let _ = self.set_term(*token_id, String::new());
555 }
556
557 touched_tokens.extend(token_ids.iter().copied());
558 replacements.push(TextReplacement::new(
559 capture_name,
560 capture.link_id(),
561 token_ids,
562 span,
563 old_text,
564 &replacement,
565 ));
566 }
567 }
568
569 (replacements, errors)
570 }
571}
572
573fn normalize_capture_name(name: impl Into<String>) -> String {
574 name.into().trim_start_matches('@').to_string()
575}
576
577fn preserve_parentheses(old_text: &str, rendered: String) -> String {
578 let trimmed_old = old_text.trim();
579 let trimmed_rendered = rendered.trim();
580 if trimmed_old.starts_with('(')
581 && trimmed_old.ends_with(')')
582 && !(trimmed_rendered.starts_with('(') && trimmed_rendered.ends_with(')'))
583 {
584 format!("({rendered})")
585 } else {
586 rendered
587 }
588}
589
590fn capture_literal_arguments(predicate: &QueryPredicate) -> Option<(&str, &str)> {
591 let [capture_argument, literal_argument] = predicate.arguments() else {
592 return None;
593 };
594 Some((
595 capture_argument.capture_name()?,
596 literal_argument.literal()?,
597 ))
598}
599
600fn captured_text(network: &LinkNetwork, link_id: Option<LinkId>) -> Option<String> {
601 let link_id = link_id?;
602 let token_ids = source_token_ids(network, link_id);
603 if token_ids.is_empty() {
604 network
605 .link(link_id)
606 .and_then(|link| link.metadata().term())
607 .map(str::to_string)
608 } else {
609 Some(text_for_tokens(network, &token_ids))
610 }
611}
612
613fn source_token_ids(network: &LinkNetwork, link_id: LinkId) -> Vec<LinkId> {
614 let mut visited = BTreeSet::new();
615 let mut token_ids = Vec::new();
616 collect_source_tokens(network, link_id, &mut visited, &mut token_ids);
617 token_ids.sort_by_key(|token_id| token_sort_key(network, *token_id));
618 token_ids.dedup();
619 token_ids
620}
621
622fn collect_source_tokens(
623 network: &LinkNetwork,
624 link_id: LinkId,
625 visited: &mut BTreeSet<LinkId>,
626 token_ids: &mut Vec<LinkId>,
627) {
628 if !visited.insert(link_id) {
629 return;
630 }
631 let Some(link) = network.link(link_id) else {
632 return;
633 };
634
635 match link.metadata().link_type() {
636 Some(LinkType::Token) => {
637 if !link.metadata().flags().is_missing() {
638 token_ids.push(link_id);
639 }
640 return;
641 }
642 Some(LinkType::Field | LinkType::Trivia) => return,
643 _ => {}
644 }
645
646 let children = network
647 .links()
648 .filter(|candidate| candidate.references().first().copied() == Some(link_id))
649 .map(Link::id)
650 .collect::<Vec<_>>();
651 for child in children {
652 collect_source_tokens(network, child, visited, token_ids);
653 }
654}
655
656fn token_sort_key(network: &LinkNetwork, token_id: LinkId) -> (usize, u64) {
657 let start = network
658 .link(token_id)
659 .and_then(|link| link.metadata().span())
660 .map_or(usize::MAX, |span| span.byte_range().start());
661 (start, token_id.as_u64())
662}
663
664fn text_for_tokens(network: &LinkNetwork, token_ids: &[LinkId]) -> String {
665 token_ids
666 .iter()
667 .filter_map(|token_id| network.link(*token_id))
668 .filter_map(|link| link.metadata().term())
669 .collect()
670}
671
672fn span_for_tokens(network: &LinkNetwork, token_ids: &[LinkId]) -> Option<SourceSpan> {
673 let spans = token_ids
674 .iter()
675 .filter_map(|token_id| network.link(*token_id))
676 .filter_map(|link| link.metadata().span())
677 .collect::<Vec<_>>();
678 let first = spans.first()?;
679 let last = spans.last()?;
680 Some(SourceSpan::new(
681 ByteRange::new(first.byte_range().start(), last.byte_range().end()),
682 first.start_point(),
683 last.end_point(),
684 ))
685}