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