Skip to main content

adotenv/
edit.rs

1use alloc::{
2    borrow::Cow,
3    string::{String, ToString},
4    vec,
5    vec::Vec,
6};
7use core::{error::Error, fmt, ops::Range};
8
9use crate::{encode::encode_native, AssignmentState, Dialect, Document, ItemKind, Quote, Severity};
10
11/// A native byte-range replacement against the original document snapshot.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TextEdit {
14    /// Original UTF-8 byte range to replace.
15    pub range: Range<usize>,
16    /// Replacement text.
17    pub replacement: String,
18}
19
20/// An edit batch whose complete output has already been parsed and verified.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct VerifiedEdits {
23    edits: Vec<TextEdit>,
24    output: String,
25}
26impl VerifiedEdits {
27    /// Replacements in ascending, non-overlapping source order.
28    #[must_use]
29    pub fn edits(&self) -> &[TextEdit] {
30        &self.edits
31    }
32    /// Consume the result, retaining the native replacements.
33    #[must_use]
34    pub fn into_edits(self) -> Vec<TextEdit> {
35        self.edits
36    }
37    /// Consume the result, retaining the verified complete text.
38    #[must_use]
39    pub fn into_string(self) -> String {
40        self.output
41    }
42    /// The verified complete text.
43    #[must_use]
44    pub fn as_str(&self) -> &str {
45        &self.output
46    }
47}
48
49/// A position for a new declaration in the original snapshot.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum InsertPosition<'a> {
53    /// After the last source item.
54    End,
55    /// After a uniquely named declaration.
56    AfterKey(&'a str),
57    /// After an explicitly selected declaration occurrence.
58    AfterEntry(usize),
59    /// Before the first lexically greater key, or at the end.
60    Sorted,
61}
62
63/// An operation whose identities refer to the original document snapshot.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum Edit<'a> {
67    /// Assign an exact literal value to one declaration.
68    Set {
69        /// The snapshot-local declaration identity.
70        entry: usize,
71        /// The exact new literal value.
72        value: &'a str,
73    },
74    /// Remove exactly one source item, without claiming neighboring comments.
75    Remove {
76        /// The snapshot-local source item identity.
77        item: usize,
78    },
79    /// Rename one declaration.
80    Rename {
81        /// The snapshot-local declaration identity.
82        entry: usize,
83        /// The new literal key name.
84        key: &'a str,
85    },
86    /// Insert an assigned or bare declaration.
87    Insert {
88        /// The new literal key name.
89        key: &'a str,
90        /// An assigned literal value, or `None` for a bare declaration.
91        value: Option<&'a str>,
92        /// Placement in the original source snapshot.
93        position: InsertPosition<'a>,
94    },
95    /// Prefix every physical line of one item with a comment marker.
96    Comment {
97        /// The snapshot-local source item identity.
98        item: usize,
99    },
100    /// Remove one comment marker (and one optional following space).
101    Uncomment {
102        /// The snapshot-local comment item identity.
103        item: usize,
104    },
105    /// Enable or disable a declaration's export prefix.
106    Export {
107        /// The snapshot-local declaration identity.
108        entry: usize,
109        /// Whether the declaration should carry `export`.
110        enabled: bool,
111    },
112}
113
114/// Why a native edit could not be produced safely.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum EditError {
118    /// No valid declaration has the requested key.
119    Missing,
120    /// A key-only selector matched this many declarations.
121    Duplicate(usize),
122    /// A snapshot-local identity or item kind is unsuitable.
123    InvalidTarget,
124    /// The requested name cannot be represented by the selected key grammar.
125    InvalidKey,
126    /// The value cannot be represented in its selected dialect and source context.
127    Unencodable,
128    /// Operations overlap or assign the same property more than once.
129    ConflictingEdits,
130    /// The complete output would reinterpret unselected source or miss the request.
131    WouldChangeMeaning,
132}
133impl fmt::Display for EditError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            Self::Missing => f.write_str("no declaration has the requested key"),
137            Self::Duplicate(count) => {
138                write!(f, "key has {count} declarations; select an occurrence")
139            }
140            Self::InvalidTarget => f.write_str("invalid snapshot-local edit target"),
141            Self::InvalidKey => f.write_str("key cannot be represented by the selected dialect"),
142            Self::Unencodable => f.write_str("value cannot be represented in the selected dialect"),
143            Self::ConflictingEdits => f.write_str("conflicting operations in edit batch"),
144            Self::WouldChangeMeaning => {
145                f.write_str("edit would change unrelated source interpretation")
146            }
147        }
148    }
149}
150impl Error for EditError {}
151
152fn key_token(key: &str, dialect: Dialect) -> Option<Cow<'_, str>> {
153    if crate::parser::valid_key_in(key, dialect) && !(dialect.quoted_keys && key.starts_with('\''))
154    {
155        Some(Cow::Borrowed(key))
156    } else if dialect.quoted_keys && !key.is_empty() && !key.contains('\'') {
157        let mut token = String::with_capacity(key.len() + 2);
158        token.push('\'');
159        token.push_str(key);
160        token.push('\'');
161        Some(Cow::Owned(token))
162    } else {
163        None
164    }
165}
166
167// Build an index only when a batch performs repeated name lookups. Single-key
168// edits retain their allocation-free lookup path. Sorted insertion means the
169// earliest source occurrence greater than the requested key, not a key successor.
170struct KeyLookup<'doc> {
171    document: &'doc Document<'doc>,
172    entries: Vec<crate::Entry<'doc>>,
173    minimum_starts_rev: Vec<usize>,
174}
175impl<'doc> KeyLookup<'doc> {
176    fn new(document: &'doc Document<'doc>, operations: &[Edit<'_>]) -> Self {
177        let lookups = operations
178            .iter()
179            .copied()
180            .filter(|operation| match *operation {
181                Edit::Rename { entry, key } => document
182                    .entry(entry)
183                    .is_some_and(|entry| entry.key() != key),
184                Edit::Insert {
185                    position: InsertPosition::Sorted | InsertPosition::AfterKey(_),
186                    ..
187                } => true,
188                _ => false,
189            })
190            .count();
191        let mut entries: Vec<_> = if lookups > 1 {
192            document.entries().collect()
193        } else {
194            Vec::new()
195        };
196        entries.sort_unstable_by(|a, b| a.key().cmp(b.key()));
197        let sorted_inserts = operations.iter().any(|operation| {
198            matches!(
199                *operation,
200                Edit::Insert {
201                    position: InsertPosition::Sorted,
202                    ..
203                }
204            )
205        });
206        let mut minimum_starts_rev = Vec::new();
207        if sorted_inserts && !entries.is_empty() {
208            minimum_starts_rev.reserve(entries.len());
209            let mut minimum = document.source().len();
210            for entry in entries.iter().rev() {
211                minimum = minimum.min(entry.line_span().start);
212                minimum_starts_rev.push(minimum);
213            }
214        }
215        Self {
216            document,
217            entries,
218            minimum_starts_rev,
219        }
220    }
221
222    fn bounds(&self, key: &str) -> Range<usize> {
223        self.entries.partition_point(|entry| entry.key() < key)
224            ..self.entries.partition_point(|entry| entry.key() <= key)
225    }
226
227    fn count(&self, key: &str) -> usize {
228        if self.entries.is_empty() {
229            self.document.get_all(key).count()
230        } else {
231            self.bounds(key).len()
232        }
233    }
234
235    fn unique(&self, key: &str) -> Result<crate::Entry<'doc>, EditError> {
236        if self.entries.is_empty() {
237            return self.document.unique(key);
238        }
239        let matches = self.bounds(key);
240        match matches.len() {
241            0 => Err(EditError::Missing),
242            1 => Ok(self.entries[matches.start]),
243            count => Err(EditError::Duplicate(count)),
244        }
245    }
246
247    fn first_greater(&self, key: &str) -> usize {
248        if self.entries.is_empty() {
249            return self
250                .document
251                .entries()
252                .find(|entry| entry.key() > key)
253                .map_or(self.document.source().len(), |entry| {
254                    entry.line_span().start
255                });
256        }
257        let start = self.entries.partition_point(|entry| entry.key() <= key);
258        if start == self.entries.len() {
259            self.document.source().len()
260        } else {
261            self.minimum_starts_rev[self.entries.len() - start - 1]
262        }
263    }
264}
265
266#[derive(Clone, Copy)]
267struct Desired<'a> {
268    key: &'a str,
269    value: Option<&'a str>,
270    quote: Quote,
271    exported: bool,
272}
273impl<'a> Desired<'a> {
274    fn from_entry(entry: crate::Entry<'a>) -> Self {
275        Self {
276            key: entry.key(),
277            value: entry.value(),
278            quote: entry.quote(),
279            exported: entry.exported(),
280        }
281    }
282    fn matches(self, entry: crate::Entry<'_>) -> bool {
283        self.key == entry.key()
284            && self.value == entry.value()
285            && self.quote == entry.quote()
286            && self.exported == entry.exported()
287            && entry.state()
288                == match self.value {
289                    None => AssignmentState::Bare,
290                    Some("") => AssignmentState::Empty,
291                    Some(_) => AssignmentState::Value,
292                }
293    }
294}
295#[derive(Clone, Copy, PartialEq, Eq)]
296enum Whole {
297    Keep,
298    Remove,
299    Comment,
300    Uncomment,
301}
302struct ItemPlan<'a> {
303    whole: Whole,
304    fields: u8,
305    desired: Option<Desired<'a>>,
306}
307impl ItemPlan<'_> {
308    fn retains(&self, item: crate::Item<'_>) -> bool {
309        match self.whole {
310            Whole::Remove => false,
311            Whole::Uncomment => {
312                item.raw().len()
313                    > 1 + usize::from(
314                        item.comment_text()
315                            .is_some_and(|body| body.starts_with(' ')),
316                    )
317            }
318            _ => true,
319        }
320    }
321}
322struct SetTarget<'a> {
323    entry: usize,
324    value: &'a str,
325    range: Range<usize>,
326    context: Range<usize>,
327}
328
329#[derive(Clone, Copy)]
330struct ContextEdits<'a, 'value> {
331    base: &'a [TextEdit],
332    targets: &'a [SetTarget<'value>],
333    sets: &'a [Option<TextEdit>],
334}
335
336fn edit_key(edit: &TextEdit) -> (usize, usize) {
337    (edit.range.start, edit.range.end)
338}
339
340struct Insertion<'a> {
341    offset: usize,
342    desired: Desired<'a>,
343    key_token: Cow<'a, str>,
344    leading: String,
345    sorted: bool,
346}
347impl<'a> Insertion<'a> {
348    fn append_to(
349        &mut self,
350        text: &mut String,
351        dialect: Dialect,
352        ending: &str,
353    ) -> Result<(Range<usize>, Desired<'a>), EditError> {
354        let start = text.len();
355        text.push_str(&self.key_token);
356        if let Some(value) = self.desired.value {
357            let (token, quote) = encode_native(
358                value,
359                Quote::None,
360                dialect,
361                &self.key_token,
362                ending,
363                true,
364                false,
365            )
366            .map_err(|_| EditError::Unencodable)?;
367            text.push('=');
368            text.push_str(&token);
369            self.desired.quote = quote;
370        }
371        text.push_str(ending);
372        Ok((start..text.len(), self.desired))
373    }
374}
375enum Expect<'a> {
376    Original(usize),
377    Entry(Desired<'a>),
378    Comments,
379    Uncommented,
380}
381struct Region<'a> {
382    range: Range<usize>,
383    expect: Expect<'a>,
384}
385fn verify_entry_region<'a>(
386    first: crate::Item<'a>,
387    desired: Desired<'_>,
388    range: &Range<usize>,
389    actual: &mut impl Iterator<Item = crate::Item<'a>>,
390) -> Result<(), EditError> {
391    if first.span().end > range.end
392        || first
393            .diagnostic()
394            .is_some_and(|d| d.severity() == Severity::Error)
395        || !first.entry().is_some_and(|entry| desired.matches(entry))
396    {
397        return Err(EditError::WouldChangeMeaning);
398    }
399    // A new quote form can expose the selected entry's own whitespace/comment
400    // suffix as separate items (Compose). Never consume another declaration.
401    let mut end = first.span().end;
402    while end < range.end {
403        let next = actual.next().ok_or(EditError::WouldChangeMeaning)?;
404        if next.span().start != end
405            || next.span().end > range.end
406            || !matches!(next.kind(), ItemKind::Blank | ItemKind::Comment)
407            || next
408                .diagnostic()
409                .is_some_and(|d| d.severity() == Severity::Error)
410        {
411            return Err(EditError::WouldChangeMeaning);
412        }
413        end = next.span().end;
414    }
415    Ok(())
416}
417struct InsertGroup<'a> {
418    offset: usize,
419    len: usize,
420    entries: Vec<(Range<usize>, Desired<'a>)>,
421}
422fn group_at<'a, 'b>(groups: &'a [InsertGroup<'b>], offset: usize) -> Option<&'a InsertGroup<'b>> {
423    groups
424        .binary_search_by_key(&offset, |group| group.offset)
425        .ok()
426        .map(|index| &groups[index])
427}
428
429fn claim(plan: &mut ItemPlan<'_>, field: u8) -> Result<(), EditError> {
430    if plan.whole != Whole::Keep || plan.fields & field != 0 {
431        return Err(EditError::ConflictingEdits);
432    }
433    plan.fields |= field;
434    Ok(())
435}
436fn whole(plan: &mut ItemPlan<'_>, action: Whole) -> Result<(), EditError> {
437    if plan.whole != Whole::Keep || plan.fields != 0 {
438        return Err(EditError::ConflictingEdits);
439    }
440    plan.whole = action;
441    Ok(())
442}
443fn comment_text(text: &str, breaks: crate::LineBreaks) -> String {
444    let mut out = String::with_capacity(text.len() + 2);
445    let mut start = 0;
446    for (index, byte) in text.bytes().enumerate() {
447        if byte == b'\n'
448            || (breaks == crate::LineBreaks::Universal
449                && byte == b'\r'
450                && text.as_bytes().get(index + 1) != Some(&b'\n'))
451        {
452            out.push_str("# ");
453            out.push_str(&text[start..=index]);
454            start = index + 1;
455        }
456    }
457    if start < text.len() {
458        out.push_str("# ");
459        out.push_str(&text[start..]);
460    }
461    out
462}
463
464// Cumulative replacement lengths let every original item boundary be mapped
465// without rescanning the whole batch for each item.
466struct Boundaries<'a> {
467    edits: &'a [TextEdit],
468    removed: Vec<usize>,
469    added: Vec<usize>,
470}
471impl<'a> Boundaries<'a> {
472    fn new(edits: &'a [TextEdit]) -> Self {
473        let mut removed = vec![0];
474        let mut added = vec![0];
475        for edit in edits {
476            removed.push(removed.last().copied().unwrap_or(0) + edit.range.len());
477            added.push(added.last().copied().unwrap_or(0) + edit.replacement.len());
478        }
479        Self {
480            edits,
481            removed,
482            added,
483        }
484    }
485    fn map(&self, offset: usize) -> usize {
486        let count = self.edits.partition_point(|edit| {
487            edit.range.end < offset || (edit.range.end == offset && !edit.range.is_empty())
488        });
489        offset - self.removed[count] + self.added[count]
490    }
491}
492
493impl Document<'_> {
494    /// Plan and verify an entire snapshot-local batch atomically.
495    ///
496    /// # Errors
497    /// Invalid selectors, conflicting operations and unintended reparsing are rejected.
498    pub fn edit(&self, operations: &[Edit<'_>]) -> Result<VerifiedEdits, EditError> {
499        let keys = KeyLookup::new(self, operations);
500        let mut plans: Vec<_> = self
501            .items()
502            .map(|item| ItemPlan {
503                whole: Whole::Keep,
504                fields: 0,
505                desired: item.entry().map(Desired::from_entry),
506            })
507            .collect();
508        let mut edits = Vec::new();
509        let mut insertions = Vec::new();
510        let mut end_insertions = vec![0; self.items.len()];
511        for operation in operations {
512            self.plan_operation(*operation, &mut plans, &mut edits, &mut insertions, &keys)?;
513        }
514        self.validate_renames(&plans)?;
515        insertions.sort_by_key(|insertion| insertion.offset);
516        self.finish_sets(
517            operations,
518            &mut plans,
519            &mut edits,
520            &insertions,
521            &mut end_insertions,
522        )?;
523        let (groups, ending) =
524            self.plan_insertions(insertions, &plans, &mut edits, &mut end_insertions)?;
525        let edits = Self::normalize_edits(edits)?;
526        let output = Self::apply_edits(self.source(), &edits);
527        let regions = self.expected_regions(&plans, &end_insertions, &edits, groups)?;
528        self.verify_regions(&output, regions, ending)?;
529        Ok(VerifiedEdits { edits, output })
530    }
531
532    fn plan_operation<'a>(
533        &'a self,
534        operation: Edit<'a>,
535        plans: &mut [ItemPlan<'a>],
536        edits: &mut Vec<TextEdit>,
537        insertions: &mut Vec<Insertion<'a>>,
538        keys: &KeyLookup<'a>,
539    ) -> Result<(), EditError> {
540        match operation {
541            Edit::Set { entry, value } => {
542                self.plan_set(entry, value, plans)?;
543            }
544            Edit::Rename { entry, key } => self.plan_rename(entry, key, plans, edits, keys)?,
545            Edit::Export { entry, enabled } => self.plan_export(entry, enabled, plans, edits)?,
546            Edit::Remove { item } => {
547                let source_item = self.item(item).ok_or(EditError::InvalidTarget)?;
548                whole(&mut plans[item], Whole::Remove)?;
549                edits.push(TextEdit {
550                    range: source_item.span(),
551                    replacement: String::new(),
552                });
553            }
554            Edit::Comment { item } => {
555                let source_item = self.item(item).ok_or(EditError::InvalidTarget)?;
556                whole(&mut plans[item], Whole::Comment)?;
557                edits.push(TextEdit {
558                    range: source_item.span(),
559                    replacement: comment_text(source_item.raw(), self.dialect().line_breaks),
560                });
561            }
562            Edit::Uncomment { item } => {
563                let source_item = self.item(item).ok_or(EditError::InvalidTarget)?;
564                if source_item.kind() != ItemKind::Comment {
565                    return Err(EditError::InvalidTarget);
566                }
567                whole(&mut plans[item], Whole::Uncomment)?;
568                let span = source_item.span();
569                let hash = source_item
570                    .raw()
571                    .find('#')
572                    .ok_or(EditError::InvalidTarget)?
573                    + span.start;
574                let end =
575                    hash + 1 + usize::from(self.source().as_bytes().get(hash + 1) == Some(&b' '));
576                edits.push(TextEdit {
577                    range: hash..end,
578                    replacement: String::new(),
579                });
580            }
581            Edit::Insert {
582                key,
583                value,
584                position,
585            } => self.plan_insertion(key, value, position, insertions, keys)?,
586        }
587        Ok(())
588    }
589
590    fn plan_set<'a>(
591        &'a self,
592        id: usize,
593        value: &'a str,
594        plans: &mut [ItemPlan<'a>],
595    ) -> Result<(), EditError> {
596        let entry = self.entry(id).ok_or(EditError::InvalidTarget)?;
597        let plan = &mut plans[entry.item_id()];
598        claim(plan, 1)?;
599        if let Some(desired) = &mut plan.desired {
600            desired.value = Some(value);
601        }
602        Ok(())
603    }
604
605    fn finish_sets<'a>(
606        &'a self,
607        operations: &[Edit<'a>],
608        plans: &mut [ItemPlan<'a>],
609        edits: &mut Vec<TextEdit>,
610        insertions: &[Insertion<'_>],
611        end_insertions: &mut [usize],
612    ) -> Result<(), EditError> {
613        // Header and whole-item edits are complete before choosing value
614        // spellings. Sets on different physical capped lines are independent;
615        // retry only within one line when a neighboring edit frees its budget.
616        edits.sort_by_key(edit_key);
617        let mut targets = Vec::new();
618        for operation in operations {
619            let Edit::Set { entry: id, value } = *operation else {
620                continue;
621            };
622            let entry = self.entry(id).ok_or(EditError::InvalidTarget)?;
623            let span = entry.line_span();
624            let range = entry
625                .value_span()
626                .unwrap_or_else(|| entry.tokens().key.end..entry.tokens().key.end);
627            let context = self.value_context(&span, &range);
628            targets.push(SetTarget {
629                entry: id,
630                value,
631                range,
632                context,
633            });
634        }
635        targets.sort_by_key(|target| (target.range.start, target.range.end));
636        let mut set_edits: Vec<Option<TextEdit>> = core::iter::repeat_with(|| None)
637            .take(targets.len())
638            .collect();
639        let mut ending_items = vec![None; targets.len()];
640        let mut group_start = 0;
641        while group_start < targets.len() {
642            let mut group_end = group_start + 1;
643            while group_end < targets.len()
644                && targets[group_end].context == targets[group_start].context
645            {
646                group_end += 1;
647            }
648            let mut remaining = group_end - group_start;
649            while remaining != 0 {
650                let mut progressed = false;
651                for index in group_start..group_end {
652                    if set_edits[index].is_some() {
653                        continue;
654                    }
655                    let context_edits = ContextEdits {
656                        base: edits,
657                        targets: &targets,
658                        sets: &set_edits,
659                    };
660                    match self.finish_set(&targets[index], plans, context_edits, insertions) {
661                        Ok((edit, item, ending_field)) => {
662                            set_edits[index] = Some(edit);
663                            ending_items[index] = ending_field.then_some(item);
664                            remaining -= 1;
665                            progressed = true;
666                        }
667                        Err(EditError::Unencodable) => {}
668                        Err(error) => return Err(error),
669                    }
670                }
671                if !progressed {
672                    return Err(EditError::Unencodable);
673                }
674            }
675            group_start = group_end;
676        }
677        Self::merge_set_edits(edits, set_edits, ending_items, end_insertions)
678    }
679
680    fn merge_set_edits(
681        edits: &mut Vec<TextEdit>,
682        set_edits: Vec<Option<TextEdit>>,
683        ending_items: Vec<Option<usize>>,
684        end_insertions: &mut [usize],
685    ) -> Result<(), EditError> {
686        if set_edits.iter().any(Option::is_none) {
687            return Err(EditError::Unencodable);
688        }
689        let base = core::mem::take(edits);
690        edits.reserve(base.len() + set_edits.len());
691        let mut base = base.into_iter().peekable();
692        let mut sets = set_edits
693            .into_iter()
694            .zip(ending_items)
695            .filter_map(|(edit, item)| edit.map(|edit| (edit, item)))
696            .peekable();
697        while base.peek().is_some() || sets.peek().is_some() {
698            let take_base = match (base.peek(), sets.peek()) {
699                (Some(left), Some((right, _))) => edit_key(left) <= edit_key(right),
700                (Some(_), None) => true,
701                (None, Some(_)) => false,
702                (None, None) => break,
703            };
704            if take_base {
705                if let Some(edit) = base.next() {
706                    edits.push(edit);
707                }
708            } else if let Some((edit, ending_item)) = sets.next() {
709                edits.push(edit);
710                if let Some(item) = ending_item {
711                    end_insertions[item] = edits.len();
712                }
713            }
714        }
715        Ok(())
716    }
717
718    fn finish_set<'a>(
719        &'a self,
720        target: &SetTarget<'a>,
721        plans: &mut [ItemPlan<'a>],
722        edits: ContextEdits<'_, 'a>,
723        insertions: &[Insertion<'_>],
724    ) -> Result<(TextEdit, usize, bool), EditError> {
725        let entry = self.entry(target.entry).ok_or(EditError::InvalidTarget)?;
726        let item = self.item(entry.item_id()).ok_or(EditError::InvalidTarget)?;
727        let span = entry.line_span();
728        let range = target.range.clone();
729        let context = target.context.clone();
730        let before = self.edited_context(context.start..range.start, edits);
731        let suffix = &self.source()[range.end..span.end];
732        let separate_comment = entry
733            .tokens()
734            .inline_comment
735            .as_ref()
736            .is_some_and(|comment| comment.start == range.end);
737        let encode = |ending: &str, preferred| {
738            let after = if ending.is_empty() {
739                self.edited_context(range.end..context.end, edits)
740            } else if suffix.is_empty() {
741                Cow::Borrowed(ending)
742            } else {
743                let mut combined = String::with_capacity(suffix.len() + ending.len());
744                combined.push_str(suffix);
745                combined.push_str(ending);
746                Cow::Owned(combined)
747            };
748            encode_native(
749                target.value,
750                preferred,
751                self.dialect(),
752                &before,
753                &after,
754                entry.value_span().is_none(),
755                separate_comment,
756            )
757            .map_err(|_| EditError::Unencodable)
758        };
759        let original_quote = plans[item.id()]
760            .desired
761            .ok_or(EditError::InvalidTarget)?
762            .quote;
763        let mut preferred = original_quote;
764        let mut ending = self.following_separator(item, insertions, plans);
765        let mut chosen = None;
766        for _ in 0..4 {
767            let (candidate, actual_quote) = encode(ending, preferred)?;
768            plans[item.id()]
769                .desired
770                .as_mut()
771                .ok_or(EditError::InvalidTarget)?
772                .quote = actual_quote;
773            let actual_ending = self.following_separator(item, insertions, plans);
774            plans[item.id()]
775                .desired
776                .as_mut()
777                .ok_or(EditError::InvalidTarget)?
778                .quote = original_quote;
779            if actual_ending == ending {
780                chosen = Some((candidate, actual_quote));
781                break;
782            }
783            preferred = actual_quote;
784            ending = actual_ending;
785        }
786        let (mut token, quote) = chosen.ok_or(EditError::Unencodable)?;
787        if separate_comment {
788            token.push(' ');
789        }
790        if entry.value_span().is_none() {
791            token.insert(0, '=');
792        }
793        plans[item.id()]
794            .desired
795            .as_mut()
796            .ok_or(EditError::InvalidTarget)?
797            .quote = quote;
798        let ending_field = range.is_empty() && range.start == span.end;
799        Ok((
800            TextEdit {
801                range,
802                replacement: token,
803            },
804            item.id(),
805            ending_field,
806        ))
807    }
808
809    fn value_context(&self, item: &Range<usize>, value: &Range<usize>) -> Range<usize> {
810        if self.dialect().line_byte_limit.is_none() {
811            return item.clone();
812        }
813        let breaks = |c| {
814            c == '\n' || (c == '\r' && self.dialect().line_breaks == crate::LineBreaks::Universal)
815        };
816        let start = self.source()[..item.start]
817            .rfind(breaks)
818            .map_or(0, |at| at + 1);
819        let end = self.source()[value.end..]
820            .find(breaks)
821            .map_or(self.source().len(), |at| {
822                let at = value.end + at;
823                at + if self.source().as_bytes()[at] == b'\r'
824                    && self.source().as_bytes().get(at + 1) == Some(&b'\n')
825                {
826                    2
827                } else {
828                    1
829                }
830            });
831        start..end
832    }
833
834    fn edited_context<'a>(
835        &'a self,
836        range: Range<usize>,
837        edits: ContextEdits<'_, '_>,
838    ) -> Cow<'a, str> {
839        let base_start = edits.base.partition_point(|edit| {
840            edit.range.end < range.start
841                || (edit.range.end == range.start && !edit.range.is_empty())
842        });
843        let base_end = edits
844            .base
845            .partition_point(|edit| edit.range.start < range.end);
846        let set_start = edits.targets.partition_point(|target| {
847            target.range.end < range.start
848                || (target.range.end == range.start && !target.range.is_empty())
849        });
850        let set_end = edits
851            .targets
852            .partition_point(|target| target.range.start < range.end);
853        let mut base = edits.base[base_start..base_end].iter().peekable();
854        let mut sets = edits.sets[set_start..set_end]
855            .iter()
856            .filter_map(Option::as_ref)
857            .peekable();
858        if base.peek().is_none() && sets.peek().is_none() {
859            return Cow::Borrowed(&self.source()[range]);
860        }
861        let mut output = String::with_capacity(range.len());
862        let mut at = range.start;
863        while base.peek().is_some() || sets.peek().is_some() {
864            let take_base = match (base.peek(), sets.peek()) {
865                (Some(left), Some(right)) => edit_key(left) <= edit_key(right),
866                (Some(_), None) => true,
867                (None, Some(_)) => false,
868                (None, None) => break,
869            };
870            let edit = if take_base { base.next() } else { sets.next() };
871            let Some(edit) = edit else {
872                break;
873            };
874            output.push_str(&self.source()[at..edit.range.start]);
875            output.push_str(&edit.replacement);
876            at = edit.range.end;
877        }
878        output.push_str(&self.source()[at..range.end]);
879        Cow::Owned(output)
880    }
881
882    fn following_separator(
883        &self,
884        item: crate::Item<'_>,
885        insertions: &[Insertion<'_>],
886        plans: &[ItemPlan<'_>],
887    ) -> &'static str {
888        let next = insertions.partition_point(|insertion| insertion.offset < item.span().end);
889        let Some(insertion) = insertions.get(next) else {
890            return "";
891        };
892        let intervening = self
893            .items()
894            .skip(item.id() + 1)
895            .find(|next| plans[next.id()].retains(*next));
896        if intervening.is_some_and(|next| next.span().end <= insertion.offset) {
897            return "";
898        }
899        self.insertion_separator(Some(item), self.preferred_ending(), plans)
900    }
901
902    fn plan_rename<'a>(
903        &'a self,
904        id: usize,
905        key: &'a str,
906        plans: &mut [ItemPlan<'a>],
907        edits: &mut Vec<TextEdit>,
908        keys: &KeyLookup<'a>,
909    ) -> Result<(), EditError> {
910        let entry = self.entry(id).ok_or(EditError::InvalidTarget)?;
911        if key != entry.key() {
912            let existing = keys.count(key);
913            if existing != 0 {
914                return Err(EditError::Duplicate(existing + 1));
915            }
916        }
917        let plan = &mut plans[entry.item_id()];
918        claim(plan, 2)?;
919        let (range, replacement) =
920            if entry.key_span() != entry.tokens().key && !key.is_empty() && !key.contains('\'') {
921                (entry.key_span(), key.to_string())
922            } else {
923                (
924                    entry.tokens().key.clone(),
925                    key_token(key, self.dialect())
926                        .ok_or(EditError::InvalidKey)?
927                        .into_owned(),
928                )
929            };
930        edits.push(TextEdit { range, replacement });
931        if let Some(desired) = &mut plan.desired {
932            desired.key = key;
933        }
934        Ok(())
935    }
936
937    fn plan_export(
938        &self,
939        id: usize,
940        enabled: bool,
941        plans: &mut [ItemPlan<'_>],
942        edits: &mut Vec<TextEdit>,
943    ) -> Result<(), EditError> {
944        let entry = self.entry(id).ok_or(EditError::InvalidTarget)?;
945        let plan = &mut plans[entry.item_id()];
946        claim(plan, 4)?;
947        if enabled != entry.exported() {
948            let (range, replacement) = if enabled {
949                let start = entry.tokens().key.start;
950                (start..start, "export ".to_string())
951            } else {
952                let export = entry
953                    .tokens()
954                    .export
955                    .as_ref()
956                    .ok_or(EditError::InvalidTarget)?;
957                (
958                    export.start..entry.tokens().space_after_export.end,
959                    String::new(),
960                )
961            };
962            edits.push(TextEdit { range, replacement });
963        }
964        if let Some(desired) = &mut plan.desired {
965            desired.exported = enabled;
966        }
967        Ok(())
968    }
969
970    fn plan_insertion<'a>(
971        &self,
972        key: &'a str,
973        value: Option<&'a str>,
974        position: InsertPosition<'_>,
975        insertions: &mut Vec<Insertion<'a>>,
976        keys: &KeyLookup<'_>,
977    ) -> Result<(), EditError> {
978        let key_token = key_token(key, self.dialect()).ok_or(EditError::InvalidKey)?;
979        let offset = match position {
980            InsertPosition::End => self.source().len(),
981            InsertPosition::AfterKey(key) => keys.unique(key)?.line_span().end,
982            InsertPosition::AfterEntry(id) => {
983                self.entry(id)
984                    .ok_or(EditError::InvalidTarget)?
985                    .line_span()
986                    .end
987            }
988            InsertPosition::Sorted => keys.first_greater(key),
989        };
990        insertions.push(Insertion {
991            offset,
992            desired: Desired {
993                key,
994                value,
995                quote: Quote::None,
996                exported: false,
997            },
998            key_token,
999            leading: String::new(),
1000            sorted: matches!(position, InsertPosition::Sorted),
1001        });
1002        Ok(())
1003    }
1004
1005    fn validate_renames(&self, plans: &[ItemPlan<'_>]) -> Result<(), EditError> {
1006        // Original-name collisions are checked while planning. Only two newly
1007        // renamed declarations can now collide, so index just their new names.
1008        let mut renamed = plans.iter().enumerate().filter_map(|(index, plan)| {
1009            if plan.fields & 2 == 0 {
1010                return None;
1011            }
1012            let desired = plan.desired?;
1013            let original = self.item(index)?.entry()?;
1014            (original.key() != desired.key).then_some(desired.key)
1015        });
1016        let Some(first) = renamed.next() else {
1017            return Ok(());
1018        };
1019        let Some(second) = renamed.next() else {
1020            return Ok(());
1021        };
1022        let mut keys = Vec::from([first, second]);
1023        keys.extend(renamed);
1024        keys.sort_unstable();
1025        if keys.windows(2).any(|pair| pair[0] == pair[1]) {
1026            return Err(EditError::WouldChangeMeaning);
1027        }
1028        Ok(())
1029    }
1030
1031    fn last_retained_item(&self, plans: &[ItemPlan<'_>]) -> Option<crate::Item<'_>> {
1032        let mut retained = self
1033            .items()
1034            .rev()
1035            .filter(|item| plans[item.id()].retains(*item));
1036        let last = retained.next()?;
1037        // A selected comment can become the output's BOM prelude. A prelude
1038        // alone is not a physical item and needs no insertion separator.
1039        let becomes_prelude = !self.has_bom()
1040            && plans[last.id()].whole == Whole::Uncomment
1041            && last.line_ending() == crate::LineEnding::None
1042            && last.raw().starts_with('#')
1043            && last
1044                .comment_text()
1045                .is_some_and(|body| body.strip_prefix(' ').unwrap_or(body) == "\u{feff}");
1046        if becomes_prelude && retained.next().is_none() {
1047            None
1048        } else {
1049            Some(last)
1050        }
1051    }
1052    fn insertion_separator(
1053        &self,
1054        previous: Option<crate::Item<'_>>,
1055        ending: &'static str,
1056        plans: &[ItemPlan<'_>],
1057    ) -> &'static str {
1058        previous.map_or("", |item| {
1059            let plan = &plans[item.id()];
1060            let next_statement = self.dialect().line_byte_limit.is_none()
1061                && plan.whole == Whole::Keep
1062                && self.dialect().quoted_tail == crate::QuotedTail::NextStatement
1063                && plan
1064                    .desired
1065                    .is_some_and(|desired| desired.quote != Quote::None);
1066            let needs_separator = (item.line_ending() == crate::LineEnding::None
1067                && !next_statement)
1068                || (self.dialect().line_breaks == crate::LineBreaks::Lf
1069                    && item.line_ending() == crate::LineEnding::Cr);
1070            if needs_separator {
1071                ending
1072            } else {
1073                ""
1074            }
1075        })
1076    }
1077
1078    fn merge_boundary_fields(
1079        insertions: &mut [Insertion<'_>],
1080        edits: &mut Vec<TextEdit>,
1081        end_insertions: &mut [usize],
1082    ) {
1083        // Until this pass, nonzero values identify an ending field's edit by
1084        // one-based index. Convert to byte lengths for region verification.
1085        for ending in end_insertions {
1086            let Some(index) = ending.checked_sub(1) else {
1087                continue;
1088            };
1089            let edit = &mut edits[index];
1090            *ending = edit.replacement.len();
1091            let at = insertions.partition_point(|insert| insert.offset < edit.range.start);
1092            if let Some(insertion) = insertions
1093                .get_mut(at)
1094                .filter(|insert| insert.offset == edit.range.start)
1095            {
1096                insertion.leading = core::mem::take(&mut edit.replacement);
1097            }
1098        }
1099        edits.retain(|edit| !edit.range.is_empty() || !edit.replacement.is_empty());
1100    }
1101
1102    fn plan_insertions<'a>(
1103        &self,
1104        mut insertions: Vec<Insertion<'a>>,
1105        plans: &[ItemPlan<'_>],
1106        edits: &mut Vec<TextEdit>,
1107        end_insertions: &mut [usize],
1108    ) -> Result<(Vec<InsertGroup<'a>>, &'static str), EditError> {
1109        Self::merge_boundary_fields(&mut insertions, edits, end_insertions);
1110        if insertions.is_empty() {
1111            return Ok((Vec::new(), ""));
1112        }
1113        let ending = self.preferred_ending();
1114        let last_retained = self.last_retained_item(plans);
1115        let mut retained = self
1116            .items()
1117            .filter(|item| plans[item.id()].retains(*item))
1118            .peekable();
1119        let mut previous = None;
1120        let mut previous_group_item = None;
1121        let mut groups = Vec::new();
1122        let mut pending = insertions.as_mut_slice();
1123        while !pending.is_empty() {
1124            let offset = pending[0].offset;
1125            let count = pending.partition_point(|insert| insert.offset == offset);
1126            let (same, remaining) = pending.split_at_mut(count);
1127            let mut text = core::mem::take(&mut same[0].leading);
1128            if same.iter().all(|insert| insert.sorted) {
1129                same.sort_by(|a, b| a.desired.key.cmp(b.desired.key));
1130            }
1131            let at_end = offset == self.source().len();
1132            while retained
1133                .peek()
1134                .is_some_and(|item| item.span().end <= offset)
1135            {
1136                previous = retained.next();
1137            }
1138            let final_in_output = remaining.is_empty()
1139                && last_retained.map_or(true, |item| item.span().start < offset);
1140            let prior = if at_end { last_retained } else { previous };
1141            let owner = prior.map(crate::Item::id);
1142            let prefix = if !groups.is_empty() && owner == previous_group_item {
1143                ""
1144            } else {
1145                self.insertion_separator(prior, ending, plans)
1146            };
1147            if let Some(owner) = owner {
1148                end_insertions[owner] += prefix.len();
1149            }
1150            previous_group_item = owner;
1151            text.push_str(prefix);
1152            let mut entries = Vec::with_capacity(same.len());
1153            let count = same.len();
1154            for (index, insertion) in same.iter_mut().enumerate() {
1155                let suffix = if !final_in_output
1156                    || self.has_final_newline()
1157                    || index + 1 != count
1158                    || (self.dialect().syntax == crate::Syntax::Compose
1159                        && insertion.desired.value.is_none())
1160                {
1161                    ending
1162                } else {
1163                    ""
1164                };
1165                entries.push(insertion.append_to(&mut text, self.dialect(), suffix)?);
1166            }
1167            groups.push(InsertGroup {
1168                offset,
1169                len: text.len(),
1170                entries,
1171            });
1172            let insertion = TextEdit {
1173                range: offset..offset,
1174                replacement: text,
1175            };
1176            edits.push(insertion);
1177            pending = remaining;
1178        }
1179        // Ending fields now belong to their insertion groups; groups precede
1180        // any remaining fields belonging to the following item at that offset.
1181        edits.rotate_right(groups.len());
1182        Ok((groups, ending))
1183    }
1184
1185    fn normalize_edits(mut edits: Vec<TextEdit>) -> Result<Vec<TextEdit>, EditError> {
1186        // Empty boundary insertions precede replacements starting at that boundary.
1187        edits.sort_by(|a, b| {
1188            a.range
1189                .start
1190                .cmp(&b.range.start)
1191                .then(a.range.end.cmp(&b.range.end))
1192        });
1193        if edits
1194            .windows(2)
1195            .any(|pair| pair[1].range.start < pair[0].range.end)
1196        {
1197            return Err(EditError::ConflictingEdits);
1198        }
1199        edits.dedup_by(|later, earlier| {
1200            if later.range.is_empty() && earlier.range == later.range {
1201                earlier.replacement.push_str(&later.replacement);
1202                true
1203            } else {
1204                false
1205            }
1206        });
1207        Ok(edits)
1208    }
1209
1210    fn apply_edits(source: &str, edits: &[TextEdit]) -> String {
1211        let removed: usize = edits.iter().map(|edit| edit.range.len()).sum();
1212        let added: usize = edits.iter().map(|edit| edit.replacement.len()).sum();
1213        let mut output = String::with_capacity(source.len() - removed + added);
1214        let mut cursor = 0;
1215        for edit in edits {
1216            output.push_str(&source[cursor..edit.range.start]);
1217            output.push_str(&edit.replacement);
1218            cursor = edit.range.end;
1219        }
1220        output.push_str(&source[cursor..]);
1221        output
1222    }
1223
1224    fn expected_regions<'a>(
1225        &self,
1226        plans: &[ItemPlan<'a>],
1227        end_insertions: &[usize],
1228        edits: &[TextEdit],
1229        groups: Vec<InsertGroup<'a>>,
1230    ) -> Result<Vec<Region<'a>>, EditError> {
1231        let boundaries = Boundaries::new(edits);
1232        let mut regions = Vec::with_capacity(self.items.len() + groups.len());
1233        let mut item = 0;
1234        while item < self.items.len() {
1235            let plan = &plans[item];
1236            if plan.whole == Whole::Remove {
1237                item += 1;
1238                continue;
1239            }
1240            let first = item;
1241            let expect = match plan.whole {
1242                Whole::Uncomment => {
1243                    let mut next = item + 1;
1244                    while next < plans.len()
1245                        && group_at(&groups, self.items[next - 1].span.end).is_none()
1246                    {
1247                        match plans[next].whole {
1248                            Whole::Remove => {}
1249                            Whole::Uncomment => item = next,
1250                            _ => break,
1251                        }
1252                        next += 1;
1253                    }
1254                    Expect::Uncommented
1255                }
1256                Whole::Comment => Expect::Comments,
1257                Whole::Keep if plan.fields != 0 => {
1258                    Expect::Entry(plan.desired.ok_or(EditError::InvalidTarget)?)
1259                }
1260                Whole::Keep => Expect::Original(item),
1261                Whole::Remove => return Err(EditError::InvalidTarget),
1262            };
1263            let end = self.items[item].span.end;
1264            let start = self.items[first].span.start;
1265            let inserted_before = group_at(&groups, start).map_or(0, |group| group.len);
1266            regions.push(Region {
1267                range: boundaries.map(start) + inserted_before
1268                    ..boundaries.map(end) + end_insertions[item],
1269                expect,
1270            });
1271            item += 1;
1272        }
1273        for group in groups {
1274            let start = boundaries.map(group.offset);
1275            for (range, desired) in group.entries {
1276                regions.push(Region {
1277                    range: start + range.start..start + range.end,
1278                    expect: Expect::Entry(desired),
1279                });
1280            }
1281        }
1282        regions.sort_by_key(|region| region.range.start);
1283        Ok(regions)
1284    }
1285
1286    fn verify_regions(
1287        &self,
1288        output: &str,
1289        mut regions: Vec<Region<'_>>,
1290        ending: &str,
1291    ) -> Result<(), EditError> {
1292        let parsed = crate::parser::parse_document(output, self.dialect());
1293        if parsed.has_bom() != self.has_bom() {
1294            // Only an explicitly uncommented leading region may expose its own
1295            // BOM. Removing text before an unrelated BOM must still be refused.
1296            let selected = regions.first_mut().filter(|region| {
1297                !self.has_bom()
1298                    && parsed.has_bom()
1299                    && region.range.start == 0
1300                    && region.range.end >= 3
1301                    && matches!(region.expect, Expect::Uncommented)
1302            });
1303            let Some(region) = selected else {
1304                return Err(EditError::WouldChangeMeaning);
1305            };
1306            region.range.start = 3;
1307        }
1308        let mut actual = parsed.items();
1309        for region in regions {
1310            if region.range.is_empty() && matches!(region.expect, Expect::Uncommented) {
1311                continue;
1312            }
1313            let first = actual.next().ok_or(EditError::WouldChangeMeaning)?;
1314            if first.span().start != region.range.start {
1315                return Err(EditError::WouldChangeMeaning);
1316            }
1317            match region.expect {
1318                Expect::Entry(desired) => {
1319                    verify_entry_region(first, desired, &region.range, &mut actual)?;
1320                }
1321                Expect::Original(id) => {
1322                    let original = self.item(id).ok_or(EditError::InvalidTarget)?;
1323                    if first.span() != region.range || first.kind() != original.kind() {
1324                        return Err(EditError::WouldChangeMeaning);
1325                    }
1326                    let original_text = original.raw();
1327                    let new_text = &output[region.range.clone()];
1328                    if !new_text.starts_with(original_text)
1329                        || (new_text.len() != original_text.len()
1330                            && &new_text[original_text.len()..] != ending)
1331                    {
1332                        return Err(EditError::WouldChangeMeaning);
1333                    }
1334                    if let Some(entry) = original.entry() {
1335                        if !first
1336                            .entry()
1337                            .is_some_and(|after| Desired::from_entry(entry).matches(after))
1338                        {
1339                            return Err(EditError::WouldChangeMeaning);
1340                        }
1341                    }
1342                    match (&self.items[id].invalid, &parsed.items[first.id()].invalid) {
1343                        (None, None) => {}
1344                        (Some(before), Some(after))
1345                            if before.code() == after.code()
1346                                && before.span().start - original.span().start
1347                                    == after.span().start - region.range.start
1348                                && before.span().end - original.span().start
1349                                    == after.span().end - region.range.start => {}
1350                        _ => return Err(EditError::WouldChangeMeaning),
1351                    }
1352                }
1353                Expect::Comments | Expect::Uncommented => {
1354                    let comments_only = matches!(region.expect, Expect::Comments);
1355                    let mut current = first;
1356                    loop {
1357                        if current.span().end > region.range.end
1358                            || current.kind() == ItemKind::Invalid
1359                            || current
1360                                .diagnostic()
1361                                .is_some_and(|diagnostic| diagnostic.severity() == Severity::Error)
1362                            || (comments_only && current.kind() != ItemKind::Comment)
1363                        {
1364                            return Err(EditError::WouldChangeMeaning);
1365                        }
1366                        if current.span().end == region.range.end {
1367                            break;
1368                        }
1369                        let next = actual.next().ok_or(EditError::WouldChangeMeaning)?;
1370                        if next.span().start != current.span().end {
1371                            return Err(EditError::WouldChangeMeaning);
1372                        }
1373                        current = next;
1374                    }
1375                }
1376            }
1377        }
1378        if actual.next().is_some() {
1379            return Err(EditError::WouldChangeMeaning);
1380        }
1381        Ok(())
1382    }
1383
1384    /// Set a uniquely named declaration's exact literal value.
1385    ///
1386    /// # Errors
1387    /// Returns a selector, encoding or verification error.
1388    pub fn set(&self, key: &str, value: &str) -> Result<VerifiedEdits, EditError> {
1389        self.edit(&[Edit::Set {
1390            entry: self.unique(key)?.id(),
1391            value,
1392        }])
1393    }
1394    /// Remove a uniquely named declaration, not its neighboring comments.
1395    ///
1396    /// # Errors
1397    /// Returns a selector or verification error.
1398    pub fn remove(&self, key: &str) -> Result<VerifiedEdits, EditError> {
1399        self.edit(&[Edit::Remove {
1400            item: self.unique(key)?.item_id(),
1401        }])
1402    }
1403    /// Rename a uniquely named declaration.
1404    ///
1405    /// # Errors
1406    /// Returns a selector, key, collision or verification error.
1407    pub fn rename(&self, key: &str, new_key: &str) -> Result<VerifiedEdits, EditError> {
1408        self.edit(&[Edit::Rename {
1409            entry: self.unique(key)?.id(),
1410            key: new_key,
1411        }])
1412    }
1413    /// Insert an assigned or bare declaration.
1414    ///
1415    /// # Errors
1416    /// Returns a selector, key, encoding or verification error.
1417    pub fn insert(
1418        &self,
1419        key: &str,
1420        value: Option<&str>,
1421        position: InsertPosition<'_>,
1422    ) -> Result<VerifiedEdits, EditError> {
1423        self.edit(&[Edit::Insert {
1424            key,
1425            value,
1426            position,
1427        }])
1428    }
1429    /// Comment every physical line of a uniquely named declaration.
1430    ///
1431    /// # Errors
1432    /// Returns a selector or verification error.
1433    pub fn comment(&self, key: &str) -> Result<VerifiedEdits, EditError> {
1434        self.edit(&[Edit::Comment {
1435            item: self.unique(key)?.item_id(),
1436        }])
1437    }
1438    /// Uncomment an explicitly selected comment item.
1439    ///
1440    /// # Errors
1441    /// Returns a target or verification error. Multiline restoration may require a batch.
1442    pub fn uncomment(&self, item: usize) -> Result<VerifiedEdits, EditError> {
1443        self.edit(&[Edit::Uncomment { item }])
1444    }
1445    /// Enable or disable export on a uniquely named declaration.
1446    ///
1447    /// # Errors
1448    /// Returns a selector or verification error.
1449    pub fn set_export(&self, key: &str, enabled: bool) -> Result<VerifiedEdits, EditError> {
1450        self.edit(&[Edit::Export {
1451            entry: self.unique(key)?.id(),
1452            enabled,
1453        }])
1454    }
1455}
1456
1457/// Set a unique key in recovering input, verifying that unrelated items survive.
1458///
1459/// # Errors
1460/// Returns the native selector, encoding or verification error.
1461pub fn set(text: &str, key: &str, value: &str) -> Result<String, EditError> {
1462    crate::parser::parse_document(text, Dialect::PORTABLE)
1463        .set(key, value)
1464        .map(VerifiedEdits::into_string)
1465}
1466/// Remove a unique key in recovering input, without deleting adjacent comments.
1467///
1468/// # Errors
1469/// Returns the native selector or verification error.
1470pub fn remove(text: &str, key: &str) -> Result<String, EditError> {
1471    crate::parser::parse_document(text, Dialect::PORTABLE)
1472        .remove(key)
1473        .map(VerifiedEdits::into_string)
1474}