Skip to main content

deb822_lossless/
lossless.rs

1//! Parser for deb822 style files.
2//!
3//! This parser can be used to parse files in the deb822 format, while preserving
4//! all whitespace and comments. It is based on the [rowan] library, which is a
5//! lossless parser library for Rust.
6//!
7//! Once parsed, the file can be traversed or modified, and then written back to
8//! a file.
9//!
10//! # Example
11//!
12//! ```rust
13//! use deb822_lossless::Deb822;
14//! use std::str::FromStr;
15//!
16//! let input = r###"Package: deb822-lossless
17//! ## Comments are preserved
18//! Maintainer: Jelmer Vernooij <jelmer@debian.org>
19//! Homepage: https://github.com/jelmer/deb822-lossless
20//! Section: rust
21//!
22//! Package: deb822-lossless
23//! Architecture: any
24//! Description: Lossless parser for deb822 style files.
25//!   This parser can be used to parse files in the deb822 format, while preserving
26//!   all whitespace and comments. It is based on the [rowan] library, which is a
27//!   lossless parser library for Rust.
28//! "###;
29//!
30//! let deb822 = Deb822::from_str(input).unwrap();
31//! assert_eq!(deb822.paragraphs().count(), 2);
32//! let homepage = deb822.paragraphs().nth(0).unwrap().get("Homepage");
33//! assert_eq!(homepage.as_deref(), Some("https://github.com/jelmer/deb822-lossless"));
34//! ```
35
36use crate::{
37    lex::lex,
38    lex::SyntaxKind::{self, *},
39    Indentation,
40};
41use rowan::ast::AstNode;
42use std::path::Path;
43use std::str::FromStr;
44
45/// A positioned parse error containing location information.
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct PositionedParseError {
48    /// The error message
49    pub message: String,
50    /// The text range where the error occurred
51    pub range: rowan::TextRange,
52    /// Optional error code for categorization
53    pub code: Option<String>,
54}
55
56impl std::fmt::Display for PositionedParseError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.message)
59    }
60}
61
62impl std::error::Error for PositionedParseError {}
63
64/// List of encountered syntax errors.
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct ParseError(pub Vec<String>);
67
68impl std::fmt::Display for ParseError {
69    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
70        for err in &self.0 {
71            writeln!(f, "{}", err)?;
72        }
73        Ok(())
74    }
75}
76
77impl std::error::Error for ParseError {}
78
79/// Error parsing deb822 control files
80#[derive(Debug)]
81pub enum Error {
82    /// A syntax error was encountered while parsing the file.
83    ParseError(ParseError),
84
85    /// An I/O error was encountered while reading the file.
86    IoError(std::io::Error),
87
88    /// An invalid value was provided (e.g., empty continuation lines).
89    InvalidValue(String),
90}
91
92impl std::fmt::Display for Error {
93    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
94        match &self {
95            Error::ParseError(err) => write!(f, "{}", err),
96            Error::IoError(err) => write!(f, "{}", err),
97            Error::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
98        }
99    }
100}
101
102impl From<ParseError> for Error {
103    fn from(err: ParseError) -> Self {
104        Self::ParseError(err)
105    }
106}
107
108impl From<std::io::Error> for Error {
109    fn from(err: std::io::Error) -> Self {
110        Self::IoError(err)
111    }
112}
113
114impl std::error::Error for Error {}
115
116/// Second, implementing the `Language` trait teaches rowan to convert between
117/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
118/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub enum Lang {}
121impl rowan::Language for Lang {
122    type Kind = SyntaxKind;
123    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
124        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
125    }
126    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
127        kind.into()
128    }
129}
130
131/// GreenNode is an immutable tree, which is cheap to change,
132/// but doesn't contain offsets and parent pointers.
133use rowan::GreenNode;
134
135/// You can construct GreenNodes by hand, but a builder
136/// is helpful for top-down parsers: it maintains a stack
137/// of currently in-progress nodes
138use rowan::GreenNodeBuilder;
139
140/// The parse results are stored as a "green tree".
141/// We'll discuss working with the results later
142pub(crate) struct Parse {
143    pub(crate) green_node: GreenNode,
144    #[allow(unused)]
145    pub(crate) errors: Vec<String>,
146    pub(crate) positioned_errors: Vec<PositionedParseError>,
147}
148
149pub(crate) fn parse(text: &str) -> Parse {
150    struct Parser<'a> {
151        /// input tokens, including whitespace,
152        /// in *reverse* order.
153        tokens: Vec<(SyntaxKind, &'a str)>,
154        /// the in-progress tree.
155        builder: GreenNodeBuilder<'static>,
156        /// the list of syntax errors we've accumulated
157        /// so far.
158        errors: Vec<String>,
159        /// positioned errors with location information
160        positioned_errors: Vec<PositionedParseError>,
161        /// All tokens with their positions in forward order for position tracking
162        token_positions: Vec<(SyntaxKind, rowan::TextSize, rowan::TextSize)>,
163        /// current token index (counting from the end since tokens are in reverse)
164        current_token_index: usize,
165    }
166
167    impl<'a> Parser<'a> {
168        /// Skip to next paragraph boundary for error recovery
169        fn skip_to_paragraph_boundary(&mut self) {
170            while self.current().is_some() {
171                match self.current() {
172                    Some(NEWLINE) => {
173                        self.bump();
174                        // Check if next line starts a new paragraph (key at start of line)
175                        if self.at_paragraph_start() {
176                            break;
177                        }
178                    }
179                    _ => {
180                        self.bump();
181                    }
182                }
183            }
184        }
185
186        /// Check if we're at the start of a new paragraph
187        fn at_paragraph_start(&self) -> bool {
188            match self.current() {
189                Some(KEY) => true,
190                Some(COMMENT) => true,
191                None => true, // EOF is a valid paragraph boundary
192                _ => false,
193            }
194        }
195
196        /// Attempt to recover from entry parsing errors
197        fn recover_entry(&mut self) {
198            // Skip to end of current line
199            while self.current().is_some() && self.current() != Some(NEWLINE) {
200                self.bump();
201            }
202            // Consume the newline if present
203            if self.current() == Some(NEWLINE) {
204                self.bump();
205            }
206        }
207        fn parse_entry(&mut self) {
208            // Handle leading comments
209            while self.current() == Some(COMMENT) {
210                self.bump();
211
212                match self.current() {
213                    Some(NEWLINE) => {
214                        self.bump();
215                    }
216                    None => {
217                        return;
218                    }
219                    Some(g) => {
220                        self.builder.start_node(ERROR.into());
221                        self.add_positioned_error(
222                            format!("expected newline after comment, got {g:?}"),
223                            Some("unexpected_token_after_comment".to_string()),
224                        );
225                        self.bump();
226                        self.builder.finish_node();
227                        self.recover_entry();
228                        return;
229                    }
230                }
231            }
232
233            self.builder.start_node(ENTRY.into());
234            let mut entry_has_errors = false;
235
236            // Parse the key
237            if self.current() == Some(KEY) {
238                self.bump();
239                self.skip_ws();
240            } else {
241                entry_has_errors = true;
242                self.builder.start_node(ERROR.into());
243
244                // Enhanced error recovery for malformed keys
245                match self.current() {
246                    Some(VALUE) | Some(WHITESPACE) => {
247                        self.add_positioned_error(
248                            "field name cannot start with whitespace or special characters"
249                                .to_string(),
250                            Some("invalid_field_name".to_string()),
251                        );
252                        // Try to consume what might be an intended key
253                        while self.current() == Some(VALUE) || self.current() == Some(WHITESPACE) {
254                            self.bump();
255                        }
256                    }
257                    Some(COLON) => {
258                        self.add_positioned_error(
259                            "field name missing before colon".to_string(),
260                            Some("missing_field_name".to_string()),
261                        );
262                    }
263                    Some(NEWLINE) => {
264                        self.add_positioned_error(
265                            "empty line where field expected".to_string(),
266                            Some("empty_field_line".to_string()),
267                        );
268                        self.builder.finish_node();
269                        self.builder.finish_node();
270                        return;
271                    }
272                    _ => {
273                        self.add_positioned_error(
274                            format!("expected field name, got {:?}", self.current()),
275                            Some("missing_key".to_string()),
276                        );
277                        if self.current().is_some() {
278                            self.bump();
279                        }
280                    }
281                }
282                self.builder.finish_node();
283            }
284
285            // Parse the colon
286            if self.current() == Some(COLON) {
287                self.bump();
288                self.skip_ws();
289            } else {
290                entry_has_errors = true;
291                self.builder.start_node(ERROR.into());
292
293                // Enhanced error recovery for missing colon
294                match self.current() {
295                    Some(VALUE) => {
296                        self.add_positioned_error(
297                            "missing colon ':' after field name".to_string(),
298                            Some("missing_colon".to_string()),
299                        );
300                        // Don't consume the value, let it be parsed as the field value
301                    }
302                    Some(NEWLINE) => {
303                        self.add_positioned_error(
304                            "field name without value (missing colon and value)".to_string(),
305                            Some("incomplete_field".to_string()),
306                        );
307                        self.builder.finish_node();
308                        self.builder.finish_node();
309                        return;
310                    }
311                    Some(KEY) => {
312                        self.add_positioned_error(
313                            "field name followed by another field name (missing colon and value)"
314                                .to_string(),
315                            Some("consecutive_field_names".to_string()),
316                        );
317                        // Don't consume the next key, let it be parsed as a new entry
318                        self.builder.finish_node();
319                        self.builder.finish_node();
320                        return;
321                    }
322                    _ => {
323                        self.add_positioned_error(
324                            format!("expected colon ':', got {:?}", self.current()),
325                            Some("missing_colon".to_string()),
326                        );
327                        if self.current().is_some() {
328                            self.bump();
329                        }
330                    }
331                }
332                self.builder.finish_node();
333            }
334
335            // Parse the value (potentially multi-line)
336            loop {
337                while self.current() == Some(WHITESPACE) || self.current() == Some(VALUE) {
338                    self.bump();
339                }
340
341                match self.current() {
342                    None => {
343                        break;
344                    }
345                    Some(NEWLINE) => {
346                        self.bump();
347                    }
348                    Some(KEY) => {
349                        // We've hit another field, this entry is complete
350                        break;
351                    }
352                    Some(g) => {
353                        self.builder.start_node(ERROR.into());
354                        self.add_positioned_error(
355                            format!("unexpected token in field value: {g:?}"),
356                            Some("unexpected_value_token".to_string()),
357                        );
358                        self.bump();
359                        self.builder.finish_node();
360                    }
361                }
362
363                // Check for continuation lines
364                if self.current() == Some(INDENT) {
365                    self.bump();
366                    self.skip_ws();
367
368                    // After indent and whitespace, we must have actual content (VALUE token)
369                    // An empty continuation line (indent followed immediately by newline or EOF)
370                    // is not valid according to Debian Policy
371                    if self.current() == Some(NEWLINE) || self.current().is_none() {
372                        self.builder.start_node(ERROR.into());
373                        self.add_positioned_error(
374                            "empty continuation line (line with only whitespace)".to_string(),
375                            Some("empty_continuation_line".to_string()),
376                        );
377                        self.builder.finish_node();
378                        break;
379                    }
380                } else {
381                    break;
382                }
383            }
384
385            self.builder.finish_node();
386
387            // If the entry had errors, we might want to recover
388            if entry_has_errors && !self.at_paragraph_start() && self.current().is_some() {
389                self.recover_entry();
390            }
391        }
392
393        fn parse_paragraph(&mut self) {
394            self.builder.start_node(PARAGRAPH.into());
395
396            let mut consecutive_errors = 0;
397            const MAX_CONSECUTIVE_ERRORS: usize = 5;
398
399            while self.current() != Some(NEWLINE) && self.current().is_some() {
400                let error_count_before = self.positioned_errors.len();
401
402                // Check if we're at a valid entry start
403                if self.current() == Some(KEY) || self.current() == Some(COMMENT) {
404                    self.parse_entry();
405
406                    // Reset consecutive error count if we successfully parsed something
407                    if self.positioned_errors.len() == error_count_before {
408                        consecutive_errors = 0;
409                    } else {
410                        consecutive_errors += 1;
411                    }
412                } else {
413                    // We're not at a valid entry start, this is an error
414                    consecutive_errors += 1;
415
416                    self.builder.start_node(ERROR.into());
417                    match self.current() {
418                        Some(VALUE) => {
419                            self.add_positioned_error(
420                                "orphaned text without field name".to_string(),
421                                Some("orphaned_text".to_string()),
422                            );
423                            // Consume the orphaned text
424                            while self.current() == Some(VALUE)
425                                || self.current() == Some(WHITESPACE)
426                            {
427                                self.bump();
428                            }
429                        }
430                        Some(COLON) => {
431                            self.add_positioned_error(
432                                "orphaned colon without field name".to_string(),
433                                Some("orphaned_colon".to_string()),
434                            );
435                            self.bump();
436                        }
437                        Some(INDENT) => {
438                            self.add_positioned_error(
439                                "unexpected indentation without field".to_string(),
440                                Some("unexpected_indent".to_string()),
441                            );
442                            self.bump();
443                        }
444                        _ => {
445                            self.add_positioned_error(
446                                format!(
447                                    "unexpected token at paragraph level: {:?}",
448                                    self.current()
449                                ),
450                                Some("unexpected_paragraph_token".to_string()),
451                            );
452                            self.bump();
453                        }
454                    }
455                    self.builder.finish_node();
456                }
457
458                // If we have too many consecutive errors, skip to paragraph boundary
459                if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
460                    self.add_positioned_error(
461                        "too many consecutive parse errors, skipping to next paragraph".to_string(),
462                        Some("parse_recovery".to_string()),
463                    );
464                    self.skip_to_paragraph_boundary();
465                    break;
466                }
467            }
468
469            self.builder.finish_node();
470        }
471
472        fn parse(mut self) -> Parse {
473            // Make sure that the root node covers all source
474            self.builder.start_node(ROOT.into());
475            while self.current().is_some() {
476                self.skip_ws_and_newlines();
477                if self.current().is_some() {
478                    self.parse_paragraph();
479                }
480            }
481            // Don't forget to eat *trailing* whitespace
482            self.skip_ws_and_newlines();
483            // Close the root node.
484            self.builder.finish_node();
485
486            // Turn the builder into a GreenNode
487            Parse {
488                green_node: self.builder.finish(),
489                errors: self.errors,
490                positioned_errors: self.positioned_errors,
491            }
492        }
493        /// Advance one token, adding it to the current branch of the tree builder.
494        fn bump(&mut self) {
495            let (kind, text) = self.tokens.pop().unwrap();
496            self.builder.token(kind.into(), text);
497            if self.current_token_index > 0 {
498                self.current_token_index -= 1;
499            }
500        }
501        /// Peek at the first unprocessed token
502        fn current(&self) -> Option<SyntaxKind> {
503            self.tokens.last().map(|(kind, _)| *kind)
504        }
505
506        /// Add a positioned error at the current position
507        fn add_positioned_error(&mut self, message: String, code: Option<String>) {
508            let range = if self.current_token_index < self.token_positions.len() {
509                let (_, start, end) = self.token_positions[self.current_token_index];
510                rowan::TextRange::new(start, end)
511            } else {
512                // Default to end of text if no current token
513                let end = self
514                    .token_positions
515                    .last()
516                    .map(|(_, _, end)| *end)
517                    .unwrap_or_else(|| rowan::TextSize::from(0));
518                rowan::TextRange::new(end, end)
519            };
520
521            self.positioned_errors.push(PositionedParseError {
522                message: message.clone(),
523                range,
524                code,
525            });
526            self.errors.push(message);
527        }
528        fn skip_ws(&mut self) {
529            while self.current() == Some(WHITESPACE) || self.current() == Some(COMMENT) {
530                self.bump()
531            }
532        }
533        fn skip_ws_and_newlines(&mut self) {
534            while self.current() == Some(WHITESPACE)
535                || self.current() == Some(COMMENT)
536                || self.current() == Some(NEWLINE)
537            {
538                self.builder.start_node(EMPTY_LINE.into());
539                while self.current() != Some(NEWLINE) && self.current().is_some() {
540                    self.bump();
541                }
542                if self.current() == Some(NEWLINE) {
543                    self.bump();
544                }
545                self.builder.finish_node();
546            }
547        }
548    }
549
550    let mut tokens = lex(text).collect::<Vec<_>>();
551
552    // Build token positions in forward order
553    let mut token_positions = Vec::new();
554    let mut position = rowan::TextSize::from(0);
555    for (kind, text) in &tokens {
556        let start = position;
557        let end = start + rowan::TextSize::of(*text);
558        token_positions.push((*kind, start, end));
559        position = end;
560    }
561
562    // Reverse tokens for parsing (but keep positions in forward order)
563    tokens.reverse();
564    let current_token_index = tokens.len().saturating_sub(1);
565
566    Parser {
567        tokens,
568        builder: GreenNodeBuilder::new(),
569        errors: Vec::new(),
570        positioned_errors: Vec::new(),
571        token_positions,
572        current_token_index,
573    }
574    .parse()
575}
576
577/// To work with the parse results we need a view into the
578/// green tree - the Syntax tree.
579/// It is also immutable, like a GreenNode,
580/// but it contains parent pointers, offsets, and
581/// has identity semantics.
582type SyntaxNode = rowan::SyntaxNode<Lang>;
583#[allow(unused)]
584type SyntaxToken = rowan::SyntaxToken<Lang>;
585#[allow(unused)]
586type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
587
588impl Parse {
589    #[cfg(test)]
590    fn syntax(&self) -> SyntaxNode {
591        SyntaxNode::new_root(self.green_node.clone())
592    }
593
594    fn root_mut(&self) -> Deb822 {
595        Deb822::cast(SyntaxNode::new_root_mut(self.green_node.clone())).unwrap()
596    }
597}
598
599/// Calculate line and column (both 0-indexed) for the given offset in the tree.
600/// Column is measured in bytes from the start of the line.
601fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
602    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
603    let mut line = 0;
604    let mut last_newline_offset = rowan::TextSize::from(0);
605
606    for element in root.preorder_with_tokens() {
607        if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
608            if token.text_range().start() >= offset {
609                break;
610            }
611
612            // Count newlines and track position of last one
613            for (idx, _) in token.text().match_indices('\n') {
614                line += 1;
615                last_newline_offset =
616                    token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
617            }
618        }
619    }
620
621    let column: usize = (offset - last_newline_offset).into();
622    (line, column)
623}
624
625macro_rules! ast_node {
626    ($ast:ident, $kind:ident) => {
627        #[doc = "An AST node representing a `"]
628        #[doc = stringify!($ast)]
629        #[doc = "`."]
630        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
631        #[repr(transparent)]
632        pub struct $ast(SyntaxNode);
633        impl $ast {
634            #[allow(unused)]
635            fn cast(node: SyntaxNode) -> Option<Self> {
636                if node.kind() == $kind {
637                    Some(Self(node))
638                } else {
639                    None
640                }
641            }
642
643            /// Get the line number (0-indexed) where this node starts.
644            pub fn line(&self) -> usize {
645                line_col_at_offset(&self.0, self.0.text_range().start()).0
646            }
647
648            /// Get the column number (0-indexed, in bytes) where this node starts.
649            pub fn column(&self) -> usize {
650                line_col_at_offset(&self.0, self.0.text_range().start()).1
651            }
652
653            /// Get both line and column (0-indexed) where this node starts.
654            /// Returns (line, column) where column is measured in bytes from the start of the line.
655            pub fn line_col(&self) -> (usize, usize) {
656                line_col_at_offset(&self.0, self.0.text_range().start())
657            }
658        }
659
660        impl AstNode for $ast {
661            type Language = Lang;
662
663            fn can_cast(kind: SyntaxKind) -> bool {
664                kind == $kind
665            }
666
667            fn cast(syntax: SyntaxNode) -> Option<Self> {
668                Self::cast(syntax)
669            }
670
671            fn syntax(&self) -> &SyntaxNode {
672                &self.0
673            }
674        }
675
676        impl std::fmt::Display for $ast {
677            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
678                write!(f, "{}", self.0.text())
679            }
680        }
681    };
682}
683
684ast_node!(Deb822, ROOT);
685ast_node!(Paragraph, PARAGRAPH);
686ast_node!(Entry, ENTRY);
687
688impl Default for Deb822 {
689    fn default() -> Self {
690        Self::new()
691    }
692}
693
694impl Deb822 {
695    /// Create an independent snapshot of this Deb822 file.
696    ///
697    /// This creates a new mutable tree that shares the same underlying immutable
698    /// GreenNode data. Modifications to the original will not affect the snapshot
699    /// and vice versa.
700    ///
701    /// This is more efficient than serializing and re-parsing because it reuses
702    /// the GreenNode structure.
703    ///
704    /// # Example
705    /// ```
706    /// use deb822_lossless::Deb822;
707    ///
708    /// let text = "Package: foo\n";
709    /// let deb822: Deb822 = text.parse().unwrap();
710    /// let snapshot = deb822.snapshot();
711    ///
712    /// // Modifications to deb822 won't affect snapshot
713    /// let mut para = deb822.paragraphs().next().unwrap();
714    /// para.set("Package", "modified");
715    ///
716    /// let snapshot_para = snapshot.paragraphs().next().unwrap();
717    /// assert_eq!(snapshot_para.get("Package").as_deref(), Some("foo"));
718    /// ```
719    pub fn snapshot(&self) -> Self {
720        Deb822(SyntaxNode::new_root_mut(self.0.green().into_owned()))
721    }
722
723    /// Create a new empty deb822 file.
724    pub fn new() -> Deb822 {
725        let mut builder = GreenNodeBuilder::new();
726
727        builder.start_node(ROOT.into());
728        builder.finish_node();
729        Deb822(SyntaxNode::new_root_mut(builder.finish()))
730    }
731
732    /// Parse deb822 text, returning a Parse result
733    pub fn parse(text: &str) -> crate::Parse<Deb822> {
734        crate::Parse::parse_deb822(text)
735    }
736
737    /// Provide a formatter that can handle indentation and trailing separators
738    ///
739    /// # Arguments
740    /// * `control` - The control file to format
741    /// * `indentation` - The indentation to use
742    /// * `immediate_empty_line` - Whether the value should always start with an empty line. If true,
743    ///   then the result becomes something like "Field:\n value". This parameter
744    ///   only applies to the values that will be formatted over more than one line.
745    /// * `max_line_length_one_liner` - If set, then this is the max length of the value
746    ///   if it is crammed into a "one-liner" value. If the value(s) fit into
747    ///   one line, this parameter will overrule immediate_empty_line.
748    /// * `sort_paragraphs` - If set, then this function will sort the paragraphs according to the
749    ///   given function.
750    /// * `sort_entries` - If set, then this function will sort the entries according to the
751    ///   given function.
752    #[must_use]
753    pub fn wrap_and_sort(
754        &self,
755        sort_paragraphs: Option<&dyn Fn(&Paragraph, &Paragraph) -> std::cmp::Ordering>,
756        wrap_and_sort_paragraph: Option<&dyn Fn(&Paragraph) -> Paragraph>,
757    ) -> Deb822 {
758        let mut builder = GreenNodeBuilder::new();
759        builder.start_node(ROOT.into());
760        let mut current = vec![];
761        let mut paragraphs = vec![];
762        for c in self.0.children_with_tokens() {
763            match c.kind() {
764                PARAGRAPH => {
765                    paragraphs.push((
766                        current,
767                        Paragraph::cast(c.as_node().unwrap().clone()).unwrap(),
768                    ));
769                    current = vec![];
770                }
771                COMMENT | ERROR => {
772                    current.push(c);
773                }
774                EMPTY_LINE => {
775                    current.extend(
776                        c.as_node()
777                            .unwrap()
778                            .children_with_tokens()
779                            .skip_while(|c| matches!(c.kind(), EMPTY_LINE | NEWLINE | WHITESPACE)),
780                    );
781                }
782                _ => {}
783            }
784        }
785        if let Some(sort_paragraph) = sort_paragraphs {
786            paragraphs.sort_by(|a, b| {
787                let a_key = &a.1;
788                let b_key = &b.1;
789                sort_paragraph(a_key, b_key)
790            });
791        }
792
793        for (i, paragraph) in paragraphs.into_iter().enumerate() {
794            if i > 0 {
795                builder.start_node(EMPTY_LINE.into());
796                builder.token(NEWLINE.into(), "\n");
797                builder.finish_node();
798            }
799            for c in paragraph.0.into_iter() {
800                builder.token(c.kind().into(), c.as_token().unwrap().text());
801            }
802            let new_paragraph = if let Some(ref ws) = wrap_and_sort_paragraph {
803                ws(&paragraph.1)
804            } else {
805                paragraph.1
806            };
807            inject(&mut builder, new_paragraph.0);
808        }
809
810        for c in current {
811            builder.token(c.kind().into(), c.as_token().unwrap().text());
812        }
813
814        builder.finish_node();
815        Self(SyntaxNode::new_root_mut(builder.finish()))
816    }
817
818    /// Normalize the spacing around field separators (colons) for all entries in all paragraphs in place.
819    ///
820    /// This ensures that there is exactly one space after the colon and before the value
821    /// for each field in every paragraph. This is a lossless operation that preserves the
822    /// field names, values, and comments, but normalizes the whitespace formatting.
823    ///
824    /// # Examples
825    ///
826    /// ```
827    /// use deb822_lossless::Deb822;
828    /// use std::str::FromStr;
829    ///
830    /// let input = "Field1:    value1\nField2:value2\n\nField3:  value3\n";
831    /// let mut deb822 = Deb822::from_str(input).unwrap();
832    ///
833    /// deb822.normalize_field_spacing();
834    /// assert_eq!(deb822.to_string(), "Field1: value1\nField2: value2\n\nField3: value3\n");
835    /// ```
836    pub fn normalize_field_spacing(&mut self) -> bool {
837        let mut any_changed = false;
838
839        // Collect paragraphs to avoid borrowing issues
840        let mut paragraphs: Vec<_> = self.paragraphs().collect();
841
842        // Normalize each paragraph
843        for para in &mut paragraphs {
844            if para.normalize_field_spacing() {
845                any_changed = true;
846            }
847        }
848
849        any_changed
850    }
851
852    /// Returns an iterator over all paragraphs in the file.
853    pub fn paragraphs(&self) -> impl Iterator<Item = Paragraph> {
854        self.0.children().filter_map(Paragraph::cast)
855    }
856
857    /// Returns paragraphs that intersect with the given text range.
858    ///
859    /// A paragraph is included if its text range overlaps with the provided range.
860    ///
861    /// # Arguments
862    ///
863    /// * `range` - The text range to query
864    ///
865    /// # Returns
866    ///
867    /// An iterator over paragraphs that intersect with the range
868    ///
869    /// # Examples
870    ///
871    /// ```
872    /// use deb822_lossless::{Deb822, TextRange};
873    ///
874    /// let input = "Package: foo\n\nPackage: bar\n\nPackage: baz\n";
875    /// let deb822 = Deb822::parse(input).tree();
876    ///
877    /// // Query paragraphs in the first half of the document
878    /// let range = TextRange::new(0.into(), 20.into());
879    /// let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
880    /// assert!(paras.len() >= 1);
881    /// ```
882    pub fn paragraphs_in_range(
883        &self,
884        range: rowan::TextRange,
885    ) -> impl Iterator<Item = Paragraph> + '_ {
886        self.paragraphs().filter(move |p| {
887            let para_range = p.text_range();
888            // Check if ranges overlap: para starts before range ends AND para ends after range starts
889            para_range.start() < range.end() && para_range.end() > range.start()
890        })
891    }
892
893    /// Find the paragraph that contains the given text offset.
894    ///
895    /// # Arguments
896    ///
897    /// * `offset` - The text offset to query
898    ///
899    /// # Returns
900    ///
901    /// The paragraph containing the offset, or None if no paragraph contains it
902    ///
903    /// # Examples
904    ///
905    /// ```
906    /// use deb822_lossless::{Deb822, TextSize};
907    ///
908    /// let input = "Package: foo\n\nPackage: bar\n";
909    /// let deb822 = Deb822::parse(input).tree();
910    ///
911    /// // Find paragraph at offset 5 (within first paragraph)
912    /// let para = deb822.paragraph_at_position(TextSize::from(5));
913    /// assert!(para.is_some());
914    /// ```
915    pub fn paragraph_at_position(&self, offset: rowan::TextSize) -> Option<Paragraph> {
916        self.paragraphs().find(|p| {
917            let range = p.text_range();
918            range.contains(offset)
919        })
920    }
921
922    /// Find the paragraph at the given line number (0-indexed).
923    ///
924    /// # Arguments
925    ///
926    /// * `line` - The line number to query (0-indexed)
927    ///
928    /// # Returns
929    ///
930    /// The paragraph at the given line, or None if no paragraph is at that line
931    ///
932    /// # Examples
933    ///
934    /// ```
935    /// use deb822_lossless::Deb822;
936    ///
937    /// let input = "Package: foo\nVersion: 1.0\n\nPackage: bar\n";
938    /// let deb822 = Deb822::parse(input).tree();
939    ///
940    /// // Find paragraph at line 0
941    /// let para = deb822.paragraph_at_line(0);
942    /// assert!(para.is_some());
943    /// ```
944    pub fn paragraph_at_line(&self, line: usize) -> Option<Paragraph> {
945        self.paragraphs().find(|p| {
946            let start_line = p.line();
947            let range = p.text_range();
948            let text_str = self.0.text().to_string();
949            let text_before_end = &text_str[..range.end().into()];
950            let end_line = text_before_end.lines().count().saturating_sub(1);
951            line >= start_line && line <= end_line
952        })
953    }
954
955    /// Find the entry at the given line and column position.
956    ///
957    /// # Arguments
958    ///
959    /// * `line` - The line number (0-indexed)
960    /// * `col` - The column number (0-indexed)
961    ///
962    /// # Returns
963    ///
964    /// The entry at the given position, or None if no entry is at that position
965    ///
966    /// # Examples
967    ///
968    /// ```
969    /// use deb822_lossless::Deb822;
970    ///
971    /// let input = "Package: foo\nVersion: 1.0\n";
972    /// let deb822 = Deb822::parse(input).tree();
973    ///
974    /// // Find entry at line 0, column 0
975    /// let entry = deb822.entry_at_line_col(0, 0);
976    /// assert!(entry.is_some());
977    /// ```
978    pub fn entry_at_line_col(&self, line: usize, col: usize) -> Option<Entry> {
979        // Convert line/col to text offset
980        let text_str = self.0.text().to_string();
981        let offset: usize = text_str.lines().take(line).map(|l| l.len() + 1).sum();
982        let position = rowan::TextSize::from((offset + col) as u32);
983
984        // Find the entry that contains this position
985        for para in self.paragraphs() {
986            for entry in para.entries() {
987                let range = entry.text_range();
988                if range.contains(position) {
989                    return Some(entry);
990                }
991            }
992        }
993        None
994    }
995
996    /// Converts the perceptual paragraph index to the node index.
997    fn convert_index(&self, index: usize) -> Option<usize> {
998        let mut current_pos = 0usize;
999        if index == 0 {
1000            return Some(0);
1001        }
1002        for (i, node) in self.0.children_with_tokens().enumerate() {
1003            if node.kind() == PARAGRAPH {
1004                if current_pos == index {
1005                    return Some(i);
1006                }
1007                current_pos += 1;
1008            }
1009        }
1010
1011        None
1012    }
1013
1014    /// Delete trailing empty lines after specified node and before any non-empty line nodes.
1015    fn delete_trailing_space(&self, start: usize) {
1016        for (i, node) in self.0.children_with_tokens().enumerate() {
1017            if i < start {
1018                continue;
1019            }
1020            if node.kind() != EMPTY_LINE {
1021                return;
1022            }
1023            // this is not a typo, the index will shift by one after deleting the node
1024            // so instead of deleting using `i`, we use `start` as the start index
1025            self.0.splice_children(start..start + 1, []);
1026        }
1027    }
1028
1029    /// Shared internal function to insert a new paragraph into the file.
1030    fn insert_empty_paragraph(&mut self, index: Option<usize>) -> Paragraph {
1031        let paragraph = Paragraph::new();
1032        let mut to_insert = vec![];
1033        if self.0.children().count() > 0 {
1034            let mut builder = GreenNodeBuilder::new();
1035            builder.start_node(EMPTY_LINE.into());
1036            builder.token(NEWLINE.into(), "\n");
1037            builder.finish_node();
1038            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
1039        }
1040        to_insert.push(paragraph.0.clone().into());
1041        let insertion_point = match index {
1042            Some(i) => {
1043                if to_insert.len() > 1 {
1044                    to_insert.swap(0, 1);
1045                }
1046                i
1047            }
1048            None => self.0.children().count(),
1049        };
1050        self.0
1051            .splice_children(insertion_point..insertion_point, to_insert);
1052        paragraph
1053    }
1054
1055    /// Insert a new empty paragraph into the file after specified index.
1056    ///
1057    /// # Examples
1058    ///
1059    /// ```
1060    /// use deb822_lossless::{Deb822, Paragraph};
1061    /// let mut d: Deb822 = vec![
1062    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
1063    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
1064    /// ]
1065    /// .into_iter()
1066    /// .collect();
1067    /// let mut p = d.insert_paragraph(0);
1068    /// p.set("Foo", "Baz");
1069    /// assert_eq!(d.to_string(), "Foo: Baz\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n");
1070    /// let mut another = d.insert_paragraph(1);
1071    /// another.set("Y", "Z");
1072    /// assert_eq!(d.to_string(), "Foo: Baz\n\nY: Z\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n");
1073    /// ```
1074    pub fn insert_paragraph(&mut self, index: usize) -> Paragraph {
1075        self.insert_empty_paragraph(self.convert_index(index))
1076    }
1077
1078    /// Remove the paragraph at the specified index from the file.
1079    ///
1080    /// # Examples
1081    ///
1082    /// ```
1083    /// use deb822_lossless::Deb822;
1084    /// let mut d: Deb822 = vec![
1085    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
1086    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
1087    /// ]
1088    /// .into_iter()
1089    /// .collect();
1090    /// d.remove_paragraph(0);
1091    /// assert_eq!(d.to_string(), "A: B\nC: D\n");
1092    /// d.remove_paragraph(0);
1093    /// assert_eq!(d.to_string(), "");
1094    /// ```
1095    pub fn remove_paragraph(&mut self, index: usize) {
1096        if let Some(index) = self.convert_index(index) {
1097            self.0.splice_children(index..index + 1, []);
1098            self.delete_trailing_space(index);
1099        }
1100    }
1101
1102    /// Move a paragraph from one index to another.
1103    ///
1104    /// This moves the paragraph at `from_index` to `to_index`, shifting other paragraphs as needed.
1105    /// If `from_index` equals `to_index`, no operation is performed.
1106    ///
1107    /// # Examples
1108    ///
1109    /// ```
1110    /// use deb822_lossless::Deb822;
1111    /// let mut d: Deb822 = vec![
1112    ///     vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
1113    ///     vec![("A", "B"), ("C", "D")].into_iter().collect(),
1114    ///     vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
1115    /// ]
1116    /// .into_iter()
1117    /// .collect();
1118    /// d.move_paragraph(0, 2);
1119    /// assert_eq!(d.to_string(), "A: B\nC: D\n\nX: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n");
1120    /// ```
1121    pub fn move_paragraph(&mut self, from_index: usize, to_index: usize) {
1122        if from_index == to_index {
1123            return;
1124        }
1125
1126        // Get the paragraph count to validate indices
1127        let paragraph_count = self.paragraphs().count();
1128        if from_index >= paragraph_count || to_index >= paragraph_count {
1129            return;
1130        }
1131
1132        // Clone the paragraph node we want to move
1133        let paragraph_to_move = self.paragraphs().nth(from_index).unwrap().0.clone();
1134
1135        // Remove the paragraph from its original position
1136        let from_physical = self.convert_index(from_index).unwrap();
1137
1138        // Determine the range to remove (paragraph and possibly preceding EMPTY_LINE)
1139        let mut start_idx = from_physical;
1140        if from_physical > 0 {
1141            if let Some(prev_node) = self.0.children_with_tokens().nth(from_physical - 1) {
1142                if prev_node.kind() == EMPTY_LINE {
1143                    start_idx = from_physical - 1;
1144                }
1145            }
1146        }
1147
1148        // Remove the paragraph and any preceding EMPTY_LINE
1149        self.0.splice_children(start_idx..from_physical + 1, []);
1150        self.delete_trailing_space(start_idx);
1151
1152        // Calculate the physical insertion point
1153        // After removal, we need to determine where to insert
1154        // The semantics are: the moved paragraph ends up at logical index to_index in the final result
1155        let insert_at = if to_index > from_index {
1156            // Moving forward: after removal, to_index-1 paragraphs should be before the moved one
1157            // So we insert after paragraph at index (to_index - 1)
1158            let target_idx = to_index - 1;
1159            if let Some(target_physical) = self.convert_index(target_idx) {
1160                target_physical + 1
1161            } else {
1162                // If convert_index returns None, insert at the end
1163                self.0.children().count()
1164            }
1165        } else {
1166            // Moving backward: after removal, to_index paragraphs should be before the moved one
1167            // So we insert at paragraph index to_index
1168            if let Some(target_physical) = self.convert_index(to_index) {
1169                target_physical
1170            } else {
1171                self.0.children().count()
1172            }
1173        };
1174
1175        // Build the nodes to insert
1176        let mut to_insert = vec![];
1177
1178        // Determine if we need to add an EMPTY_LINE before the paragraph
1179        let needs_empty_line_before = if insert_at == 0 {
1180            // At the beginning - no empty line before
1181            false
1182        } else if insert_at > 0 {
1183            // Check if there's already an EMPTY_LINE at the insertion point
1184            if let Some(node_at_insert) = self.0.children_with_tokens().nth(insert_at - 1) {
1185                node_at_insert.kind() != EMPTY_LINE
1186            } else {
1187                false
1188            }
1189        } else {
1190            false
1191        };
1192
1193        if needs_empty_line_before {
1194            let mut builder = GreenNodeBuilder::new();
1195            builder.start_node(EMPTY_LINE.into());
1196            builder.token(NEWLINE.into(), "\n");
1197            builder.finish_node();
1198            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
1199        }
1200
1201        to_insert.push(paragraph_to_move.into());
1202
1203        // Determine if we need to add an EMPTY_LINE after the paragraph
1204        let needs_empty_line_after = if insert_at < self.0.children().count() {
1205            // There are nodes after - check if next node is EMPTY_LINE
1206            if let Some(node_after) = self.0.children_with_tokens().nth(insert_at) {
1207                node_after.kind() != EMPTY_LINE
1208            } else {
1209                false
1210            }
1211        } else {
1212            false
1213        };
1214
1215        if needs_empty_line_after {
1216            let mut builder = GreenNodeBuilder::new();
1217            builder.start_node(EMPTY_LINE.into());
1218            builder.token(NEWLINE.into(), "\n");
1219            builder.finish_node();
1220            to_insert.push(SyntaxNode::new_root_mut(builder.finish()).into());
1221        }
1222
1223        // Insert at the new position
1224        self.0.splice_children(insert_at..insert_at, to_insert);
1225    }
1226
1227    /// Add a new empty paragraph to the end of the file.
1228    pub fn add_paragraph(&mut self) -> Paragraph {
1229        self.insert_empty_paragraph(None)
1230    }
1231
1232    /// Swap two paragraphs by their indices.
1233    ///
1234    /// This method swaps the positions of two paragraphs while preserving their
1235    /// content, formatting, whitespace, and comments. The paragraphs at positions
1236    /// `index1` and `index2` will exchange places.
1237    ///
1238    /// # Arguments
1239    ///
1240    /// * `index1` - The index of the first paragraph to swap
1241    /// * `index2` - The index of the second paragraph to swap
1242    ///
1243    /// # Panics
1244    ///
1245    /// Panics if either `index1` or `index2` is out of bounds.
1246    ///
1247    /// # Examples
1248    ///
1249    /// ```
1250    /// use deb822_lossless::Deb822;
1251    /// let mut d: Deb822 = vec![
1252    ///     vec![("Foo", "Bar")].into_iter().collect(),
1253    ///     vec![("A", "B")].into_iter().collect(),
1254    ///     vec![("X", "Y")].into_iter().collect(),
1255    /// ]
1256    /// .into_iter()
1257    /// .collect();
1258    /// d.swap_paragraphs(0, 2);
1259    /// assert_eq!(d.to_string(), "X: Y\n\nA: B\n\nFoo: Bar\n");
1260    /// ```
1261    pub fn swap_paragraphs(&mut self, index1: usize, index2: usize) {
1262        if index1 == index2 {
1263            return;
1264        }
1265
1266        // Collect all children
1267        let mut children: Vec<_> = self.0.children().map(|n| n.clone().into()).collect();
1268
1269        // Find the child indices for paragraphs
1270        let mut para_child_indices = vec![];
1271        for (child_idx, child) in self.0.children().enumerate() {
1272            if child.kind() == PARAGRAPH {
1273                para_child_indices.push(child_idx);
1274            }
1275        }
1276
1277        // Validate paragraph indices
1278        if index1 >= para_child_indices.len() {
1279            panic!("index1 {} out of bounds", index1);
1280        }
1281        if index2 >= para_child_indices.len() {
1282            panic!("index2 {} out of bounds", index2);
1283        }
1284
1285        let child_idx1 = para_child_indices[index1];
1286        let child_idx2 = para_child_indices[index2];
1287
1288        // Swap the children in the vector
1289        children.swap(child_idx1, child_idx2);
1290
1291        // Replace all children
1292        let num_children = children.len();
1293        self.0.splice_children(0..num_children, children);
1294    }
1295
1296    /// Read a deb822 file from the given path.
1297    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
1298        let text = std::fs::read_to_string(path)?;
1299        Ok(Self::from_str(&text)?)
1300    }
1301
1302    /// Read a deb822 file from the given path, ignoring any syntax errors.
1303    pub fn from_file_relaxed(
1304        path: impl AsRef<Path>,
1305    ) -> Result<(Self, Vec<String>), std::io::Error> {
1306        let text = std::fs::read_to_string(path)?;
1307        Ok(Self::from_str_relaxed(&text))
1308    }
1309
1310    /// Parse a deb822 file from a string, allowing syntax errors.
1311    pub fn from_str_relaxed(s: &str) -> (Self, Vec<String>) {
1312        let parsed = parse(s);
1313        (parsed.root_mut(), parsed.errors)
1314    }
1315
1316    /// Read a deb822 file from a Read object.
1317    pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, Error> {
1318        let mut buf = String::new();
1319        r.read_to_string(&mut buf)?;
1320        Ok(Self::from_str(&buf)?)
1321    }
1322
1323    /// Read a deb822 file from a Read object, allowing syntax errors.
1324    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<(Self, Vec<String>), std::io::Error> {
1325        let mut buf = String::new();
1326        r.read_to_string(&mut buf)?;
1327        Ok(Self::from_str_relaxed(&buf))
1328    }
1329}
1330
1331fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
1332    builder.start_node(node.kind().into());
1333    for child in node.children_with_tokens() {
1334        match child {
1335            rowan::NodeOrToken::Node(child) => {
1336                inject(builder, child);
1337            }
1338            rowan::NodeOrToken::Token(token) => {
1339                builder.token(token.kind().into(), token.text());
1340            }
1341        }
1342    }
1343    builder.finish_node();
1344}
1345
1346impl FromIterator<Paragraph> for Deb822 {
1347    fn from_iter<T: IntoIterator<Item = Paragraph>>(iter: T) -> Self {
1348        let mut builder = GreenNodeBuilder::new();
1349        builder.start_node(ROOT.into());
1350        for (i, paragraph) in iter.into_iter().enumerate() {
1351            if i > 0 {
1352                builder.start_node(EMPTY_LINE.into());
1353                builder.token(NEWLINE.into(), "\n");
1354                builder.finish_node();
1355            }
1356            inject(&mut builder, paragraph.0);
1357        }
1358        builder.finish_node();
1359        Self(SyntaxNode::new_root_mut(builder.finish()))
1360    }
1361}
1362
1363impl From<Vec<(String, String)>> for Paragraph {
1364    fn from(v: Vec<(String, String)>) -> Self {
1365        v.into_iter().collect()
1366    }
1367}
1368
1369impl From<Vec<(&str, &str)>> for Paragraph {
1370    fn from(v: Vec<(&str, &str)>) -> Self {
1371        v.into_iter().collect()
1372    }
1373}
1374
1375impl FromIterator<(String, String)> for Paragraph {
1376    fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
1377        let mut builder = GreenNodeBuilder::new();
1378        builder.start_node(PARAGRAPH.into());
1379        for (key, value) in iter {
1380            builder.start_node(ENTRY.into());
1381            builder.token(KEY.into(), &key);
1382            builder.token(COLON.into(), ":");
1383            builder.token(WHITESPACE.into(), " ");
1384            for (i, line) in value.split('\n').enumerate() {
1385                if i > 0 {
1386                    builder.token(INDENT.into(), " ");
1387                }
1388                builder.token(VALUE.into(), line);
1389                builder.token(NEWLINE.into(), "\n");
1390            }
1391            builder.finish_node();
1392        }
1393        builder.finish_node();
1394        Self(SyntaxNode::new_root_mut(builder.finish()))
1395    }
1396}
1397
1398impl<'a> FromIterator<(&'a str, &'a str)> for Paragraph {
1399    fn from_iter<T: IntoIterator<Item = (&'a str, &'a str)>>(iter: T) -> Self {
1400        let mut builder = GreenNodeBuilder::new();
1401        builder.start_node(PARAGRAPH.into());
1402        for (key, value) in iter {
1403            builder.start_node(ENTRY.into());
1404            builder.token(KEY.into(), key);
1405            builder.token(COLON.into(), ":");
1406            builder.token(WHITESPACE.into(), " ");
1407            for (i, line) in value.split('\n').enumerate() {
1408                if i > 0 {
1409                    builder.token(INDENT.into(), " ");
1410                }
1411                builder.token(VALUE.into(), line);
1412                builder.token(NEWLINE.into(), "\n");
1413            }
1414            builder.finish_node();
1415        }
1416        builder.finish_node();
1417        Self(SyntaxNode::new_root_mut(builder.finish()))
1418    }
1419}
1420
1421/// Detected indentation pattern for multi-line field values
1422#[derive(Debug, Clone, PartialEq, Eq)]
1423pub enum IndentPattern {
1424    /// All fields use a fixed number of spaces for indentation
1425    Fixed(usize),
1426    /// Each field's indentation matches its field name length + 2 (for ": ")
1427    FieldNameLength,
1428}
1429
1430impl IndentPattern {
1431    /// Convert the pattern to a concrete indentation string for a given field name
1432    fn to_string(&self, field_name: &str) -> String {
1433        match self {
1434            IndentPattern::Fixed(spaces) => " ".repeat(*spaces),
1435            IndentPattern::FieldNameLength => " ".repeat(field_name.len() + 2),
1436        }
1437    }
1438}
1439
1440impl Paragraph {
1441    /// Create a new empty paragraph.
1442    pub fn new() -> Paragraph {
1443        let mut builder = GreenNodeBuilder::new();
1444
1445        builder.start_node(PARAGRAPH.into());
1446        builder.finish_node();
1447        Paragraph(SyntaxNode::new_root_mut(builder.finish()))
1448    }
1449
1450    /// Create an independent snapshot of this Paragraph.
1451    ///
1452    /// This creates a new mutable tree that shares the same underlying immutable
1453    /// GreenNode data. Modifications to the original will not affect the snapshot
1454    /// and vice versa.
1455    ///
1456    /// This is more efficient than serializing and re-parsing because it reuses
1457    /// the GreenNode structure.
1458    pub fn snapshot(&self) -> Self {
1459        Paragraph(SyntaxNode::new_root_mut(self.0.green().into_owned()))
1460    }
1461
1462    /// Returns the text range covered by this paragraph.
1463    pub fn text_range(&self) -> rowan::TextRange {
1464        self.0.text_range()
1465    }
1466
1467    /// Returns entries that intersect with the given text range.
1468    ///
1469    /// An entry is included if its text range overlaps with the provided range.
1470    ///
1471    /// # Arguments
1472    ///
1473    /// * `range` - The text range to query
1474    ///
1475    /// # Returns
1476    ///
1477    /// An iterator over entries that intersect with the range
1478    ///
1479    /// # Examples
1480    ///
1481    /// ```
1482    /// use deb822_lossless::{Deb822, TextRange};
1483    ///
1484    /// let input = "Package: foo\nVersion: 1.0\nArchitecture: amd64\n";
1485    /// let deb822 = Deb822::parse(input).tree();
1486    /// let para = deb822.paragraphs().next().unwrap();
1487    ///
1488    /// // Query entries in a specific range
1489    /// let range = TextRange::new(0.into(), 15.into());
1490    /// let entries: Vec<_> = para.entries_in_range(range).collect();
1491    /// assert!(entries.len() >= 1);
1492    /// ```
1493    pub fn entries_in_range(&self, range: rowan::TextRange) -> impl Iterator<Item = Entry> + '_ {
1494        self.entries().filter(move |e| {
1495            let entry_range = e.text_range();
1496            // Check if ranges overlap
1497            entry_range.start() < range.end() && entry_range.end() > range.start()
1498        })
1499    }
1500
1501    /// Find the entry that contains the given text offset.
1502    ///
1503    /// # Arguments
1504    ///
1505    /// * `offset` - The text offset to query
1506    ///
1507    /// # Returns
1508    ///
1509    /// The entry containing the offset, or None if no entry contains it
1510    ///
1511    /// # Examples
1512    ///
1513    /// ```
1514    /// use deb822_lossless::{Deb822, TextSize};
1515    ///
1516    /// let input = "Package: foo\nVersion: 1.0\n";
1517    /// let deb822 = Deb822::parse(input).tree();
1518    /// let para = deb822.paragraphs().next().unwrap();
1519    ///
1520    /// // Find entry at offset 5 (within "Package: foo")
1521    /// let entry = para.entry_at_position(TextSize::from(5));
1522    /// assert!(entry.is_some());
1523    /// ```
1524    pub fn entry_at_position(&self, offset: rowan::TextSize) -> Option<Entry> {
1525        self.entries().find(|e| {
1526            let range = e.text_range();
1527            range.contains(offset)
1528        })
1529    }
1530
1531    /// Reformat this paragraph
1532    ///
1533    /// # Arguments
1534    /// * `indentation` - The indentation to use
1535    /// * `immediate_empty_line` - Whether multi-line values should always start with an empty line
1536    /// * `max_line_length_one_liner` - If set, then this is the max length of the value if it is
1537    ///   crammed into a "one-liner" value
1538    /// * `sort_entries` - If set, then this function will sort the entries according to the given
1539    ///   function
1540    /// * `format_value` - If set, then this function will format the value according to the given
1541    ///   function
1542    #[must_use]
1543    pub fn wrap_and_sort(
1544        &self,
1545        indentation: Indentation,
1546        immediate_empty_line: bool,
1547        max_line_length_one_liner: Option<usize>,
1548        sort_entries: Option<&dyn Fn(&Entry, &Entry) -> std::cmp::Ordering>,
1549        format_value: Option<&dyn Fn(&str, &str) -> String>,
1550    ) -> Paragraph {
1551        let mut builder = GreenNodeBuilder::new();
1552
1553        let mut current = vec![];
1554        let mut entries = vec![];
1555
1556        builder.start_node(PARAGRAPH.into());
1557        for c in self.0.children_with_tokens() {
1558            match c.kind() {
1559                ENTRY => {
1560                    entries.push((current, Entry::cast(c.as_node().unwrap().clone()).unwrap()));
1561                    current = vec![];
1562                }
1563                ERROR | COMMENT => {
1564                    current.push(c);
1565                }
1566                _ => {}
1567            }
1568        }
1569
1570        if let Some(sort_entry) = sort_entries {
1571            entries.sort_by(|a, b| {
1572                let a_key = &a.1;
1573                let b_key = &b.1;
1574                sort_entry(a_key, b_key)
1575            });
1576        }
1577
1578        for (pre, entry) in entries.into_iter() {
1579            for c in pre.into_iter() {
1580                builder.token(c.kind().into(), c.as_token().unwrap().text());
1581            }
1582
1583            inject(
1584                &mut builder,
1585                entry
1586                    .wrap_and_sort(
1587                        indentation,
1588                        immediate_empty_line,
1589                        max_line_length_one_liner,
1590                        format_value,
1591                    )
1592                    .0,
1593            );
1594        }
1595
1596        for c in current {
1597            builder.token(c.kind().into(), c.as_token().unwrap().text());
1598        }
1599
1600        builder.finish_node();
1601        Self(SyntaxNode::new_root_mut(builder.finish()))
1602    }
1603
1604    /// Normalize the spacing around field separators (colons) for all entries in place.
1605    ///
1606    /// This ensures that there is exactly one space after the colon and before the value
1607    /// for each field in the paragraph. This is a lossless operation that preserves the
1608    /// field names, values, and comments, but normalizes the whitespace formatting.
1609    ///
1610    /// # Examples
1611    ///
1612    /// ```
1613    /// use deb822_lossless::Deb822;
1614    /// use std::str::FromStr;
1615    ///
1616    /// let input = "Field1:    value1\nField2:value2\n";
1617    /// let mut deb822 = Deb822::from_str(input).unwrap();
1618    /// let mut para = deb822.paragraphs().next().unwrap();
1619    ///
1620    /// para.normalize_field_spacing();
1621    /// assert_eq!(para.to_string(), "Field1: value1\nField2: value2\n");
1622    /// ```
1623    pub fn normalize_field_spacing(&mut self) -> bool {
1624        let mut any_changed = false;
1625
1626        // Collect entries to avoid borrowing issues
1627        let mut entries: Vec<_> = self.entries().collect();
1628
1629        // Normalize each entry
1630        for entry in &mut entries {
1631            if entry.normalize_field_spacing() {
1632                any_changed = true;
1633            }
1634        }
1635
1636        any_changed
1637    }
1638
1639    /// Returns the value of the given key in the paragraph.
1640    ///
1641    /// Field names are compared case-insensitively.
1642    pub fn get(&self, key: &str) -> Option<String> {
1643        self.entries()
1644            .find(|e| {
1645                e.key()
1646                    .as_deref()
1647                    .is_some_and(|k| k.eq_ignore_ascii_case(key))
1648            })
1649            .map(|e| e.value())
1650    }
1651
1652    /// Returns the entry for the given key in the paragraph.
1653    ///
1654    /// Field names are compared case-insensitively.
1655    pub fn get_entry(&self, key: &str) -> Option<Entry> {
1656        self.entries().find(|e| {
1657            e.key()
1658                .as_deref()
1659                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1660        })
1661    }
1662
1663    /// Returns the value of the given key with a specific indentation pattern applied.
1664    ///
1665    /// This returns the field value reformatted as if it were written with the specified
1666    /// indentation pattern. For single-line values, this is the same as `get()`.
1667    /// For multi-line values, the continuation lines are prefixed with indentation
1668    /// calculated from the indent pattern.
1669    ///
1670    /// Field names are compared case-insensitively.
1671    ///
1672    /// # Arguments
1673    /// * `key` - The field name to retrieve
1674    /// * `indent_pattern` - The indentation pattern to apply
1675    ///
1676    /// # Example
1677    /// ```
1678    /// use deb822_lossless::{Deb822, IndentPattern};
1679    /// use std::str::FromStr;
1680    ///
1681    /// let input = "Field: First\n   Second\n   Third\n";
1682    /// let deb = Deb822::from_str(input).unwrap();
1683    /// let para = deb.paragraphs().next().unwrap();
1684    ///
1685    /// // Get with fixed 2-space indentation - strips 2 spaces from each line
1686    /// let value = para.get_with_indent("Field", &IndentPattern::Fixed(2)).unwrap();
1687    /// assert_eq!(value, "First\n Second\n Third");
1688    /// ```
1689    pub fn get_with_indent(&self, key: &str, indent_pattern: &IndentPattern) -> Option<String> {
1690        use crate::lex::SyntaxKind::{INDENT, VALUE};
1691
1692        self.entries()
1693            .find(|e| {
1694                e.key()
1695                    .as_deref()
1696                    .is_some_and(|k| k.eq_ignore_ascii_case(key))
1697            })
1698            .and_then(|e| {
1699                let field_key = e.key()?;
1700                let expected_indent = indent_pattern.to_string(&field_key);
1701                let expected_len = expected_indent.len();
1702
1703                let mut result = String::new();
1704                let mut first = true;
1705                let mut last_indent: Option<String> = None;
1706
1707                for token in e.0.children_with_tokens().filter_map(|it| it.into_token()) {
1708                    match token.kind() {
1709                        INDENT => {
1710                            last_indent = Some(token.text().to_string());
1711                        }
1712                        VALUE => {
1713                            if !first {
1714                                result.push('\n');
1715                                // Add any indentation beyond the expected amount
1716                                if let Some(ref indent_text) = last_indent {
1717                                    if indent_text.len() > expected_len {
1718                                        result.push_str(&indent_text[expected_len..]);
1719                                    }
1720                                }
1721                            }
1722                            result.push_str(token.text());
1723                            first = false;
1724                            last_indent = None;
1725                        }
1726                        _ => {}
1727                    }
1728                }
1729
1730                Some(result)
1731            })
1732    }
1733
1734    /// Get a multi-line field value with single-space indentation stripped.
1735    ///
1736    /// This is a convenience wrapper around `get_with_indent()` that uses
1737    /// `IndentPattern::Fixed(1)`, which is the standard indentation for
1738    /// multi-line fields in Debian control files.
1739    ///
1740    /// # Arguments
1741    ///
1742    /// * `key` - The field name (case-insensitive)
1743    ///
1744    /// # Returns
1745    ///
1746    /// The field value with single-space indentation stripped from continuation lines,
1747    /// or `None` if the field doesn't exist.
1748    ///
1749    /// # Example
1750    ///
1751    /// ```
1752    /// use deb822_lossless::Deb822;
1753    ///
1754    /// let text = "Description: Short description\n Additional line\n";
1755    /// let deb822 = Deb822::parse(text).tree();
1756    /// let para = deb822.paragraphs().next().unwrap();
1757    /// let value = para.get_multiline("Description").unwrap();
1758    /// assert_eq!(value, "Short description\nAdditional line");
1759    /// ```
1760    pub fn get_multiline(&self, key: &str) -> Option<String> {
1761        self.get_with_indent(key, &IndentPattern::Fixed(1))
1762    }
1763
1764    /// Set a multi-line field value with single-space indentation.
1765    ///
1766    /// This is a convenience wrapper around `try_set_with_forced_indent()` that uses
1767    /// `IndentPattern::Fixed(1)`, which is the standard indentation for
1768    /// multi-line fields in Debian control files.
1769    ///
1770    /// # Arguments
1771    ///
1772    /// * `key` - The field name
1773    /// * `value` - The field value (will be formatted with single-space indentation)
1774    /// * `field_order` - Optional field ordering specification
1775    ///
1776    /// # Returns
1777    ///
1778    /// `Ok(())` if successful, or an `Error` if the value is invalid.
1779    ///
1780    /// # Example
1781    ///
1782    /// ```
1783    /// use deb822_lossless::Paragraph;
1784    ///
1785    /// let mut para = Paragraph::new();
1786    /// para.set_multiline("Description", "Short description\nAdditional line", None).unwrap();
1787    /// assert_eq!(para.get_multiline("Description").unwrap(), "Short description\nAdditional line");
1788    /// ```
1789    pub fn set_multiline(
1790        &mut self,
1791        key: &str,
1792        value: &str,
1793        field_order: Option<&[&str]>,
1794    ) -> Result<(), Error> {
1795        self.try_set_with_forced_indent(key, value, &IndentPattern::Fixed(1), field_order)
1796    }
1797
1798    /// Returns whether the paragraph contains the given key.
1799    pub fn contains_key(&self, key: &str) -> bool {
1800        self.get(key).is_some()
1801    }
1802
1803    /// Returns an iterator over all entries in the paragraph.
1804    pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
1805        self.0.children().filter_map(Entry::cast)
1806    }
1807
1808    /// Returns an iterator over all items in the paragraph.
1809    pub fn items(&self) -> impl Iterator<Item = (String, String)> + '_ {
1810        self.entries()
1811            .filter_map(|e| e.key().map(|k| (k, e.value())))
1812    }
1813
1814    /// Returns an iterator over all values for the given key in the paragraph.
1815    ///
1816    /// Field names are compared case-insensitively.
1817    pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = String> + 'a {
1818        self.items().filter_map(move |(k, v)| {
1819            if k.eq_ignore_ascii_case(key) {
1820                Some(v)
1821            } else {
1822                None
1823            }
1824        })
1825    }
1826
1827    /// Returns an iterator over all keys in the paragraph.
1828    pub fn keys(&self) -> impl Iterator<Item = String> + '_ {
1829        self.entries().filter_map(|e| e.key())
1830    }
1831
1832    /// Remove the given field from the paragraph.
1833    ///
1834    /// Field names are compared case-insensitively.
1835    pub fn remove(&mut self, key: &str) {
1836        for mut entry in self.entries() {
1837            if entry
1838                .key()
1839                .as_deref()
1840                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1841            {
1842                entry.detach();
1843            }
1844        }
1845    }
1846
1847    /// Insert a new field
1848    pub fn insert(&mut self, key: &str, value: &str) {
1849        let entry = Entry::new(key, value);
1850        let count = self.0.children_with_tokens().count();
1851        self.0.splice_children(count..count, vec![entry.0.into()]);
1852    }
1853
1854    /// Insert a comment line before this paragraph.
1855    ///
1856    /// The comment should not include the leading '#' character or newline,
1857    /// these will be added automatically.
1858    ///
1859    /// # Examples
1860    ///
1861    /// ```
1862    /// use deb822_lossless::Deb822;
1863    /// let mut d: Deb822 = vec![
1864    ///     vec![("Foo", "Bar")].into_iter().collect(),
1865    /// ]
1866    /// .into_iter()
1867    /// .collect();
1868    /// let mut para = d.paragraphs().next().unwrap();
1869    /// para.insert_comment_before("This is a comment");
1870    /// assert_eq!(d.to_string(), "# This is a comment\nFoo: Bar\n");
1871    /// ```
1872    pub fn insert_comment_before(&mut self, comment: &str) {
1873        use rowan::GreenNodeBuilder;
1874
1875        // Create an EMPTY_LINE node containing the comment tokens
1876        // This matches the structure used elsewhere in the parser
1877        let mut builder = GreenNodeBuilder::new();
1878        builder.start_node(EMPTY_LINE.into());
1879        builder.token(COMMENT.into(), &format!("# {}", comment));
1880        builder.token(NEWLINE.into(), "\n");
1881        builder.finish_node();
1882        let green = builder.finish();
1883
1884        // Convert to syntax node and insert before this paragraph
1885        let comment_node = SyntaxNode::new_root_mut(green);
1886
1887        let index = self.0.index();
1888        let parent = self.0.parent().expect("Paragraph must have a parent");
1889        parent.splice_children(index..index, vec![comment_node.into()]);
1890    }
1891
1892    /// Detect the indentation pattern used in this paragraph.
1893    ///
1894    /// This method analyzes existing multi-line fields to determine if they use:
1895    /// 1. A fixed indentation (all fields use the same number of spaces)
1896    /// 2. Field-name-length-based indentation (indent matches field name + ": ")
1897    ///
1898    /// If no pattern can be detected, defaults to field name length + 2.
1899    fn detect_indent_pattern(&self) -> IndentPattern {
1900        // Collect indentation data from existing multi-line fields
1901        let indent_data: Vec<(String, usize)> = self
1902            .entries()
1903            .filter_map(|entry| {
1904                let field_key = entry.key()?;
1905                let indent = entry.get_indent()?;
1906                Some((field_key, indent.len()))
1907            })
1908            .collect();
1909
1910        if indent_data.is_empty() {
1911            // No existing multi-line fields, default to field name length
1912            return IndentPattern::FieldNameLength;
1913        }
1914
1915        // Check if all fields use the same fixed indentation
1916        let first_indent_len = indent_data[0].1;
1917        let all_same = indent_data.iter().all(|(_, len)| *len == first_indent_len);
1918
1919        if all_same {
1920            // All fields use the same indentation - use that
1921            return IndentPattern::Fixed(first_indent_len);
1922        }
1923
1924        // Check if fields use field-name-length-based indentation
1925        let all_match_field_length = indent_data
1926            .iter()
1927            .all(|(field_key, indent_len)| *indent_len == field_key.len() + 2);
1928
1929        if all_match_field_length {
1930            // Fields use field-name-length-based indentation
1931            return IndentPattern::FieldNameLength;
1932        }
1933
1934        // Can't detect a clear pattern, default to field name length + 2
1935        IndentPattern::FieldNameLength
1936    }
1937
1938    /// Try to set a field in the paragraph, inserting at the appropriate location if new.
1939    ///
1940    /// # Errors
1941    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
1942    pub fn try_set(&mut self, key: &str, value: &str) -> Result<(), Error> {
1943        self.try_set_with_indent_pattern(key, value, None, None)
1944    }
1945
1946    /// Set a field in the paragraph, inserting at the appropriate location if new
1947    ///
1948    /// # Panics
1949    /// Panics if the value contains empty continuation lines (lines with only whitespace)
1950    pub fn set(&mut self, key: &str, value: &str) {
1951        self.try_set(key, value)
1952            .expect("Invalid value: empty continuation line")
1953    }
1954
1955    /// Set a field using a specific field ordering
1956    pub fn set_with_field_order(&mut self, key: &str, value: &str, field_order: &[&str]) {
1957        self.try_set_with_indent_pattern(key, value, None, Some(field_order))
1958            .expect("Invalid value: empty continuation line")
1959    }
1960
1961    /// Try to set a field with optional default indentation pattern and field ordering.
1962    ///
1963    /// This method allows setting a field while optionally specifying a default indentation pattern
1964    /// to use when the field doesn't already have multi-line indentation to preserve.
1965    /// If the field already exists and is multi-line, its existing indentation is preserved.
1966    ///
1967    /// # Arguments
1968    /// * `key` - The field name
1969    /// * `value` - The field value
1970    /// * `default_indent_pattern` - Optional default indentation pattern to use for new fields or
1971    ///   fields without existing multi-line indentation. If None, will preserve existing field's
1972    ///   indentation or auto-detect from other fields
1973    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
1974    ///
1975    /// # Errors
1976    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
1977    pub fn try_set_with_indent_pattern(
1978        &mut self,
1979        key: &str,
1980        value: &str,
1981        default_indent_pattern: Option<&IndentPattern>,
1982        field_order: Option<&[&str]>,
1983    ) -> Result<(), Error> {
1984        // Check if the field already exists and extract its formatting (case-insensitive)
1985        let existing_entry = self.entries().find(|entry| {
1986            entry
1987                .key()
1988                .as_deref()
1989                .is_some_and(|k| k.eq_ignore_ascii_case(key))
1990        });
1991
1992        // Determine indentation to use
1993        let indent = existing_entry
1994            .as_ref()
1995            .and_then(|entry| entry.get_indent())
1996            .unwrap_or_else(|| {
1997                // No existing indentation, use default pattern or auto-detect
1998                if let Some(pattern) = default_indent_pattern {
1999                    pattern.to_string(key)
2000                } else {
2001                    self.detect_indent_pattern().to_string(key)
2002                }
2003            });
2004
2005        let post_colon_ws = existing_entry
2006            .as_ref()
2007            .and_then(|entry| entry.get_post_colon_whitespace())
2008            .unwrap_or_else(|| " ".to_string());
2009
2010        // When replacing an existing field, preserve the original case of the field name
2011        let actual_key = existing_entry
2012            .as_ref()
2013            .and_then(|e| e.key())
2014            .unwrap_or_else(|| key.to_string());
2015
2016        let new_entry = Entry::try_with_formatting(&actual_key, value, &post_colon_ws, &indent)?;
2017
2018        // Check if the field already exists and replace it (case-insensitive)
2019        for entry in self.entries() {
2020            if entry
2021                .key()
2022                .as_deref()
2023                .is_some_and(|k| k.eq_ignore_ascii_case(key))
2024            {
2025                self.0.splice_children(
2026                    entry.0.index()..entry.0.index() + 1,
2027                    vec![new_entry.0.into()],
2028                );
2029                return Ok(());
2030            }
2031        }
2032
2033        // Insert new field
2034        if let Some(order) = field_order {
2035            let insertion_index = self.find_insertion_index(key, order);
2036            self.0
2037                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
2038        } else {
2039            // Insert at the end if no field order specified
2040            let insertion_index = self.0.children_with_tokens().count();
2041            self.0
2042                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
2043        }
2044        Ok(())
2045    }
2046
2047    /// Set a field with optional default indentation pattern and field ordering.
2048    ///
2049    /// This method allows setting a field while optionally specifying a default indentation pattern
2050    /// to use when the field doesn't already have multi-line indentation to preserve.
2051    /// If the field already exists and is multi-line, its existing indentation is preserved.
2052    ///
2053    /// # Arguments
2054    /// * `key` - The field name
2055    /// * `value` - The field value
2056    /// * `default_indent_pattern` - Optional default indentation pattern to use for new fields or
2057    ///   fields without existing multi-line indentation. If None, will preserve existing field's
2058    ///   indentation or auto-detect from other fields
2059    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
2060    ///
2061    /// # Panics
2062    /// Panics if the value contains empty continuation lines (lines with only whitespace)
2063    pub fn set_with_indent_pattern(
2064        &mut self,
2065        key: &str,
2066        value: &str,
2067        default_indent_pattern: Option<&IndentPattern>,
2068        field_order: Option<&[&str]>,
2069    ) {
2070        self.try_set_with_indent_pattern(key, value, default_indent_pattern, field_order)
2071            .expect("Invalid value: empty continuation line")
2072    }
2073
2074    /// Try to set a field, forcing a specific indentation pattern regardless of existing indentation.
2075    ///
2076    /// Unlike `try_set_with_indent_pattern`, this method does NOT preserve existing field indentation.
2077    /// It always applies the specified indentation pattern to the field.
2078    ///
2079    /// # Arguments
2080    /// * `key` - The field name
2081    /// * `value` - The field value
2082    /// * `indent_pattern` - The indentation pattern to use for this field
2083    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
2084    ///
2085    /// # Errors
2086    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
2087    pub fn try_set_with_forced_indent(
2088        &mut self,
2089        key: &str,
2090        value: &str,
2091        indent_pattern: &IndentPattern,
2092        field_order: Option<&[&str]>,
2093    ) -> Result<(), Error> {
2094        // Check if the field already exists (case-insensitive)
2095        let existing_entry = self.entries().find(|entry| {
2096            entry
2097                .key()
2098                .as_deref()
2099                .is_some_and(|k| k.eq_ignore_ascii_case(key))
2100        });
2101
2102        // Get post-colon whitespace from existing field, or default to single space
2103        let post_colon_ws = existing_entry
2104            .as_ref()
2105            .and_then(|entry| entry.get_post_colon_whitespace())
2106            .unwrap_or_else(|| " ".to_string());
2107
2108        // When replacing an existing field, preserve the original case of the field name
2109        let actual_key = existing_entry
2110            .as_ref()
2111            .and_then(|e| e.key())
2112            .unwrap_or_else(|| key.to_string());
2113
2114        // Force the indentation pattern
2115        let indent = indent_pattern.to_string(&actual_key);
2116        let new_entry = Entry::try_with_formatting(&actual_key, value, &post_colon_ws, &indent)?;
2117
2118        // Check if the field already exists and replace it (case-insensitive)
2119        for entry in self.entries() {
2120            if entry
2121                .key()
2122                .as_deref()
2123                .is_some_and(|k| k.eq_ignore_ascii_case(key))
2124            {
2125                self.0.splice_children(
2126                    entry.0.index()..entry.0.index() + 1,
2127                    vec![new_entry.0.into()],
2128                );
2129                return Ok(());
2130            }
2131        }
2132
2133        // Insert new field
2134        if let Some(order) = field_order {
2135            let insertion_index = self.find_insertion_index(key, order);
2136            self.0
2137                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
2138        } else {
2139            // Insert at the end if no field order specified
2140            let insertion_index = self.0.children_with_tokens().count();
2141            self.0
2142                .splice_children(insertion_index..insertion_index, vec![new_entry.0.into()]);
2143        }
2144        Ok(())
2145    }
2146
2147    /// Set a field, forcing a specific indentation pattern regardless of existing indentation.
2148    ///
2149    /// Unlike `set_with_indent_pattern`, this method does NOT preserve existing field indentation.
2150    /// It always applies the specified indentation pattern to the field.
2151    ///
2152    /// # Arguments
2153    /// * `key` - The field name
2154    /// * `value` - The field value
2155    /// * `indent_pattern` - The indentation pattern to use for this field
2156    /// * `field_order` - Optional field ordering for positioning the field. If None, inserts at end
2157    ///
2158    /// # Panics
2159    /// Panics if the value contains empty continuation lines (lines with only whitespace)
2160    pub fn set_with_forced_indent(
2161        &mut self,
2162        key: &str,
2163        value: &str,
2164        indent_pattern: &IndentPattern,
2165        field_order: Option<&[&str]>,
2166    ) {
2167        self.try_set_with_forced_indent(key, value, indent_pattern, field_order)
2168            .expect("Invalid value: empty continuation line")
2169    }
2170
2171    /// Change the indentation of an existing field without modifying its value.
2172    ///
2173    /// This method finds an existing field and reapplies it with a new indentation pattern,
2174    /// preserving the field's current value.
2175    ///
2176    /// # Arguments
2177    /// * `key` - The field name to update
2178    /// * `indent_pattern` - The new indentation pattern to apply
2179    ///
2180    /// # Returns
2181    /// Returns `Ok(true)` if the field was found and updated, `Ok(false)` if the field doesn't exist,
2182    /// or `Err` if there was an error (e.g., invalid value with empty continuation lines)
2183    ///
2184    /// # Errors
2185    /// Returns an error if the field value contains empty continuation lines (lines with only whitespace)
2186    pub fn change_field_indent(
2187        &mut self,
2188        key: &str,
2189        indent_pattern: &IndentPattern,
2190    ) -> Result<bool, Error> {
2191        // Check if the field exists (case-insensitive)
2192        let existing_entry = self.entries().find(|entry| {
2193            entry
2194                .key()
2195                .as_deref()
2196                .is_some_and(|k| k.eq_ignore_ascii_case(key))
2197        });
2198
2199        if let Some(entry) = existing_entry {
2200            let value = entry.value();
2201            let actual_key = entry.key().unwrap_or_else(|| key.to_string());
2202
2203            // Get post-colon whitespace from existing field
2204            let post_colon_ws = entry
2205                .get_post_colon_whitespace()
2206                .unwrap_or_else(|| " ".to_string());
2207
2208            // Apply the new indentation pattern
2209            let indent = indent_pattern.to_string(&actual_key);
2210            let new_entry =
2211                Entry::try_with_formatting(&actual_key, &value, &post_colon_ws, &indent)?;
2212
2213            // Replace the existing entry
2214            self.0.splice_children(
2215                entry.0.index()..entry.0.index() + 1,
2216                vec![new_entry.0.into()],
2217            );
2218            Ok(true)
2219        } else {
2220            Ok(false)
2221        }
2222    }
2223
2224    /// Find the appropriate insertion index for a new field based on field ordering
2225    fn find_insertion_index(&self, key: &str, field_order: &[&str]) -> usize {
2226        // Find position of the new field in the canonical order (case-insensitive)
2227        let new_field_position = field_order
2228            .iter()
2229            .position(|&field| field.eq_ignore_ascii_case(key));
2230
2231        let mut insertion_index = self.0.children_with_tokens().count();
2232
2233        // Find the right position based on canonical field order
2234        for (i, child) in self.0.children_with_tokens().enumerate() {
2235            if let Some(node) = child.as_node() {
2236                if let Some(entry) = Entry::cast(node.clone()) {
2237                    if let Some(existing_key) = entry.key() {
2238                        let existing_position = field_order
2239                            .iter()
2240                            .position(|&field| field.eq_ignore_ascii_case(&existing_key));
2241
2242                        match (new_field_position, existing_position) {
2243                            // Both fields are in the canonical order
2244                            (Some(new_pos), Some(existing_pos)) => {
2245                                if new_pos < existing_pos {
2246                                    insertion_index = i;
2247                                    break;
2248                                }
2249                            }
2250                            // New field is in canonical order, existing is not
2251                            (Some(_), None) => {
2252                                // Continue looking - unknown fields go after known ones
2253                            }
2254                            // New field is not in canonical order, existing is
2255                            (None, Some(_)) => {
2256                                // Continue until we find all known fields
2257                            }
2258                            // Neither field is in canonical order, maintain alphabetical
2259                            (None, None) => {
2260                                if key < existing_key.as_str() {
2261                                    insertion_index = i;
2262                                    break;
2263                                }
2264                            }
2265                        }
2266                    }
2267                }
2268            }
2269        }
2270
2271        // If we have a position in canonical order but haven't found where to insert yet,
2272        // we need to insert after all known fields that come before it
2273        if new_field_position.is_some() && insertion_index == self.0.children_with_tokens().count()
2274        {
2275            // Look for the position after the last known field that comes before our field
2276            let children: Vec<_> = self.0.children_with_tokens().enumerate().collect();
2277            for (i, child) in children.into_iter().rev() {
2278                if let Some(node) = child.as_node() {
2279                    if let Some(entry) = Entry::cast(node.clone()) {
2280                        if let Some(existing_key) = entry.key() {
2281                            if field_order
2282                                .iter()
2283                                .any(|&f| f.eq_ignore_ascii_case(&existing_key))
2284                            {
2285                                // Found a known field, insert after it
2286                                insertion_index = i + 1;
2287                                break;
2288                            }
2289                        }
2290                    }
2291                }
2292            }
2293        }
2294
2295        insertion_index
2296    }
2297
2298    /// Rename the given field in the paragraph.
2299    ///
2300    /// Field names are compared case-insensitively.
2301    pub fn rename(&mut self, old_key: &str, new_key: &str) -> bool {
2302        for entry in self.entries() {
2303            if entry
2304                .key()
2305                .as_deref()
2306                .is_some_and(|k| k.eq_ignore_ascii_case(old_key))
2307            {
2308                self.0.splice_children(
2309                    entry.0.index()..entry.0.index() + 1,
2310                    vec![Entry::new(new_key, entry.value().as_str()).0.into()],
2311                );
2312                return true;
2313            }
2314        }
2315        false
2316    }
2317}
2318
2319impl Default for Paragraph {
2320    fn default() -> Self {
2321        Self::new()
2322    }
2323}
2324
2325impl std::str::FromStr for Paragraph {
2326    type Err = ParseError;
2327
2328    fn from_str(text: &str) -> Result<Self, Self::Err> {
2329        let deb822 = Deb822::from_str(text)?;
2330
2331        let mut paragraphs = deb822.paragraphs();
2332
2333        paragraphs
2334            .next()
2335            .ok_or_else(|| ParseError(vec!["no paragraphs".to_string()]))
2336    }
2337}
2338
2339#[cfg(feature = "python-debian")]
2340impl<'py> pyo3::IntoPyObject<'py> for Paragraph {
2341    type Target = pyo3::PyAny;
2342    type Output = pyo3::Bound<'py, Self::Target>;
2343    type Error = pyo3::PyErr;
2344
2345    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
2346        use pyo3::prelude::*;
2347        let d = pyo3::types::PyDict::new(py);
2348        for (k, v) in self.items() {
2349            d.set_item(k, v)?;
2350        }
2351        let m = py.import("debian.deb822")?;
2352        let cls = m.getattr("Deb822")?;
2353        cls.call1((d,))
2354    }
2355}
2356
2357#[cfg(feature = "python-debian")]
2358impl<'py> pyo3::IntoPyObject<'py> for &Paragraph {
2359    type Target = pyo3::PyAny;
2360    type Output = pyo3::Bound<'py, Self::Target>;
2361    type Error = pyo3::PyErr;
2362
2363    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
2364        use pyo3::prelude::*;
2365        let d = pyo3::types::PyDict::new(py);
2366        for (k, v) in self.items() {
2367            d.set_item(k, v)?;
2368        }
2369        let m = py.import("debian.deb822")?;
2370        let cls = m.getattr("Deb822")?;
2371        cls.call1((d,))
2372    }
2373}
2374
2375#[cfg(feature = "python-debian")]
2376impl<'py> pyo3::FromPyObject<'_, 'py> for Paragraph {
2377    type Error = pyo3::PyErr;
2378
2379    fn extract(obj: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
2380        use pyo3::types::PyAnyMethods;
2381        let d = obj.call_method0("__str__")?.extract::<String>()?;
2382        Paragraph::from_str(&d)
2383            .map_err(|e| pyo3::exceptions::PyValueError::new_err((e.to_string(),)))
2384    }
2385}
2386
2387impl Entry {
2388    /// Create an independent snapshot of this Entry.
2389    ///
2390    /// This creates a new mutable tree that shares the same underlying immutable
2391    /// GreenNode data. Modifications to the original will not affect the snapshot
2392    /// and vice versa.
2393    ///
2394    /// This is more efficient than serializing and re-parsing because it reuses
2395    /// the GreenNode structure.
2396    pub fn snapshot(&self) -> Self {
2397        Entry(SyntaxNode::new_root_mut(self.0.green().into_owned()))
2398    }
2399
2400    /// Returns the text range of this entry in the source text.
2401    pub fn text_range(&self) -> rowan::TextRange {
2402        self.0.text_range()
2403    }
2404
2405    /// Returns the text range of the key (field name) in this entry.
2406    pub fn key_range(&self) -> Option<rowan::TextRange> {
2407        self.0
2408            .children_with_tokens()
2409            .filter_map(|it| it.into_token())
2410            .find(|it| it.kind() == KEY)
2411            .map(|it| it.text_range())
2412    }
2413
2414    /// Returns the text range of the colon separator in this entry.
2415    pub fn colon_range(&self) -> Option<rowan::TextRange> {
2416        self.0
2417            .children_with_tokens()
2418            .filter_map(|it| it.into_token())
2419            .find(|it| it.kind() == COLON)
2420            .map(|it| it.text_range())
2421    }
2422
2423    /// Returns the text range of the value portion (excluding the key and colon) in this entry.
2424    /// This includes all VALUE tokens and any continuation lines.
2425    pub fn value_range(&self) -> Option<rowan::TextRange> {
2426        let value_tokens: Vec<_> = self
2427            .0
2428            .children_with_tokens()
2429            .filter_map(|it| it.into_token())
2430            .filter(|it| it.kind() == VALUE)
2431            .collect();
2432
2433        if value_tokens.is_empty() {
2434            return None;
2435        }
2436
2437        let first = value_tokens.first().unwrap();
2438        let last = value_tokens.last().unwrap();
2439        Some(rowan::TextRange::new(
2440            first.text_range().start(),
2441            last.text_range().end(),
2442        ))
2443    }
2444
2445    /// Returns the text ranges of all individual value lines in this entry.
2446    /// Multi-line values will return multiple ranges.
2447    pub fn value_line_ranges(&self) -> Vec<rowan::TextRange> {
2448        self.0
2449            .children_with_tokens()
2450            .filter_map(|it| it.into_token())
2451            .filter(|it| it.kind() == VALUE)
2452            .map(|it| it.text_range())
2453            .collect()
2454    }
2455
2456    /// Create a new entry with the given key and value.
2457    pub fn new(key: &str, value: &str) -> Entry {
2458        Self::with_indentation(key, value, " ")
2459    }
2460
2461    /// Create a new entry with the given key, value, and custom indentation for continuation lines.
2462    ///
2463    /// # Arguments
2464    /// * `key` - The field name
2465    /// * `value` - The field value (may contain '\n' for multi-line values)
2466    /// * `indent` - The indentation string to use for continuation lines
2467    pub fn with_indentation(key: &str, value: &str, indent: &str) -> Entry {
2468        Entry::with_formatting(key, value, " ", indent)
2469    }
2470
2471    /// Try to create a new entry with specific formatting, validating the value.
2472    ///
2473    /// # Arguments
2474    /// * `key` - The field name
2475    /// * `value` - The field value (may contain '\n' for multi-line values)
2476    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2477    /// * `indent` - The indentation string to use for continuation lines
2478    ///
2479    /// # Errors
2480    /// Returns an error if the value contains empty continuation lines (lines with only whitespace)
2481    pub fn try_with_formatting(
2482        key: &str,
2483        value: &str,
2484        post_colon_ws: &str,
2485        indent: &str,
2486    ) -> Result<Entry, Error> {
2487        let mut builder = GreenNodeBuilder::new();
2488
2489        builder.start_node(ENTRY.into());
2490        builder.token(KEY.into(), key);
2491        builder.token(COLON.into(), ":");
2492
2493        // Add the post-colon whitespace token by token
2494        let mut i = 0;
2495        while i < post_colon_ws.len() {
2496            if post_colon_ws[i..].starts_with('\n') {
2497                builder.token(NEWLINE.into(), "\n");
2498                i += 1;
2499            } else {
2500                // Collect consecutive non-newline chars as WHITESPACE
2501                let start = i;
2502                while i < post_colon_ws.len() && !post_colon_ws[i..].starts_with('\n') {
2503                    i += post_colon_ws[i..].chars().next().unwrap().len_utf8();
2504                }
2505                builder.token(WHITESPACE.into(), &post_colon_ws[start..i]);
2506            }
2507        }
2508
2509        for (line_idx, line) in value.split('\n').enumerate() {
2510            if line_idx > 0 {
2511                // Validate that continuation lines are not empty or whitespace-only
2512                // According to Debian Policy, continuation lines must have content
2513                if line.trim().is_empty() {
2514                    return Err(Error::InvalidValue(format!(
2515                        "empty continuation line (line with only whitespace) at line {}",
2516                        line_idx + 1
2517                    )));
2518                }
2519                builder.token(INDENT.into(), indent);
2520            }
2521            builder.token(VALUE.into(), line);
2522            builder.token(NEWLINE.into(), "\n");
2523        }
2524        builder.finish_node();
2525        Ok(Entry(SyntaxNode::new_root_mut(builder.finish())))
2526    }
2527
2528    /// Create a new entry with specific formatting for post-colon whitespace and indentation.
2529    ///
2530    /// # Arguments
2531    /// * `key` - The field name
2532    /// * `value` - The field value (may contain '\n' for multi-line values)
2533    /// * `post_colon_ws` - The whitespace after the colon (e.g., " " or "\n ")
2534    /// * `indent` - The indentation string to use for continuation lines
2535    ///
2536    /// # Panics
2537    /// Panics if the value contains empty continuation lines (lines with only whitespace)
2538    pub fn with_formatting(key: &str, value: &str, post_colon_ws: &str, indent: &str) -> Entry {
2539        Self::try_with_formatting(key, value, post_colon_ws, indent)
2540            .expect("Invalid value: empty continuation line")
2541    }
2542
2543    #[must_use]
2544    /// Reformat this entry
2545    ///
2546    /// # Arguments
2547    /// * `indentation` - The indentation to use
2548    /// * `immediate_empty_line` - Whether multi-line values should always start with an empty line
2549    /// * `max_line_length_one_liner` - If set, then this is the max length of the value if it is
2550    ///   crammed into a "one-liner" value
2551    /// * `format_value` - If set, then this function will format the value according to the given
2552    ///   function
2553    ///
2554    /// # Returns
2555    /// The reformatted entry
2556    pub fn wrap_and_sort(
2557        &self,
2558        mut indentation: Indentation,
2559        immediate_empty_line: bool,
2560        max_line_length_one_liner: Option<usize>,
2561        format_value: Option<&dyn Fn(&str, &str) -> String>,
2562    ) -> Entry {
2563        let mut builder = GreenNodeBuilder::new();
2564
2565        let mut content = vec![];
2566        builder.start_node(ENTRY.into());
2567        for c in self.0.children_with_tokens() {
2568            let text = c.as_token().map(|t| t.text());
2569            match c.kind() {
2570                KEY => {
2571                    builder.token(KEY.into(), text.unwrap());
2572                    if indentation == Indentation::FieldNameLength {
2573                        indentation = Indentation::Spaces(text.unwrap().len() as u32);
2574                    }
2575                }
2576                COLON => {
2577                    builder.token(COLON.into(), ":");
2578                }
2579                INDENT => {
2580                    // Discard original whitespace
2581                }
2582                ERROR | COMMENT | VALUE | WHITESPACE | NEWLINE => {
2583                    content.push(c);
2584                }
2585                EMPTY_LINE | ENTRY | ROOT | PARAGRAPH => unreachable!(),
2586            }
2587        }
2588
2589        let indentation = if let crate::Indentation::Spaces(i) = indentation {
2590            i
2591        } else {
2592            1
2593        };
2594
2595        assert!(indentation > 0);
2596
2597        // Strip trailing whitespace and newlines
2598        while let Some(c) = content.last() {
2599            if c.kind() == NEWLINE || c.kind() == WHITESPACE {
2600                content.pop();
2601            } else {
2602                break;
2603            }
2604        }
2605
2606        // Reformat iff there is a format function and the value
2607        // has no errors or comments
2608        let tokens = if let Some(ref format_value) = format_value {
2609            if !content
2610                .iter()
2611                .any(|c| c.kind() == ERROR || c.kind() == COMMENT)
2612            {
2613                let concat = content
2614                    .iter()
2615                    .filter_map(|c| c.as_token().map(|t| t.text()))
2616                    .collect::<String>();
2617                let formatted = format_value(self.key().as_ref().unwrap(), &concat);
2618                crate::lex::lex_inline(&formatted)
2619                    .map(|(k, t)| (k, t.to_string()))
2620                    .collect::<Vec<_>>()
2621            } else {
2622                content
2623                    .into_iter()
2624                    .map(|n| n.into_token().unwrap())
2625                    .map(|i| (i.kind(), i.text().to_string()))
2626                    .collect::<Vec<_>>()
2627            }
2628        } else {
2629            content
2630                .into_iter()
2631                .map(|n| n.into_token().unwrap())
2632                .map(|i| (i.kind(), i.text().to_string()))
2633                .collect::<Vec<_>>()
2634        };
2635
2636        rebuild_value(
2637            &mut builder,
2638            tokens,
2639            self.key().map_or(0, |k| k.len()),
2640            indentation,
2641            immediate_empty_line,
2642            max_line_length_one_liner,
2643        );
2644
2645        builder.finish_node();
2646        Self(SyntaxNode::new_root_mut(builder.finish()))
2647    }
2648
2649    /// Returns the key of the entry.
2650    pub fn key(&self) -> Option<String> {
2651        self.0
2652            .children_with_tokens()
2653            .filter_map(|it| it.into_token())
2654            .find(|it| it.kind() == KEY)
2655            .map(|it| it.text().to_string())
2656    }
2657
2658    /// Returns the value of the entry.
2659    pub fn value(&self) -> String {
2660        let mut parts = self
2661            .0
2662            .children_with_tokens()
2663            .filter_map(|it| it.into_token())
2664            .filter(|it| it.kind() == VALUE)
2665            .map(|it| it.text().to_string());
2666
2667        match parts.next() {
2668            None => String::new(),
2669            Some(first) => {
2670                let mut result = first;
2671                for part in parts {
2672                    result.push('\n');
2673                    result.push_str(&part);
2674                }
2675                result
2676            }
2677        }
2678    }
2679
2680    /// Returns the indentation string used for continuation lines in this entry.
2681    /// Returns None if the entry has no continuation lines.
2682    fn get_indent(&self) -> Option<String> {
2683        self.0
2684            .children_with_tokens()
2685            .filter_map(|it| it.into_token())
2686            .find(|it| it.kind() == INDENT)
2687            .map(|it| it.text().to_string())
2688    }
2689
2690    /// Returns the whitespace immediately after the colon in this entry.
2691    /// This includes WHITESPACE, NEWLINE, and INDENT tokens up to the first VALUE token.
2692    /// Returns None if there is no whitespace (which would be malformed).
2693    fn get_post_colon_whitespace(&self) -> Option<String> {
2694        let mut found_colon = false;
2695        let mut whitespace = String::new();
2696
2697        for token in self
2698            .0
2699            .children_with_tokens()
2700            .filter_map(|it| it.into_token())
2701        {
2702            if token.kind() == COLON {
2703                found_colon = true;
2704                continue;
2705            }
2706
2707            if found_colon {
2708                if token.kind() == WHITESPACE || token.kind() == NEWLINE || token.kind() == INDENT {
2709                    whitespace.push_str(token.text());
2710                } else {
2711                    // We've reached a non-whitespace token, stop collecting
2712                    break;
2713                }
2714            }
2715        }
2716
2717        if whitespace.is_empty() {
2718            None
2719        } else {
2720            Some(whitespace)
2721        }
2722    }
2723
2724    /// Normalize the spacing around the field separator (colon) in place.
2725    ///
2726    /// This ensures that there is exactly one space after the colon and before the value.
2727    /// This is a lossless operation that preserves the field name and value content,
2728    /// but normalizes the whitespace formatting.
2729    ///
2730    /// # Examples
2731    ///
2732    /// ```
2733    /// use deb822_lossless::Deb822;
2734    /// use std::str::FromStr;
2735    ///
2736    /// // Parse an entry with extra spacing after the colon
2737    /// let input = "Field:    value\n";
2738    /// let mut deb822 = Deb822::from_str(input).unwrap();
2739    /// let mut para = deb822.paragraphs().next().unwrap();
2740    ///
2741    /// para.normalize_field_spacing();
2742    /// assert_eq!(para.get("Field").as_deref(), Some("value"));
2743    /// ```
2744    pub fn normalize_field_spacing(&mut self) -> bool {
2745        use rowan::GreenNodeBuilder;
2746
2747        // Store the original text for comparison
2748        let original_text = self.0.text().to_string();
2749
2750        // Build normalized entry
2751        let mut builder = GreenNodeBuilder::new();
2752        builder.start_node(ENTRY.into());
2753
2754        let mut seen_colon = false;
2755        let mut skip_whitespace = false;
2756
2757        for child in self.0.children_with_tokens() {
2758            match child.kind() {
2759                KEY => {
2760                    builder.token(KEY.into(), child.as_token().unwrap().text());
2761                }
2762                COLON => {
2763                    builder.token(COLON.into(), ":");
2764                    seen_colon = true;
2765                    skip_whitespace = true;
2766                }
2767                WHITESPACE if skip_whitespace => {
2768                    // Skip existing whitespace after colon
2769                    continue;
2770                }
2771                VALUE if skip_whitespace => {
2772                    // Add exactly one space before the first value token
2773                    builder.token(WHITESPACE.into(), " ");
2774                    builder.token(VALUE.into(), child.as_token().unwrap().text());
2775                    skip_whitespace = false;
2776                }
2777                NEWLINE if skip_whitespace && seen_colon => {
2778                    // Empty value case (e.g., "Field:\n" or "Field:  \n")
2779                    // Normalize to no trailing space - just output newline
2780                    builder.token(NEWLINE.into(), "\n");
2781                    skip_whitespace = false;
2782                }
2783                _ => {
2784                    // Copy all other tokens as-is
2785                    if let Some(token) = child.as_token() {
2786                        builder.token(token.kind().into(), token.text());
2787                    }
2788                }
2789            }
2790        }
2791
2792        builder.finish_node();
2793        let normalized_green = builder.finish();
2794        let normalized = SyntaxNode::new_root_mut(normalized_green);
2795
2796        // Check if normalization made any changes
2797        let changed = original_text != normalized.text().to_string();
2798
2799        if changed {
2800            // Replace this entry in place
2801            if let Some(parent) = self.0.parent() {
2802                let index = self.0.index();
2803                parent.splice_children(index..index + 1, vec![normalized.into()]);
2804            }
2805        }
2806
2807        changed
2808    }
2809
2810    /// Detach this entry from the paragraph.
2811    pub fn detach(&mut self) {
2812        self.0.detach();
2813    }
2814}
2815
2816impl FromStr for Deb822 {
2817    type Err = ParseError;
2818
2819    fn from_str(s: &str) -> Result<Self, Self::Err> {
2820        Deb822::parse(s).to_result()
2821    }
2822}
2823
2824#[test]
2825fn test_parse_simple() {
2826    const CONTROLV1: &str = r#"Source: foo
2827Maintainer: Foo Bar <foo@example.com>
2828Section: net
2829
2830# This is a comment
2831
2832Package: foo
2833Architecture: all
2834Depends:
2835 bar,
2836 blah
2837Description: This is a description
2838 And it is
2839 .
2840 multiple
2841 lines
2842"#;
2843    let parsed = parse(CONTROLV1);
2844    let node = parsed.syntax();
2845    assert_eq!(
2846        format!("{:#?}", node),
2847        r###"ROOT@0..203
2848  PARAGRAPH@0..63
2849    ENTRY@0..12
2850      KEY@0..6 "Source"
2851      COLON@6..7 ":"
2852      WHITESPACE@7..8 " "
2853      VALUE@8..11 "foo"
2854      NEWLINE@11..12 "\n"
2855    ENTRY@12..50
2856      KEY@12..22 "Maintainer"
2857      COLON@22..23 ":"
2858      WHITESPACE@23..24 " "
2859      VALUE@24..49 "Foo Bar <foo@example. ..."
2860      NEWLINE@49..50 "\n"
2861    ENTRY@50..63
2862      KEY@50..57 "Section"
2863      COLON@57..58 ":"
2864      WHITESPACE@58..59 " "
2865      VALUE@59..62 "net"
2866      NEWLINE@62..63 "\n"
2867  EMPTY_LINE@63..64
2868    NEWLINE@63..64 "\n"
2869  EMPTY_LINE@64..84
2870    COMMENT@64..83 "# This is a comment"
2871    NEWLINE@83..84 "\n"
2872  EMPTY_LINE@84..85
2873    NEWLINE@84..85 "\n"
2874  PARAGRAPH@85..203
2875    ENTRY@85..98
2876      KEY@85..92 "Package"
2877      COLON@92..93 ":"
2878      WHITESPACE@93..94 " "
2879      VALUE@94..97 "foo"
2880      NEWLINE@97..98 "\n"
2881    ENTRY@98..116
2882      KEY@98..110 "Architecture"
2883      COLON@110..111 ":"
2884      WHITESPACE@111..112 " "
2885      VALUE@112..115 "all"
2886      NEWLINE@115..116 "\n"
2887    ENTRY@116..137
2888      KEY@116..123 "Depends"
2889      COLON@123..124 ":"
2890      NEWLINE@124..125 "\n"
2891      INDENT@125..126 " "
2892      VALUE@126..130 "bar,"
2893      NEWLINE@130..131 "\n"
2894      INDENT@131..132 " "
2895      VALUE@132..136 "blah"
2896      NEWLINE@136..137 "\n"
2897    ENTRY@137..203
2898      KEY@137..148 "Description"
2899      COLON@148..149 ":"
2900      WHITESPACE@149..150 " "
2901      VALUE@150..171 "This is a description"
2902      NEWLINE@171..172 "\n"
2903      INDENT@172..173 " "
2904      VALUE@173..182 "And it is"
2905      NEWLINE@182..183 "\n"
2906      INDENT@183..184 " "
2907      VALUE@184..185 "."
2908      NEWLINE@185..186 "\n"
2909      INDENT@186..187 " "
2910      VALUE@187..195 "multiple"
2911      NEWLINE@195..196 "\n"
2912      INDENT@196..197 " "
2913      VALUE@197..202 "lines"
2914      NEWLINE@202..203 "\n"
2915"###
2916    );
2917    assert_eq!(parsed.errors, Vec::<String>::new());
2918
2919    let root = parsed.root_mut();
2920    assert_eq!(root.paragraphs().count(), 2);
2921    let source = root.paragraphs().next().unwrap();
2922    assert_eq!(
2923        source.keys().collect::<Vec<_>>(),
2924        vec!["Source", "Maintainer", "Section"]
2925    );
2926    assert_eq!(source.get("Source").as_deref(), Some("foo"));
2927    assert_eq!(
2928        source.get("Maintainer").as_deref(),
2929        Some("Foo Bar <foo@example.com>")
2930    );
2931    assert_eq!(source.get("Section").as_deref(), Some("net"));
2932    assert_eq!(
2933        source.items().collect::<Vec<_>>(),
2934        vec![
2935            ("Source".into(), "foo".into()),
2936            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
2937            ("Section".into(), "net".into()),
2938        ]
2939    );
2940
2941    let binary = root.paragraphs().nth(1).unwrap();
2942    assert_eq!(
2943        binary.keys().collect::<Vec<_>>(),
2944        vec!["Package", "Architecture", "Depends", "Description"]
2945    );
2946    assert_eq!(binary.get("Package").as_deref(), Some("foo"));
2947    assert_eq!(binary.get("Architecture").as_deref(), Some("all"));
2948    assert_eq!(binary.get("Depends").as_deref(), Some("bar,\nblah"));
2949    assert_eq!(
2950        binary.get("Description").as_deref(),
2951        Some("This is a description\nAnd it is\n.\nmultiple\nlines")
2952    );
2953
2954    assert_eq!(node.text(), CONTROLV1);
2955}
2956
2957#[test]
2958fn test_with_trailing_whitespace() {
2959    const CONTROLV1: &str = r#"Source: foo
2960Maintainer: Foo Bar <foo@example.com>
2961
2962
2963"#;
2964    let parsed = parse(CONTROLV1);
2965    let node = parsed.syntax();
2966    assert_eq!(
2967        format!("{:#?}", node),
2968        r###"ROOT@0..52
2969  PARAGRAPH@0..50
2970    ENTRY@0..12
2971      KEY@0..6 "Source"
2972      COLON@6..7 ":"
2973      WHITESPACE@7..8 " "
2974      VALUE@8..11 "foo"
2975      NEWLINE@11..12 "\n"
2976    ENTRY@12..50
2977      KEY@12..22 "Maintainer"
2978      COLON@22..23 ":"
2979      WHITESPACE@23..24 " "
2980      VALUE@24..49 "Foo Bar <foo@example. ..."
2981      NEWLINE@49..50 "\n"
2982  EMPTY_LINE@50..51
2983    NEWLINE@50..51 "\n"
2984  EMPTY_LINE@51..52
2985    NEWLINE@51..52 "\n"
2986"###
2987    );
2988    assert_eq!(parsed.errors, Vec::<String>::new());
2989
2990    let root = parsed.root_mut();
2991    assert_eq!(root.paragraphs().count(), 1);
2992    let source = root.paragraphs().next().unwrap();
2993    assert_eq!(
2994        source.items().collect::<Vec<_>>(),
2995        vec![
2996            ("Source".into(), "foo".into()),
2997            ("Maintainer".into(), "Foo Bar <foo@example.com>".into()),
2998        ]
2999    );
3000}
3001
3002fn rebuild_value(
3003    builder: &mut GreenNodeBuilder,
3004    mut tokens: Vec<(SyntaxKind, String)>,
3005    key_len: usize,
3006    indentation: u32,
3007    immediate_empty_line: bool,
3008    max_line_length_one_liner: Option<usize>,
3009) {
3010    let first_line_len = tokens
3011        .iter()
3012        .take_while(|(k, _t)| *k != NEWLINE)
3013        .map(|(_k, t)| t.len())
3014        .sum::<usize>() + key_len + 2 /* ": " */;
3015
3016    let has_newline = tokens.iter().any(|(k, _t)| *k == NEWLINE);
3017
3018    let mut last_was_newline = false;
3019    if max_line_length_one_liner
3020        .map(|mll| first_line_len <= mll)
3021        .unwrap_or(false)
3022        && !has_newline
3023    {
3024        // Just copy tokens if the value fits into one line
3025        for (k, t) in tokens {
3026            builder.token(k.into(), &t);
3027        }
3028    } else {
3029        // Insert a leading newline if the value is multi-line and immediate_empty_line is set
3030        if immediate_empty_line && has_newline {
3031            builder.token(NEWLINE.into(), "\n");
3032            last_was_newline = true;
3033        } else {
3034            builder.token(WHITESPACE.into(), " ");
3035        }
3036        // Strip leading whitespace and newlines
3037        let mut start_idx = 0;
3038        while start_idx < tokens.len() {
3039            if tokens[start_idx].0 == NEWLINE || tokens[start_idx].0 == WHITESPACE {
3040                start_idx += 1;
3041            } else {
3042                break;
3043            }
3044        }
3045        tokens.drain(..start_idx);
3046        // Pre-allocate indentation string to avoid repeated allocations
3047        let indent_str = " ".repeat(indentation as usize);
3048        for (k, t) in tokens {
3049            if last_was_newline {
3050                builder.token(INDENT.into(), &indent_str);
3051            }
3052            builder.token(k.into(), &t);
3053            last_was_newline = k == NEWLINE;
3054        }
3055    }
3056
3057    if !last_was_newline {
3058        builder.token(NEWLINE.into(), "\n");
3059    }
3060}
3061
3062#[cfg(test)]
3063mod tests {
3064    use super::*;
3065    #[test]
3066    fn test_parse() {
3067        let d: super::Deb822 = r#"Source: foo
3068Maintainer: Foo Bar <jelmer@jelmer.uk>
3069Section: net
3070
3071Package: foo
3072Architecture: all
3073Depends: libc6
3074Description: This is a description
3075 With details
3076"#
3077        .parse()
3078        .unwrap();
3079        let mut ps = d.paragraphs();
3080        let p = ps.next().unwrap();
3081
3082        assert_eq!(p.get("Source").as_deref(), Some("foo"));
3083        assert_eq!(
3084            p.get("Maintainer").as_deref(),
3085            Some("Foo Bar <jelmer@jelmer.uk>")
3086        );
3087        assert_eq!(p.get("Section").as_deref(), Some("net"));
3088
3089        let b = ps.next().unwrap();
3090        assert_eq!(b.get("Package").as_deref(), Some("foo"));
3091    }
3092
3093    #[test]
3094    fn test_after_multi_line() {
3095        let d: super::Deb822 = r#"Source: golang-github-blah-blah
3096Section: devel
3097Priority: optional
3098Standards-Version: 4.2.0
3099Maintainer: Some Maintainer <example@example.com>
3100Build-Depends: debhelper (>= 11~),
3101               dh-golang,
3102               golang-any
3103Homepage: https://github.com/j-keck/arping
3104"#
3105        .parse()
3106        .unwrap();
3107        let mut ps = d.paragraphs();
3108        let p = ps.next().unwrap();
3109        assert_eq!(p.get("Source").as_deref(), Some("golang-github-blah-blah"));
3110        assert_eq!(p.get("Section").as_deref(), Some("devel"));
3111        assert_eq!(p.get("Priority").as_deref(), Some("optional"));
3112        assert_eq!(p.get("Standards-Version").as_deref(), Some("4.2.0"));
3113        assert_eq!(
3114            p.get("Maintainer").as_deref(),
3115            Some("Some Maintainer <example@example.com>")
3116        );
3117        assert_eq!(
3118            p.get("Build-Depends").as_deref(),
3119            Some("debhelper (>= 11~),\ndh-golang,\ngolang-any")
3120        );
3121        assert_eq!(
3122            p.get("Homepage").as_deref(),
3123            Some("https://github.com/j-keck/arping")
3124        );
3125    }
3126
3127    #[test]
3128    fn test_remove_field() {
3129        let d: super::Deb822 = r#"Source: foo
3130# Comment
3131Maintainer: Foo Bar <jelmer@jelmer.uk>
3132Section: net
3133
3134Package: foo
3135Architecture: all
3136Depends: libc6
3137Description: This is a description
3138 With details
3139"#
3140        .parse()
3141        .unwrap();
3142        let mut ps = d.paragraphs();
3143        let mut p = ps.next().unwrap();
3144        p.set("Foo", "Bar");
3145        p.remove("Section");
3146        p.remove("Nonexistent");
3147        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3148        assert_eq!(
3149            p.to_string(),
3150            r#"Source: foo
3151# Comment
3152Maintainer: Foo Bar <jelmer@jelmer.uk>
3153Foo: Bar
3154"#
3155        );
3156    }
3157
3158    #[test]
3159    fn test_rename_field() {
3160        let d: super::Deb822 = r#"Source: foo
3161Vcs-Browser: https://salsa.debian.org/debian/foo
3162"#
3163        .parse()
3164        .unwrap();
3165        let mut ps = d.paragraphs();
3166        let mut p = ps.next().unwrap();
3167        assert!(p.rename("Vcs-Browser", "Homepage"));
3168        assert_eq!(
3169            p.to_string(),
3170            r#"Source: foo
3171Homepage: https://salsa.debian.org/debian/foo
3172"#
3173        );
3174
3175        assert_eq!(
3176            p.get("Homepage").as_deref(),
3177            Some("https://salsa.debian.org/debian/foo")
3178        );
3179        assert_eq!(p.get("Vcs-Browser").as_deref(), None);
3180
3181        // Nonexistent field
3182        assert!(!p.rename("Nonexistent", "Homepage"));
3183    }
3184
3185    #[test]
3186    fn test_set_field() {
3187        let d: super::Deb822 = r#"Source: foo
3188Maintainer: Foo Bar <joe@example.com>
3189"#
3190        .parse()
3191        .unwrap();
3192        let mut ps = d.paragraphs();
3193        let mut p = ps.next().unwrap();
3194        p.set("Maintainer", "Somebody Else <jane@example.com>");
3195        assert_eq!(
3196            p.get("Maintainer").as_deref(),
3197            Some("Somebody Else <jane@example.com>")
3198        );
3199        assert_eq!(
3200            p.to_string(),
3201            r#"Source: foo
3202Maintainer: Somebody Else <jane@example.com>
3203"#
3204        );
3205    }
3206
3207    #[test]
3208    fn test_set_new_field() {
3209        let d: super::Deb822 = r#"Source: foo
3210"#
3211        .parse()
3212        .unwrap();
3213        let mut ps = d.paragraphs();
3214        let mut p = ps.next().unwrap();
3215        p.set("Maintainer", "Somebody <joe@example.com>");
3216        assert_eq!(
3217            p.get("Maintainer").as_deref(),
3218            Some("Somebody <joe@example.com>")
3219        );
3220        assert_eq!(
3221            p.to_string(),
3222            r#"Source: foo
3223Maintainer: Somebody <joe@example.com>
3224"#
3225        );
3226    }
3227
3228    #[test]
3229    fn test_add_paragraph() {
3230        let mut d = super::Deb822::new();
3231        let mut p = d.add_paragraph();
3232        p.set("Foo", "Bar");
3233        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3234        assert_eq!(
3235            p.to_string(),
3236            r#"Foo: Bar
3237"#
3238        );
3239        assert_eq!(
3240            d.to_string(),
3241            r#"Foo: Bar
3242"#
3243        );
3244
3245        let mut p = d.add_paragraph();
3246        p.set("Foo", "Blah");
3247        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3248        assert_eq!(
3249            d.to_string(),
3250            r#"Foo: Bar
3251
3252Foo: Blah
3253"#
3254        );
3255    }
3256
3257    #[test]
3258    fn test_crud_paragraph() {
3259        let mut d = super::Deb822::new();
3260        let mut p = d.insert_paragraph(0);
3261        p.set("Foo", "Bar");
3262        assert_eq!(p.get("Foo").as_deref(), Some("Bar"));
3263        assert_eq!(
3264            d.to_string(),
3265            r#"Foo: Bar
3266"#
3267        );
3268
3269        // test prepend
3270        let mut p = d.insert_paragraph(0);
3271        p.set("Foo", "Blah");
3272        assert_eq!(p.get("Foo").as_deref(), Some("Blah"));
3273        assert_eq!(
3274            d.to_string(),
3275            r#"Foo: Blah
3276
3277Foo: Bar
3278"#
3279        );
3280
3281        // test delete
3282        d.remove_paragraph(1);
3283        assert_eq!(d.to_string(), "Foo: Blah\n\n");
3284
3285        // test update again
3286        p.set("Foo", "Baz");
3287        assert_eq!(d.to_string(), "Foo: Baz\n\n");
3288
3289        // test delete again
3290        d.remove_paragraph(0);
3291        assert_eq!(d.to_string(), "");
3292    }
3293
3294    #[test]
3295    fn test_swap_paragraphs() {
3296        // Test basic swap
3297        let mut d: super::Deb822 = vec![
3298            vec![("Foo", "Bar")].into_iter().collect(),
3299            vec![("A", "B")].into_iter().collect(),
3300            vec![("X", "Y")].into_iter().collect(),
3301        ]
3302        .into_iter()
3303        .collect();
3304
3305        d.swap_paragraphs(0, 2);
3306        assert_eq!(d.to_string(), "X: Y\n\nA: B\n\nFoo: Bar\n");
3307
3308        // Swap back
3309        d.swap_paragraphs(0, 2);
3310        assert_eq!(d.to_string(), "Foo: Bar\n\nA: B\n\nX: Y\n");
3311
3312        // Swap adjacent paragraphs
3313        d.swap_paragraphs(0, 1);
3314        assert_eq!(d.to_string(), "A: B\n\nFoo: Bar\n\nX: Y\n");
3315
3316        // Swap with same index should be no-op
3317        let before = d.to_string();
3318        d.swap_paragraphs(1, 1);
3319        assert_eq!(d.to_string(), before);
3320    }
3321
3322    #[test]
3323    fn test_swap_paragraphs_preserves_content() {
3324        // Test that field content is preserved
3325        let mut d: super::Deb822 = vec![
3326            vec![("Field1", "Value1"), ("Field2", "Value2")]
3327                .into_iter()
3328                .collect(),
3329            vec![("FieldA", "ValueA"), ("FieldB", "ValueB")]
3330                .into_iter()
3331                .collect(),
3332        ]
3333        .into_iter()
3334        .collect();
3335
3336        d.swap_paragraphs(0, 1);
3337
3338        let mut paras = d.paragraphs();
3339        let p1 = paras.next().unwrap();
3340        assert_eq!(p1.get("FieldA").as_deref(), Some("ValueA"));
3341        assert_eq!(p1.get("FieldB").as_deref(), Some("ValueB"));
3342
3343        let p2 = paras.next().unwrap();
3344        assert_eq!(p2.get("Field1").as_deref(), Some("Value1"));
3345        assert_eq!(p2.get("Field2").as_deref(), Some("Value2"));
3346    }
3347
3348    #[test]
3349    #[should_panic(expected = "out of bounds")]
3350    fn test_swap_paragraphs_out_of_bounds() {
3351        let mut d: super::Deb822 = vec![
3352            vec![("Foo", "Bar")].into_iter().collect(),
3353            vec![("A", "B")].into_iter().collect(),
3354        ]
3355        .into_iter()
3356        .collect();
3357
3358        d.swap_paragraphs(0, 5);
3359    }
3360
3361    #[test]
3362    fn test_multiline_entry() {
3363        use super::SyntaxKind::*;
3364        use rowan::ast::AstNode;
3365
3366        let entry = super::Entry::new("foo", "bar\nbaz");
3367        let tokens: Vec<_> = entry
3368            .syntax()
3369            .descendants_with_tokens()
3370            .filter_map(|tok| tok.into_token())
3371            .collect();
3372
3373        assert_eq!("foo: bar\n baz\n", entry.to_string());
3374        assert_eq!("bar\nbaz", entry.value());
3375
3376        assert_eq!(
3377            vec![
3378                (KEY, "foo"),
3379                (COLON, ":"),
3380                (WHITESPACE, " "),
3381                (VALUE, "bar"),
3382                (NEWLINE, "\n"),
3383                (INDENT, " "),
3384                (VALUE, "baz"),
3385                (NEWLINE, "\n"),
3386            ],
3387            tokens
3388                .iter()
3389                .map(|token| (token.kind(), token.text()))
3390                .collect::<Vec<_>>()
3391        );
3392    }
3393
3394    #[test]
3395    fn test_apt_entry() {
3396        let text = r#"Package: cvsd
3397Binary: cvsd
3398Version: 1.0.24
3399Maintainer: Arthur de Jong <adejong@debian.org>
3400Build-Depends: debhelper (>= 9), po-debconf
3401Architecture: any
3402Standards-Version: 3.9.3
3403Format: 3.0 (native)
3404Files:
3405 b7a7d67a02974c52c408fdb5e118406d 890 cvsd_1.0.24.dsc
3406 b73ee40774c3086cb8490cdbb96ac883 258139 cvsd_1.0.24.tar.gz
3407Vcs-Browser: http://arthurdejong.org/viewvc/cvsd/
3408Vcs-Cvs: :pserver:anonymous@arthurdejong.org:/arthur/
3409Checksums-Sha256:
3410 a7bb7a3aacee19cd14ce5c26cb86e348b1608e6f1f6e97c6ea7c58efa440ac43 890 cvsd_1.0.24.dsc
3411 46bc517760c1070ae408693b89603986b53e6f068ae6bdc744e2e830e46b8cba 258139 cvsd_1.0.24.tar.gz
3412Homepage: http://arthurdejong.org/cvsd/
3413Package-List:
3414 cvsd deb vcs optional
3415Directory: pool/main/c/cvsd
3416Priority: source
3417Section: vcs
3418
3419"#;
3420        let d: super::Deb822 = text.parse().unwrap();
3421        let p = d.paragraphs().next().unwrap();
3422        assert_eq!(p.get("Binary").as_deref(), Some("cvsd"));
3423        assert_eq!(p.get("Version").as_deref(), Some("1.0.24"));
3424        assert_eq!(
3425            p.get("Maintainer").as_deref(),
3426            Some("Arthur de Jong <adejong@debian.org>")
3427        );
3428    }
3429
3430    #[test]
3431    fn test_format() {
3432        let d: super::Deb822 = r#"Source: foo
3433Maintainer: Foo Bar <foo@example.com>
3434Section:      net
3435Blah: blah  # comment
3436Multi-Line:
3437  Ahoi!
3438     Matey!
3439
3440"#
3441        .parse()
3442        .unwrap();
3443        let mut ps = d.paragraphs();
3444        let p = ps.next().unwrap();
3445        let result = p.wrap_and_sort(
3446            crate::Indentation::FieldNameLength,
3447            false,
3448            None,
3449            None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3450            None,
3451        );
3452        assert_eq!(
3453            result.to_string(),
3454            r#"Source: foo
3455Maintainer: Foo Bar <foo@example.com>
3456Section: net
3457Blah: blah  # comment
3458Multi-Line: Ahoi!
3459          Matey!
3460"#
3461        );
3462    }
3463
3464    #[test]
3465    fn test_format_sort_paragraphs() {
3466        let d: super::Deb822 = r#"Source: foo
3467Maintainer: Foo Bar <foo@example.com>
3468
3469# This is a comment
3470Source: bar
3471Maintainer: Bar Foo <bar@example.com>
3472
3473"#
3474        .parse()
3475        .unwrap();
3476        let result = d.wrap_and_sort(
3477            Some(&|a: &super::Paragraph, b: &super::Paragraph| {
3478                a.get("Source").cmp(&b.get("Source"))
3479            }),
3480            Some(&|p| {
3481                p.wrap_and_sort(
3482                    crate::Indentation::FieldNameLength,
3483                    false,
3484                    None,
3485                    None::<&dyn Fn(&super::Entry, &super::Entry) -> std::cmp::Ordering>,
3486                    None,
3487                )
3488            }),
3489        );
3490        assert_eq!(
3491            result.to_string(),
3492            r#"# This is a comment
3493Source: bar
3494Maintainer: Bar Foo <bar@example.com>
3495
3496Source: foo
3497Maintainer: Foo Bar <foo@example.com>
3498"#,
3499        );
3500    }
3501
3502    #[test]
3503    fn test_format_sort_fields() {
3504        let d: super::Deb822 = r#"Source: foo
3505Maintainer: Foo Bar <foo@example.com>
3506Build-Depends: debhelper (>= 9), po-debconf
3507Homepage: https://example.com/
3508
3509"#
3510        .parse()
3511        .unwrap();
3512        let result = d.wrap_and_sort(
3513            None,
3514            Some(&|p: &super::Paragraph| -> super::Paragraph {
3515                p.wrap_and_sort(
3516                    crate::Indentation::FieldNameLength,
3517                    false,
3518                    None,
3519                    Some(&|a: &super::Entry, b: &super::Entry| a.key().cmp(&b.key())),
3520                    None,
3521                )
3522            }),
3523        );
3524        assert_eq!(
3525            result.to_string(),
3526            r#"Build-Depends: debhelper (>= 9), po-debconf
3527Homepage: https://example.com/
3528Maintainer: Foo Bar <foo@example.com>
3529Source: foo
3530"#
3531        );
3532    }
3533
3534    #[test]
3535    fn test_para_from_iter() {
3536        let p: super::Paragraph = vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect();
3537        assert_eq!(
3538            p.to_string(),
3539            r#"Foo: Bar
3540Baz: Qux
3541"#
3542        );
3543
3544        let p: super::Paragraph = vec![
3545            ("Foo".to_string(), "Bar".to_string()),
3546            ("Baz".to_string(), "Qux".to_string()),
3547        ]
3548        .into_iter()
3549        .collect();
3550
3551        assert_eq!(
3552            p.to_string(),
3553            r#"Foo: Bar
3554Baz: Qux
3555"#
3556        );
3557    }
3558
3559    #[test]
3560    fn test_deb822_from_iter() {
3561        let d: super::Deb822 = vec![
3562            vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
3563            vec![("A", "B"), ("C", "D")].into_iter().collect(),
3564        ]
3565        .into_iter()
3566        .collect();
3567        assert_eq!(
3568            d.to_string(),
3569            r#"Foo: Bar
3570Baz: Qux
3571
3572A: B
3573C: D
3574"#
3575        );
3576    }
3577
3578    #[test]
3579    fn test_format_parse_error() {
3580        assert_eq!(ParseError(vec!["foo".to_string()]).to_string(), "foo\n");
3581    }
3582
3583    #[test]
3584    fn test_set_with_field_order() {
3585        let mut p = super::Paragraph::new();
3586        let custom_order = &["Foo", "Bar", "Baz"];
3587
3588        p.set_with_field_order("Baz", "3", custom_order);
3589        p.set_with_field_order("Foo", "1", custom_order);
3590        p.set_with_field_order("Bar", "2", custom_order);
3591        p.set_with_field_order("Unknown", "4", custom_order);
3592
3593        let keys: Vec<_> = p.keys().collect();
3594        assert_eq!(keys[0], "Foo");
3595        assert_eq!(keys[1], "Bar");
3596        assert_eq!(keys[2], "Baz");
3597        assert_eq!(keys[3], "Unknown");
3598    }
3599
3600    #[test]
3601    fn test_positioned_parse_error() {
3602        let error = PositionedParseError {
3603            message: "test error".to_string(),
3604            range: rowan::TextRange::new(rowan::TextSize::from(5), rowan::TextSize::from(10)),
3605            code: Some("test_code".to_string()),
3606        };
3607        assert_eq!(error.to_string(), "test error");
3608        assert_eq!(error.range.start(), rowan::TextSize::from(5));
3609        assert_eq!(error.range.end(), rowan::TextSize::from(10));
3610        assert_eq!(error.code, Some("test_code".to_string()));
3611    }
3612
3613    #[test]
3614    fn test_format_error() {
3615        assert_eq!(
3616            super::Error::ParseError(ParseError(vec!["foo".to_string()])).to_string(),
3617            "foo\n"
3618        );
3619    }
3620
3621    #[test]
3622    fn test_get_all() {
3623        let d: super::Deb822 = r#"Source: foo
3624Maintainer: Foo Bar <foo@example.com>
3625Maintainer: Bar Foo <bar@example.com>"#
3626            .parse()
3627            .unwrap();
3628        let p = d.paragraphs().next().unwrap();
3629        assert_eq!(
3630            p.get_all("Maintainer").collect::<Vec<_>>(),
3631            vec!["Foo Bar <foo@example.com>", "Bar Foo <bar@example.com>"]
3632        );
3633    }
3634
3635    #[test]
3636    fn test_get_with_indent_single_line() {
3637        let input = "Field: single line value\n";
3638        let deb = super::Deb822::from_str(input).unwrap();
3639        let para = deb.paragraphs().next().unwrap();
3640
3641        // Single-line values should be unchanged regardless of indent pattern
3642        assert_eq!(
3643            para.get_with_indent("Field", &super::IndentPattern::Fixed(2)),
3644            Some("single line value".to_string())
3645        );
3646        assert_eq!(
3647            para.get_with_indent("Field", &super::IndentPattern::FieldNameLength),
3648            Some("single line value".to_string())
3649        );
3650    }
3651
3652    #[test]
3653    fn test_get_with_indent_fixed() {
3654        let input = "Field: First\n   Second\n   Third\n";
3655        let deb = super::Deb822::from_str(input).unwrap();
3656        let para = deb.paragraphs().next().unwrap();
3657
3658        // Get with fixed 2-space indentation - strips 2 spaces, leaves 1
3659        let value = para
3660            .get_with_indent("Field", &super::IndentPattern::Fixed(2))
3661            .unwrap();
3662        assert_eq!(value, "First\n Second\n Third");
3663
3664        // Get with fixed 1-space indentation - strips 1 space, leaves 2
3665        let value = para
3666            .get_with_indent("Field", &super::IndentPattern::Fixed(1))
3667            .unwrap();
3668        assert_eq!(value, "First\n  Second\n  Third");
3669
3670        // Get with fixed 3-space indentation - strips all 3 spaces
3671        let value = para
3672            .get_with_indent("Field", &super::IndentPattern::Fixed(3))
3673            .unwrap();
3674        assert_eq!(value, "First\nSecond\nThird");
3675    }
3676
3677    #[test]
3678    fn test_get_with_indent_field_name_length() {
3679        let input = "Description: First line\n             Second line\n             Third line\n";
3680        let deb = super::Deb822::from_str(input).unwrap();
3681        let para = deb.paragraphs().next().unwrap();
3682
3683        // Get with FieldNameLength pattern
3684        // "Description: " is 13 characters, so strips 13 spaces, leaves 0
3685        let value = para
3686            .get_with_indent("Description", &super::IndentPattern::FieldNameLength)
3687            .unwrap();
3688        assert_eq!(value, "First line\nSecond line\nThird line");
3689
3690        // Get with fixed 2-space indentation - strips 2, leaves 11
3691        let value = para
3692            .get_with_indent("Description", &super::IndentPattern::Fixed(2))
3693            .unwrap();
3694        assert_eq!(
3695            value,
3696            "First line\n           Second line\n           Third line"
3697        );
3698    }
3699
3700    #[test]
3701    fn test_get_with_indent_nonexistent() {
3702        let input = "Field: value\n";
3703        let deb = super::Deb822::from_str(input).unwrap();
3704        let para = deb.paragraphs().next().unwrap();
3705
3706        assert_eq!(
3707            para.get_with_indent("NonExistent", &super::IndentPattern::Fixed(2)),
3708            None
3709        );
3710    }
3711
3712    #[test]
3713    fn test_get_entry() {
3714        let input = r#"Package: test-package
3715Maintainer: Test User <test@example.com>
3716Description: A simple test package
3717 with multiple lines
3718"#;
3719        let deb = super::Deb822::from_str(input).unwrap();
3720        let para = deb.paragraphs().next().unwrap();
3721
3722        // Test getting existing entry
3723        let entry = para.get_entry("Package");
3724        assert!(entry.is_some());
3725        let entry = entry.unwrap();
3726        assert_eq!(entry.key(), Some("Package".to_string()));
3727        assert_eq!(entry.value(), "test-package");
3728
3729        // Test case-insensitive lookup
3730        let entry = para.get_entry("package");
3731        assert!(entry.is_some());
3732        assert_eq!(entry.unwrap().value(), "test-package");
3733
3734        // Test multi-line value
3735        let entry = para.get_entry("Description");
3736        assert!(entry.is_some());
3737        assert_eq!(
3738            entry.unwrap().value(),
3739            "A simple test package\nwith multiple lines"
3740        );
3741
3742        // Test non-existent field
3743        assert_eq!(para.get_entry("NonExistent"), None);
3744    }
3745
3746    #[test]
3747    fn test_entry_ranges() {
3748        let input = r#"Package: test-package
3749Maintainer: Test User <test@example.com>
3750Description: A simple test package
3751 with multiple lines
3752 of description text"#;
3753
3754        let deb822 = super::Deb822::from_str(input).unwrap();
3755        let paragraph = deb822.paragraphs().next().unwrap();
3756        let entries: Vec<_> = paragraph.entries().collect();
3757
3758        // Test first entry (Package)
3759        let package_entry = &entries[0];
3760        assert_eq!(package_entry.key(), Some("Package".to_string()));
3761
3762        // Test key_range
3763        let key_range = package_entry.key_range().unwrap();
3764        assert_eq!(
3765            &input[key_range.start().into()..key_range.end().into()],
3766            "Package"
3767        );
3768
3769        // Test colon_range
3770        let colon_range = package_entry.colon_range().unwrap();
3771        assert_eq!(
3772            &input[colon_range.start().into()..colon_range.end().into()],
3773            ":"
3774        );
3775
3776        // Test value_range
3777        let value_range = package_entry.value_range().unwrap();
3778        assert_eq!(
3779            &input[value_range.start().into()..value_range.end().into()],
3780            "test-package"
3781        );
3782
3783        // Test text_range covers the whole entry
3784        let text_range = package_entry.text_range();
3785        assert_eq!(
3786            &input[text_range.start().into()..text_range.end().into()],
3787            "Package: test-package\n"
3788        );
3789
3790        // Test single-line value_line_ranges
3791        let value_lines = package_entry.value_line_ranges();
3792        assert_eq!(value_lines.len(), 1);
3793        assert_eq!(
3794            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3795            "test-package"
3796        );
3797    }
3798
3799    #[test]
3800    fn test_multiline_entry_ranges() {
3801        let input = r#"Description: Short description
3802 Extended description line 1
3803 Extended description line 2"#;
3804
3805        let deb822 = super::Deb822::from_str(input).unwrap();
3806        let paragraph = deb822.paragraphs().next().unwrap();
3807        let entry = paragraph.entries().next().unwrap();
3808
3809        assert_eq!(entry.key(), Some("Description".to_string()));
3810
3811        // Test value_range spans all lines
3812        let value_range = entry.value_range().unwrap();
3813        let full_value = &input[value_range.start().into()..value_range.end().into()];
3814        assert!(full_value.contains("Short description"));
3815        assert!(full_value.contains("Extended description line 1"));
3816        assert!(full_value.contains("Extended description line 2"));
3817
3818        // Test value_line_ranges gives individual lines
3819        let value_lines = entry.value_line_ranges();
3820        assert_eq!(value_lines.len(), 3);
3821
3822        assert_eq!(
3823            &input[value_lines[0].start().into()..value_lines[0].end().into()],
3824            "Short description"
3825        );
3826        assert_eq!(
3827            &input[value_lines[1].start().into()..value_lines[1].end().into()],
3828            "Extended description line 1"
3829        );
3830        assert_eq!(
3831            &input[value_lines[2].start().into()..value_lines[2].end().into()],
3832            "Extended description line 2"
3833        );
3834    }
3835
3836    #[test]
3837    fn test_entries_public_access() {
3838        let input = r#"Package: test
3839Version: 1.0"#;
3840
3841        let deb822 = super::Deb822::from_str(input).unwrap();
3842        let paragraph = deb822.paragraphs().next().unwrap();
3843
3844        // Test that entries() method is now public
3845        let entries: Vec<_> = paragraph.entries().collect();
3846        assert_eq!(entries.len(), 2);
3847        assert_eq!(entries[0].key(), Some("Package".to_string()));
3848        assert_eq!(entries[1].key(), Some("Version".to_string()));
3849    }
3850
3851    #[test]
3852    fn test_empty_value_ranges() {
3853        let input = r#"EmptyField: "#;
3854
3855        let deb822 = super::Deb822::from_str(input).unwrap();
3856        let paragraph = deb822.paragraphs().next().unwrap();
3857        let entry = paragraph.entries().next().unwrap();
3858
3859        assert_eq!(entry.key(), Some("EmptyField".to_string()));
3860
3861        // Empty value should still have ranges
3862        assert!(entry.key_range().is_some());
3863        assert!(entry.colon_range().is_some());
3864
3865        // Empty value might not have value tokens
3866        let value_lines = entry.value_line_ranges();
3867        // This depends on how the parser handles empty values
3868        // but we should not panic
3869        assert!(value_lines.len() <= 1);
3870    }
3871
3872    #[test]
3873    fn test_range_ordering() {
3874        let input = r#"Field: value"#;
3875
3876        let deb822 = super::Deb822::from_str(input).unwrap();
3877        let paragraph = deb822.paragraphs().next().unwrap();
3878        let entry = paragraph.entries().next().unwrap();
3879
3880        let key_range = entry.key_range().unwrap();
3881        let colon_range = entry.colon_range().unwrap();
3882        let value_range = entry.value_range().unwrap();
3883        let text_range = entry.text_range();
3884
3885        // Verify ranges are in correct order
3886        assert!(key_range.end() <= colon_range.start());
3887        assert!(colon_range.end() <= value_range.start());
3888        assert!(key_range.start() >= text_range.start());
3889        assert!(value_range.end() <= text_range.end());
3890    }
3891
3892    #[test]
3893    fn test_error_recovery_missing_colon() {
3894        let input = r#"Source foo
3895Maintainer: Test User <test@example.com>
3896"#;
3897        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3898
3899        // Should still parse successfully with errors
3900        assert!(!errors.is_empty());
3901        assert!(errors.iter().any(|e| e.contains("missing colon")));
3902
3903        // Should still have a paragraph with the valid field
3904        let paragraph = deb822.paragraphs().next().unwrap();
3905        assert_eq!(
3906            paragraph.get("Maintainer").as_deref(),
3907            Some("Test User <test@example.com>")
3908        );
3909    }
3910
3911    #[test]
3912    fn test_error_recovery_missing_field_name() {
3913        let input = r#": orphaned value
3914Package: test
3915"#;
3916
3917        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3918
3919        // Should have errors about missing field name
3920        assert!(!errors.is_empty());
3921        assert!(errors
3922            .iter()
3923            .any(|e| e.contains("field name") || e.contains("missing")));
3924
3925        // The valid field should be in one of the paragraphs
3926        let paragraphs: Vec<_> = deb822.paragraphs().collect();
3927        let mut found_package = false;
3928        for paragraph in paragraphs.iter() {
3929            if paragraph.get("Package").is_some() {
3930                found_package = true;
3931                assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
3932            }
3933        }
3934        assert!(found_package, "Package field not found in any paragraph");
3935    }
3936
3937    #[test]
3938    fn test_error_recovery_orphaned_text() {
3939        let input = r#"Package: test
3940some orphaned text without field name
3941Version: 1.0
3942"#;
3943        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3944
3945        // Should have errors about orphaned text
3946        assert!(!errors.is_empty());
3947        assert!(errors.iter().any(|e| e.contains("orphaned")
3948            || e.contains("unexpected")
3949            || e.contains("field name")));
3950
3951        // Should still parse the valid fields (may be split across paragraphs)
3952        let mut all_fields = std::collections::HashMap::new();
3953        for paragraph in deb822.paragraphs() {
3954            for (key, value) in paragraph.items() {
3955                all_fields.insert(key, value);
3956            }
3957        }
3958
3959        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
3960        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
3961    }
3962
3963    #[test]
3964    fn test_error_recovery_consecutive_field_names() {
3965        let input = r#"Package: test
3966Description
3967Maintainer: Another field without proper value
3968Version: 1.0
3969"#;
3970        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
3971
3972        // Should have errors about missing values
3973        assert!(!errors.is_empty());
3974        assert!(errors.iter().any(|e| e.contains("consecutive")
3975            || e.contains("missing")
3976            || e.contains("incomplete")));
3977
3978        // Should still parse valid fields (may be split across paragraphs due to errors)
3979        let mut all_fields = std::collections::HashMap::new();
3980        for paragraph in deb822.paragraphs() {
3981            for (key, value) in paragraph.items() {
3982                all_fields.insert(key, value);
3983            }
3984        }
3985
3986        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
3987        assert_eq!(
3988            all_fields.get("Maintainer"),
3989            Some(&"Another field without proper value".to_string())
3990        );
3991        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
3992    }
3993
3994    #[test]
3995    fn test_error_recovery_malformed_multiline() {
3996        let input = r#"Package: test
3997Description: Short desc
3998  Proper continuation
3999invalid continuation without indent
4000 Another proper continuation
4001Version: 1.0
4002"#;
4003        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4004
4005        // Should recover from malformed continuation
4006        assert!(!errors.is_empty());
4007
4008        // Should still parse other fields correctly
4009        let paragraph = deb822.paragraphs().next().unwrap();
4010        assert_eq!(paragraph.get("Package").as_deref(), Some("test"));
4011        assert_eq!(paragraph.get("Version").as_deref(), Some("1.0"));
4012    }
4013
4014    #[test]
4015    fn test_error_recovery_mixed_errors() {
4016        let input = r#"Package test without colon
4017: orphaned colon
4018Description: Valid field
4019some orphaned text
4020Another-Field: Valid too
4021"#;
4022        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4023
4024        // Should have multiple different errors
4025        assert!(!errors.is_empty());
4026        assert!(errors.len() >= 2);
4027
4028        // Should still parse the valid fields
4029        let paragraph = deb822.paragraphs().next().unwrap();
4030        assert_eq!(paragraph.get("Description").as_deref(), Some("Valid field"));
4031        assert_eq!(paragraph.get("Another-Field").as_deref(), Some("Valid too"));
4032    }
4033
4034    #[test]
4035    fn test_error_recovery_paragraph_boundary() {
4036        let input = r#"Package: first-package
4037Description: First paragraph
4038
4039corrupted data here
4040: more corruption
4041completely broken line
4042
4043Package: second-package
4044Version: 1.0
4045"#;
4046        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4047
4048        // Should have errors from the corrupted section
4049        assert!(!errors.is_empty());
4050
4051        // Should still parse both paragraphs correctly
4052        let paragraphs: Vec<_> = deb822.paragraphs().collect();
4053        assert_eq!(paragraphs.len(), 2);
4054
4055        assert_eq!(
4056            paragraphs[0].get("Package").as_deref(),
4057            Some("first-package")
4058        );
4059        assert_eq!(
4060            paragraphs[1].get("Package").as_deref(),
4061            Some("second-package")
4062        );
4063        assert_eq!(paragraphs[1].get("Version").as_deref(), Some("1.0"));
4064    }
4065
4066    #[test]
4067    fn test_error_recovery_with_positioned_errors() {
4068        let input = r#"Package test
4069Description: Valid
4070"#;
4071        let parsed = super::parse(input);
4072
4073        // Should have positioned errors with proper ranges
4074        assert!(!parsed.positioned_errors.is_empty());
4075
4076        let first_error = &parsed.positioned_errors[0];
4077        assert!(!first_error.message.is_empty());
4078        assert!(first_error.range.start() <= first_error.range.end());
4079        assert!(first_error.code.is_some());
4080
4081        // Error should point to the problematic location
4082        let error_text = &input[first_error.range.start().into()..first_error.range.end().into()];
4083        assert!(!error_text.is_empty());
4084    }
4085
4086    #[test]
4087    fn test_error_recovery_preserves_whitespace() {
4088        let input = r#"Source: package
4089Maintainer   Test User <test@example.com>
4090Section:    utils
4091
4092"#;
4093        let (deb822, errors) = super::Deb822::from_str_relaxed(input);
4094
4095        // Should have error about missing colon
4096        assert!(!errors.is_empty());
4097
4098        // Should preserve original formatting in output
4099        let output = deb822.to_string();
4100        assert!(output.contains("Section:    utils"));
4101
4102        // Should still extract valid fields
4103        let paragraph = deb822.paragraphs().next().unwrap();
4104        assert_eq!(paragraph.get("Source").as_deref(), Some("package"));
4105        assert_eq!(paragraph.get("Section").as_deref(), Some("utils"));
4106    }
4107
4108    #[test]
4109    fn test_error_recovery_empty_fields() {
4110        let input = r#"Package: test
4111Description:
4112Maintainer: Valid User
4113EmptyField:
4114Version: 1.0
4115"#;
4116        let (deb822, _errors) = super::Deb822::from_str_relaxed(input);
4117
4118        // Empty fields should parse without major errors - collect all fields from all paragraphs
4119        let mut all_fields = std::collections::HashMap::new();
4120        for paragraph in deb822.paragraphs() {
4121            for (key, value) in paragraph.items() {
4122                all_fields.insert(key, value);
4123            }
4124        }
4125
4126        assert_eq!(all_fields.get("Package"), Some(&"test".to_string()));
4127        assert_eq!(all_fields.get("Description"), Some(&"".to_string()));
4128        assert_eq!(
4129            all_fields.get("Maintainer"),
4130            Some(&"Valid User".to_string())
4131        );
4132        assert_eq!(all_fields.get("EmptyField"), Some(&"".to_string()));
4133        assert_eq!(all_fields.get("Version"), Some(&"1.0".to_string()));
4134    }
4135
4136    #[test]
4137    fn test_insert_comment_before() {
4138        let d: super::Deb822 = vec![
4139            vec![("Source", "foo"), ("Maintainer", "Bar <bar@example.com>")]
4140                .into_iter()
4141                .collect(),
4142            vec![("Package", "foo"), ("Architecture", "all")]
4143                .into_iter()
4144                .collect(),
4145        ]
4146        .into_iter()
4147        .collect();
4148
4149        // Insert comment before first paragraph
4150        let mut p1 = d.paragraphs().next().unwrap();
4151        p1.insert_comment_before("This is the source paragraph");
4152
4153        // Insert comment before second paragraph
4154        let mut p2 = d.paragraphs().nth(1).unwrap();
4155        p2.insert_comment_before("This is the binary paragraph");
4156
4157        let output = d.to_string();
4158        assert_eq!(
4159            output,
4160            r#"# This is the source paragraph
4161Source: foo
4162Maintainer: Bar <bar@example.com>
4163
4164# This is the binary paragraph
4165Package: foo
4166Architecture: all
4167"#
4168        );
4169    }
4170
4171    #[test]
4172    fn test_parse_continuation_with_colon() {
4173        // Test that continuation lines with colons are properly parsed
4174        let input = "Package: test\nDescription: short\n line: with colon\n";
4175        let result = input.parse::<Deb822>();
4176        assert!(result.is_ok());
4177
4178        let deb822 = result.unwrap();
4179        let para = deb822.paragraphs().next().unwrap();
4180        assert_eq!(para.get("Package").as_deref(), Some("test"));
4181        assert_eq!(
4182            para.get("Description").as_deref(),
4183            Some("short\nline: with colon")
4184        );
4185    }
4186
4187    #[test]
4188    fn test_parse_continuation_starting_with_colon() {
4189        // Test continuation line STARTING with a colon (issue #315)
4190        let input = "Package: test\nDescription: short\n :value\n";
4191        let result = input.parse::<Deb822>();
4192        assert!(result.is_ok());
4193
4194        let deb822 = result.unwrap();
4195        let para = deb822.paragraphs().next().unwrap();
4196        assert_eq!(para.get("Package").as_deref(), Some("test"));
4197        assert_eq!(para.get("Description").as_deref(), Some("short\n:value"));
4198    }
4199
4200    #[test]
4201    fn test_normalize_field_spacing_single_space() {
4202        // Field already has correct spacing
4203        let input = "Field: value\n";
4204        let deb822 = input.parse::<Deb822>().unwrap();
4205        let mut para = deb822.paragraphs().next().unwrap();
4206
4207        para.normalize_field_spacing();
4208        assert_eq!(para.to_string(), "Field: value\n");
4209    }
4210
4211    #[test]
4212    fn test_normalize_field_spacing_extra_spaces() {
4213        // Field has extra spaces after colon
4214        let input = "Field:    value\n";
4215        let deb822 = input.parse::<Deb822>().unwrap();
4216        let mut para = deb822.paragraphs().next().unwrap();
4217
4218        para.normalize_field_spacing();
4219        assert_eq!(para.to_string(), "Field: value\n");
4220    }
4221
4222    #[test]
4223    fn test_normalize_field_spacing_no_space() {
4224        // Field has no space after colon
4225        let input = "Field:value\n";
4226        let deb822 = input.parse::<Deb822>().unwrap();
4227        let mut para = deb822.paragraphs().next().unwrap();
4228
4229        para.normalize_field_spacing();
4230        assert_eq!(para.to_string(), "Field: value\n");
4231    }
4232
4233    #[test]
4234    fn test_normalize_field_spacing_multiple_fields() {
4235        // Multiple fields with various spacing
4236        let input = "Field1:    value1\nField2:value2\nField3:  value3\n";
4237        let deb822 = input.parse::<Deb822>().unwrap();
4238        let mut para = deb822.paragraphs().next().unwrap();
4239
4240        para.normalize_field_spacing();
4241        assert_eq!(
4242            para.to_string(),
4243            "Field1: value1\nField2: value2\nField3: value3\n"
4244        );
4245    }
4246
4247    #[test]
4248    fn test_normalize_field_spacing_multiline_value() {
4249        // Field with multiline value
4250        let input = "Description:    short\n continuation line\n .  \n final line\n";
4251        let deb822 = input.parse::<Deb822>().unwrap();
4252        let mut para = deb822.paragraphs().next().unwrap();
4253
4254        para.normalize_field_spacing();
4255        assert_eq!(
4256            para.to_string(),
4257            "Description: short\n continuation line\n .  \n final line\n"
4258        );
4259    }
4260
4261    #[test]
4262    fn test_normalize_field_spacing_empty_value_with_whitespace() {
4263        // Field with empty value (only whitespace) should normalize to no space
4264        let input = "Field:  \n";
4265        let deb822 = input.parse::<Deb822>().unwrap();
4266        let mut para = deb822.paragraphs().next().unwrap();
4267
4268        para.normalize_field_spacing();
4269        // When value is empty/whitespace-only, normalize to no space
4270        assert_eq!(para.to_string(), "Field:\n");
4271    }
4272
4273    #[test]
4274    fn test_normalize_field_spacing_no_value() {
4275        // Field with no value (just newline) should stay unchanged
4276        let input = "Depends:\n";
4277        let deb822 = input.parse::<Deb822>().unwrap();
4278        let mut para = deb822.paragraphs().next().unwrap();
4279
4280        para.normalize_field_spacing();
4281        // Should remain with no space
4282        assert_eq!(para.to_string(), "Depends:\n");
4283    }
4284
4285    #[test]
4286    fn test_normalize_field_spacing_multiple_paragraphs() {
4287        // Multiple paragraphs
4288        let input = "Field1:    value1\n\nField2:  value2\n";
4289        let mut deb822 = input.parse::<Deb822>().unwrap();
4290
4291        deb822.normalize_field_spacing();
4292        assert_eq!(deb822.to_string(), "Field1: value1\n\nField2: value2\n");
4293    }
4294
4295    #[test]
4296    fn test_normalize_field_spacing_preserves_comments() {
4297        // Normalize spacing while preserving comments (comments are at document level)
4298        let input = "# Comment\nField:    value\n";
4299        let mut deb822 = input.parse::<Deb822>().unwrap();
4300
4301        deb822.normalize_field_spacing();
4302        assert_eq!(deb822.to_string(), "# Comment\nField: value\n");
4303    }
4304
4305    #[test]
4306    fn test_normalize_field_spacing_preserves_values() {
4307        // Ensure values are preserved exactly
4308        let input = "Source:   foo-bar\nMaintainer:Foo Bar <test@example.com>\n";
4309        let deb822 = input.parse::<Deb822>().unwrap();
4310        let mut para = deb822.paragraphs().next().unwrap();
4311
4312        para.normalize_field_spacing();
4313
4314        assert_eq!(para.get("Source").as_deref(), Some("foo-bar"));
4315        assert_eq!(
4316            para.get("Maintainer").as_deref(),
4317            Some("Foo Bar <test@example.com>")
4318        );
4319    }
4320
4321    #[test]
4322    fn test_normalize_field_spacing_tab_after_colon() {
4323        // Field with tab after colon (should be normalized to single space)
4324        let input = "Field:\tvalue\n";
4325        let deb822 = input.parse::<Deb822>().unwrap();
4326        let mut para = deb822.paragraphs().next().unwrap();
4327
4328        para.normalize_field_spacing();
4329        assert_eq!(para.to_string(), "Field: value\n");
4330    }
4331
4332    #[test]
4333    fn test_set_preserves_indentation() {
4334        // Test that Paragraph.set() preserves the original indentation
4335        let original = r#"Source: example
4336Build-Depends: foo,
4337               bar,
4338               baz
4339"#;
4340
4341        let mut para: super::Paragraph = original.parse().unwrap();
4342
4343        // Modify the Build-Depends field
4344        para.set("Build-Depends", "foo,\nbar,\nbaz");
4345
4346        // The indentation should be preserved (15 spaces for "Build-Depends: ")
4347        let expected = r#"Source: example
4348Build-Depends: foo,
4349               bar,
4350               baz
4351"#;
4352        assert_eq!(para.to_string(), expected);
4353    }
4354
4355    #[test]
4356    fn test_set_new_field_detects_field_name_length_indent() {
4357        // Test that new fields detect field-name-length-based indentation
4358        let original = r#"Source: example
4359Build-Depends: foo,
4360               bar,
4361               baz
4362Depends: lib1,
4363         lib2
4364"#;
4365
4366        let mut para: super::Paragraph = original.parse().unwrap();
4367
4368        // Add a new multi-line field - should detect that indentation is field-name-length + 2
4369        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4370
4371        // "Recommends: " is 12 characters, so indentation should be 12 spaces
4372        assert!(para
4373            .to_string()
4374            .contains("Recommends: pkg1,\n            pkg2,"));
4375    }
4376
4377    #[test]
4378    fn test_set_new_field_detects_fixed_indent() {
4379        // Test that new fields detect fixed indentation pattern
4380        let original = r#"Source: example
4381Build-Depends: foo,
4382     bar,
4383     baz
4384Depends: lib1,
4385     lib2
4386"#;
4387
4388        let mut para: super::Paragraph = original.parse().unwrap();
4389
4390        // Add a new multi-line field - should detect fixed 5-space indentation
4391        para.set("Recommends", "pkg1,\npkg2,\npkg3");
4392
4393        // Should use the same 5-space indentation
4394        assert!(para
4395            .to_string()
4396            .contains("Recommends: pkg1,\n     pkg2,\n     pkg3\n"));
4397    }
4398
4399    #[test]
4400    fn test_set_new_field_no_multiline_fields() {
4401        // Test that new fields use field-name-length when no existing multi-line fields
4402        let original = r#"Source: example
4403Maintainer: Test <test@example.com>
4404"#;
4405
4406        let mut para: super::Paragraph = original.parse().unwrap();
4407
4408        // Add a new multi-line field - should default to field name length + 2
4409        para.set("Depends", "foo,\nbar,\nbaz");
4410
4411        // "Depends: " is 9 characters, so indentation should be 9 spaces
4412        let expected = r#"Source: example
4413Maintainer: Test <test@example.com>
4414Depends: foo,
4415         bar,
4416         baz
4417"#;
4418        assert_eq!(para.to_string(), expected);
4419    }
4420
4421    #[test]
4422    fn test_set_new_field_mixed_indentation() {
4423        // Test that new fields fall back to field-name-length when pattern is inconsistent
4424        let original = r#"Source: example
4425Build-Depends: foo,
4426               bar
4427Depends: lib1,
4428     lib2
4429"#;
4430
4431        let mut para: super::Paragraph = original.parse().unwrap();
4432
4433        // Add a new multi-line field - mixed pattern, should fall back to field name length + 2
4434        para.set("Recommends", "pkg1,\npkg2");
4435
4436        // "Recommends: " is 12 characters
4437        assert!(para
4438            .to_string()
4439            .contains("Recommends: pkg1,\n            pkg2\n"));
4440    }
4441
4442    #[test]
4443    fn test_entry_with_indentation() {
4444        // Test Entry::with_indentation directly
4445        let entry = super::Entry::with_indentation("Test-Field", "value1\nvalue2\nvalue3", "    ");
4446
4447        assert_eq!(
4448            entry.to_string(),
4449            "Test-Field: value1\n    value2\n    value3\n"
4450        );
4451    }
4452
4453    #[test]
4454    fn test_set_with_indent_pattern_fixed() {
4455        // Test setting a field with explicit fixed indentation pattern
4456        let original = r#"Source: example
4457Maintainer: Test <test@example.com>
4458"#;
4459
4460        let mut para: super::Paragraph = original.parse().unwrap();
4461
4462        // Add a new multi-line field with fixed 4-space indentation
4463        para.set_with_indent_pattern(
4464            "Depends",
4465            "foo,\nbar,\nbaz",
4466            Some(&super::IndentPattern::Fixed(4)),
4467            None,
4468        );
4469
4470        // Should use the specified 4-space indentation
4471        let expected = r#"Source: example
4472Maintainer: Test <test@example.com>
4473Depends: foo,
4474    bar,
4475    baz
4476"#;
4477        assert_eq!(para.to_string(), expected);
4478    }
4479
4480    #[test]
4481    fn test_set_with_indent_pattern_field_name_length() {
4482        // Test setting a field with field-name-length indentation pattern
4483        let original = r#"Source: example
4484Maintainer: Test <test@example.com>
4485"#;
4486
4487        let mut para: super::Paragraph = original.parse().unwrap();
4488
4489        // Add a new multi-line field with field-name-length indentation
4490        para.set_with_indent_pattern(
4491            "Build-Depends",
4492            "libfoo,\nlibbar,\nlibbaz",
4493            Some(&super::IndentPattern::FieldNameLength),
4494            None,
4495        );
4496
4497        // "Build-Depends: " is 15 characters, so indentation should be 15 spaces
4498        let expected = r#"Source: example
4499Maintainer: Test <test@example.com>
4500Build-Depends: libfoo,
4501               libbar,
4502               libbaz
4503"#;
4504        assert_eq!(para.to_string(), expected);
4505    }
4506
4507    #[test]
4508    fn test_set_with_indent_pattern_override_auto_detection() {
4509        // Test that explicit default pattern overrides auto-detection for new fields
4510        let original = r#"Source: example
4511Build-Depends: foo,
4512               bar,
4513               baz
4514"#;
4515
4516        let mut para: super::Paragraph = original.parse().unwrap();
4517
4518        // Add a NEW field with fixed 2-space indentation, overriding the auto-detected pattern
4519        para.set_with_indent_pattern(
4520            "Depends",
4521            "lib1,\nlib2,\nlib3",
4522            Some(&super::IndentPattern::Fixed(2)),
4523            None,
4524        );
4525
4526        // Should use the specified 2-space indentation, not the auto-detected 15-space
4527        let expected = r#"Source: example
4528Build-Depends: foo,
4529               bar,
4530               baz
4531Depends: lib1,
4532  lib2,
4533  lib3
4534"#;
4535        assert_eq!(para.to_string(), expected);
4536    }
4537
4538    #[test]
4539    fn test_set_with_indent_pattern_none_auto_detects() {
4540        // Test that None pattern auto-detects from existing fields
4541        let original = r#"Source: example
4542Build-Depends: foo,
4543     bar,
4544     baz
4545"#;
4546
4547        let mut para: super::Paragraph = original.parse().unwrap();
4548
4549        // Add a field with None pattern - should auto-detect fixed 5-space
4550        para.set_with_indent_pattern("Depends", "lib1,\nlib2", None, None);
4551
4552        // Should auto-detect and use the 5-space indentation
4553        let expected = r#"Source: example
4554Build-Depends: foo,
4555     bar,
4556     baz
4557Depends: lib1,
4558     lib2
4559"#;
4560        assert_eq!(para.to_string(), expected);
4561    }
4562
4563    #[test]
4564    fn test_set_with_indent_pattern_with_field_order() {
4565        // Test setting a field with both indent pattern and field ordering
4566        let original = r#"Source: example
4567Maintainer: Test <test@example.com>
4568"#;
4569
4570        let mut para: super::Paragraph = original.parse().unwrap();
4571
4572        // Add a field with fixed indentation and specific field ordering
4573        para.set_with_indent_pattern(
4574            "Priority",
4575            "optional",
4576            Some(&super::IndentPattern::Fixed(4)),
4577            Some(&["Source", "Priority", "Maintainer"]),
4578        );
4579
4580        // Priority should be inserted between Source and Maintainer
4581        let expected = r#"Source: example
4582Priority: optional
4583Maintainer: Test <test@example.com>
4584"#;
4585        assert_eq!(para.to_string(), expected);
4586    }
4587
4588    #[test]
4589    fn test_set_with_indent_pattern_replace_existing() {
4590        // Test that replacing an existing multi-line field preserves its indentation
4591        let original = r#"Source: example
4592Depends: foo,
4593         bar
4594"#;
4595
4596        let mut para: super::Paragraph = original.parse().unwrap();
4597
4598        // Replace Depends - the default pattern is ignored, existing indentation is preserved
4599        para.set_with_indent_pattern(
4600            "Depends",
4601            "lib1,\nlib2,\nlib3",
4602            Some(&super::IndentPattern::Fixed(3)),
4603            None,
4604        );
4605
4606        // Should preserve the existing 9-space indentation, not use the default 3-space
4607        let expected = r#"Source: example
4608Depends: lib1,
4609         lib2,
4610         lib3
4611"#;
4612        assert_eq!(para.to_string(), expected);
4613    }
4614
4615    #[test]
4616    fn test_change_field_indent() {
4617        // Test changing indentation of an existing field without changing its value
4618        let original = r#"Source: example
4619Depends: foo,
4620         bar,
4621         baz
4622"#;
4623        let mut para: super::Paragraph = original.parse().unwrap();
4624
4625        // Change Depends field to use 2-space indentation
4626        let result = para
4627            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4628            .unwrap();
4629        assert!(result, "Field should have been found and updated");
4630
4631        let expected = r#"Source: example
4632Depends: foo,
4633  bar,
4634  baz
4635"#;
4636        assert_eq!(para.to_string(), expected);
4637    }
4638
4639    #[test]
4640    fn test_change_field_indent_nonexistent() {
4641        // Test changing indentation of a non-existent field
4642        let original = r#"Source: example
4643"#;
4644        let mut para: super::Paragraph = original.parse().unwrap();
4645
4646        // Try to change indentation of non-existent field
4647        let result = para
4648            .change_field_indent("Depends", &super::IndentPattern::Fixed(2))
4649            .unwrap();
4650        assert!(!result, "Should return false for non-existent field");
4651
4652        // Paragraph should be unchanged
4653        assert_eq!(para.to_string(), original);
4654    }
4655
4656    #[test]
4657    fn test_change_field_indent_case_insensitive() {
4658        // Test that change_field_indent is case-insensitive
4659        let original = r#"Build-Depends: foo,
4660               bar
4661"#;
4662        let mut para: super::Paragraph = original.parse().unwrap();
4663
4664        // Change using different case
4665        let result = para
4666            .change_field_indent("build-depends", &super::IndentPattern::Fixed(1))
4667            .unwrap();
4668        assert!(result, "Should find field case-insensitively");
4669
4670        let expected = r#"Build-Depends: foo,
4671 bar
4672"#;
4673        assert_eq!(para.to_string(), expected);
4674    }
4675
4676    #[test]
4677    fn test_entry_get_indent() {
4678        // Test that we can extract indentation from an entry
4679        let original = r#"Build-Depends: foo,
4680               bar,
4681               baz
4682"#;
4683        let para: super::Paragraph = original.parse().unwrap();
4684        let entry = para.entries().next().unwrap();
4685
4686        assert_eq!(entry.get_indent(), Some("               ".to_string()));
4687    }
4688
4689    #[test]
4690    fn test_entry_get_indent_single_line() {
4691        // Single-line entries should return None for indentation
4692        let original = r#"Source: example
4693"#;
4694        let para: super::Paragraph = original.parse().unwrap();
4695        let entry = para.entries().next().unwrap();
4696
4697        assert_eq!(entry.get_indent(), None);
4698    }
4699}
4700
4701#[test]
4702fn test_move_paragraph_forward() {
4703    let mut d: Deb822 = vec![
4704        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4705        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4706        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4707    ]
4708    .into_iter()
4709    .collect();
4710    d.move_paragraph(0, 2);
4711    assert_eq!(
4712        d.to_string(),
4713        "A: B\nC: D\n\nX: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n"
4714    );
4715}
4716
4717#[test]
4718fn test_move_paragraph_backward() {
4719    let mut d: Deb822 = vec![
4720        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4721        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4722        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4723    ]
4724    .into_iter()
4725    .collect();
4726    d.move_paragraph(2, 0);
4727    assert_eq!(
4728        d.to_string(),
4729        "X: Y\nZ: W\n\nFoo: Bar\nBaz: Qux\n\nA: B\nC: D\n"
4730    );
4731}
4732
4733#[test]
4734fn test_move_paragraph_middle() {
4735    let mut d: Deb822 = vec![
4736        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4737        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4738        vec![("X", "Y"), ("Z", "W")].into_iter().collect(),
4739    ]
4740    .into_iter()
4741    .collect();
4742    d.move_paragraph(2, 1);
4743    assert_eq!(
4744        d.to_string(),
4745        "Foo: Bar\nBaz: Qux\n\nX: Y\nZ: W\n\nA: B\nC: D\n"
4746    );
4747}
4748
4749#[test]
4750fn test_move_paragraph_same_index() {
4751    let mut d: Deb822 = vec![
4752        vec![("Foo", "Bar"), ("Baz", "Qux")].into_iter().collect(),
4753        vec![("A", "B"), ("C", "D")].into_iter().collect(),
4754    ]
4755    .into_iter()
4756    .collect();
4757    let original = d.to_string();
4758    d.move_paragraph(1, 1);
4759    assert_eq!(d.to_string(), original);
4760}
4761
4762#[test]
4763fn test_move_paragraph_single() {
4764    let mut d: Deb822 = vec![vec![("Foo", "Bar")].into_iter().collect()]
4765        .into_iter()
4766        .collect();
4767    let original = d.to_string();
4768    d.move_paragraph(0, 0);
4769    assert_eq!(d.to_string(), original);
4770}
4771
4772#[test]
4773fn test_move_paragraph_invalid_index() {
4774    let mut d: Deb822 = vec![
4775        vec![("Foo", "Bar")].into_iter().collect(),
4776        vec![("A", "B")].into_iter().collect(),
4777    ]
4778    .into_iter()
4779    .collect();
4780    let original = d.to_string();
4781    d.move_paragraph(0, 5);
4782    assert_eq!(d.to_string(), original);
4783}
4784
4785#[test]
4786fn test_move_paragraph_with_comments() {
4787    let text = r#"Foo: Bar
4788
4789# This is a comment
4790
4791A: B
4792
4793X: Y
4794"#;
4795    let mut d: Deb822 = text.parse().unwrap();
4796    d.move_paragraph(0, 2);
4797    assert_eq!(
4798        d.to_string(),
4799        "# This is a comment\n\nA: B\n\nX: Y\n\nFoo: Bar\n"
4800    );
4801}
4802
4803#[test]
4804fn test_case_insensitive_get() {
4805    let text = "Package: test\nVersion: 1.0\n";
4806    let d: Deb822 = text.parse().unwrap();
4807    let p = d.paragraphs().next().unwrap();
4808
4809    // Test different case variations
4810    assert_eq!(p.get("Package").as_deref(), Some("test"));
4811    assert_eq!(p.get("package").as_deref(), Some("test"));
4812    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
4813    assert_eq!(p.get("PaCkAgE").as_deref(), Some("test"));
4814
4815    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
4816    assert_eq!(p.get("version").as_deref(), Some("1.0"));
4817    assert_eq!(p.get("VERSION").as_deref(), Some("1.0"));
4818}
4819
4820#[test]
4821fn test_case_insensitive_set() {
4822    let text = "Package: test\n";
4823    let d: Deb822 = text.parse().unwrap();
4824    let mut p = d.paragraphs().next().unwrap();
4825
4826    // Set with different case should update the existing field
4827    p.set("package", "updated");
4828    assert_eq!(p.get("Package").as_deref(), Some("updated"));
4829    assert_eq!(p.get("package").as_deref(), Some("updated"));
4830
4831    // Set with UPPERCASE
4832    p.set("PACKAGE", "updated2");
4833    assert_eq!(p.get("Package").as_deref(), Some("updated2"));
4834
4835    // Field count should remain 1
4836    assert_eq!(p.keys().count(), 1);
4837}
4838
4839#[test]
4840fn test_case_insensitive_remove() {
4841    let text = "Package: test\nVersion: 1.0\n";
4842    let d: Deb822 = text.parse().unwrap();
4843    let mut p = d.paragraphs().next().unwrap();
4844
4845    // Remove with different case
4846    p.remove("package");
4847    assert_eq!(p.get("Package"), None);
4848    assert_eq!(p.get("Version").as_deref(), Some("1.0"));
4849
4850    // Remove with uppercase
4851    p.remove("VERSION");
4852    assert_eq!(p.get("Version"), None);
4853
4854    // No fields left
4855    assert_eq!(p.keys().count(), 0);
4856}
4857
4858#[test]
4859fn test_case_preservation() {
4860    let text = "Package: test\n";
4861    let d: Deb822 = text.parse().unwrap();
4862    let mut p = d.paragraphs().next().unwrap();
4863
4864    // Original case should be preserved
4865    let original_text = d.to_string();
4866    assert_eq!(original_text, "Package: test\n");
4867
4868    // Set with different case should preserve original case
4869    p.set("package", "updated");
4870
4871    // The field name should still be "Package" (original case preserved)
4872    let updated_text = d.to_string();
4873    assert_eq!(updated_text, "Package: updated\n");
4874}
4875
4876#[test]
4877fn test_case_insensitive_contains_key() {
4878    let text = "Package: test\n";
4879    let d: Deb822 = text.parse().unwrap();
4880    let p = d.paragraphs().next().unwrap();
4881
4882    assert!(p.contains_key("Package"));
4883    assert!(p.contains_key("package"));
4884    assert!(p.contains_key("PACKAGE"));
4885    assert!(!p.contains_key("NonExistent"));
4886}
4887
4888#[test]
4889fn test_case_insensitive_get_all() {
4890    let text = "Package: test1\npackage: test2\n";
4891    let d: Deb822 = text.parse().unwrap();
4892    let p = d.paragraphs().next().unwrap();
4893
4894    let values: Vec<String> = p.get_all("PACKAGE").collect();
4895    assert_eq!(values, vec!["test1", "test2"]);
4896}
4897
4898#[test]
4899fn test_case_insensitive_rename() {
4900    let text = "Package: test\n";
4901    let d: Deb822 = text.parse().unwrap();
4902    let mut p = d.paragraphs().next().unwrap();
4903
4904    // Rename with different case
4905    assert!(p.rename("package", "NewName"));
4906    assert_eq!(p.get("NewName").as_deref(), Some("test"));
4907    assert_eq!(p.get("Package"), None);
4908}
4909
4910#[test]
4911fn test_rename_changes_case() {
4912    let text = "Package: test\n";
4913    let d: Deb822 = text.parse().unwrap();
4914    let mut p = d.paragraphs().next().unwrap();
4915
4916    // Rename to different case of the same name
4917    assert!(p.rename("package", "PACKAGE"));
4918
4919    // The field name should now be uppercase
4920    let updated_text = d.to_string();
4921    assert_eq!(updated_text, "PACKAGE: test\n");
4922
4923    // Can still get with any case
4924    assert_eq!(p.get("package").as_deref(), Some("test"));
4925    assert_eq!(p.get("Package").as_deref(), Some("test"));
4926    assert_eq!(p.get("PACKAGE").as_deref(), Some("test"));
4927}
4928
4929#[test]
4930fn test_reject_whitespace_only_continuation_line() {
4931    // Issue #350: A continuation line with only whitespace should not be accepted
4932    // According to Debian Policy, continuation lines must have content after the leading space
4933    // A line with only whitespace (like " \n") should terminate the field
4934
4935    // This should be rejected/treated as an error
4936    let text = "Build-Depends:\n \ndebhelper\n";
4937    let parsed = Deb822::parse(text);
4938
4939    // The empty line with just whitespace should cause an error
4940    // or at minimum, should not be included as part of the field value
4941    assert!(
4942        !parsed.errors().is_empty(),
4943        "Expected parse errors for whitespace-only continuation line"
4944    );
4945}
4946
4947#[test]
4948fn test_reject_empty_continuation_line_in_multiline_field() {
4949    // Test that an empty line terminates a multi-line field (and generates an error)
4950    let text = "Depends: foo,\n bar,\n \n baz\n";
4951    let parsed = Deb822::parse(text);
4952
4953    // The empty line should cause parse errors
4954    assert!(
4955        !parsed.errors().is_empty(),
4956        "Empty continuation line should generate parse errors"
4957    );
4958
4959    // Verify we got the specific error about empty continuation line
4960    let has_empty_line_error = parsed
4961        .errors()
4962        .iter()
4963        .any(|e| e.contains("empty continuation line"));
4964    assert!(
4965        has_empty_line_error,
4966        "Should have an error about empty continuation line"
4967    );
4968}
4969
4970#[test]
4971#[should_panic(expected = "empty continuation line")]
4972fn test_set_rejects_empty_continuation_lines() {
4973    // Test that Paragraph.set() panics for values with empty continuation lines
4974    let text = "Package: test\n";
4975    let deb822 = text.parse::<Deb822>().unwrap();
4976    let mut para = deb822.paragraphs().next().unwrap();
4977
4978    // Try to set a field with an empty continuation line
4979    // This should panic with an appropriate error message
4980    let value_with_empty_line = "foo\n \nbar";
4981    para.set("Depends", value_with_empty_line);
4982}
4983
4984#[test]
4985fn test_try_set_returns_error_for_empty_continuation_lines() {
4986    // Test that Paragraph.try_set() returns an error for values with empty continuation lines
4987    let text = "Package: test\n";
4988    let deb822 = text.parse::<Deb822>().unwrap();
4989    let mut para = deb822.paragraphs().next().unwrap();
4990
4991    // Try to set a field with an empty continuation line
4992    let value_with_empty_line = "foo\n \nbar";
4993    let result = para.try_set("Depends", value_with_empty_line);
4994
4995    // Should return an error
4996    assert!(
4997        result.is_err(),
4998        "try_set() should return an error for empty continuation lines"
4999    );
5000
5001    // Verify it's the right kind of error
5002    match result {
5003        Err(Error::InvalidValue(msg)) => {
5004            assert!(
5005                msg.contains("empty continuation line"),
5006                "Error message should mention empty continuation line"
5007            );
5008        }
5009        _ => panic!("Expected InvalidValue error"),
5010    }
5011}
5012
5013#[test]
5014fn test_try_set_with_indent_pattern_returns_error() {
5015    // Test that try_set_with_indent_pattern() returns an error for empty continuation lines
5016    let text = "Package: test\n";
5017    let deb822 = text.parse::<Deb822>().unwrap();
5018    let mut para = deb822.paragraphs().next().unwrap();
5019
5020    let value_with_empty_line = "foo\n \nbar";
5021    let result = para.try_set_with_indent_pattern(
5022        "Depends",
5023        value_with_empty_line,
5024        Some(&IndentPattern::Fixed(2)),
5025        None,
5026    );
5027
5028    assert!(
5029        result.is_err(),
5030        "try_set_with_indent_pattern() should return an error"
5031    );
5032}
5033
5034#[test]
5035fn test_try_set_succeeds_for_valid_value() {
5036    // Test that try_set() succeeds for valid values
5037    let text = "Package: test\n";
5038    let deb822 = text.parse::<Deb822>().unwrap();
5039    let mut para = deb822.paragraphs().next().unwrap();
5040
5041    // Valid multiline value
5042    let valid_value = "foo\nbar";
5043    let result = para.try_set("Depends", valid_value);
5044
5045    assert!(result.is_ok(), "try_set() should succeed for valid values");
5046    assert_eq!(para.get("Depends").as_deref(), Some("foo\nbar"));
5047}
5048
5049#[test]
5050fn test_field_with_empty_first_line() {
5051    // Test parsing a field where the value starts on a continuation line (empty first line)
5052    // This is valid according to Debian Policy - the first line can be empty
5053    let text = "Foo:\n blah\n blah\n";
5054    let parsed = Deb822::parse(text);
5055
5056    // This should be valid - no errors
5057    assert!(
5058        parsed.errors().is_empty(),
5059        "Empty first line should be valid. Got errors: {:?}",
5060        parsed.errors()
5061    );
5062
5063    let deb822 = parsed.tree();
5064    let para = deb822.paragraphs().next().unwrap();
5065    assert_eq!(para.get("Foo").as_deref(), Some("blah\nblah"));
5066}
5067
5068#[test]
5069fn test_try_set_with_empty_first_line() {
5070    // Test that try_set() works with values that have empty first line
5071    let text = "Package: test\n";
5072    let deb822 = text.parse::<Deb822>().unwrap();
5073    let mut para = deb822.paragraphs().next().unwrap();
5074
5075    // Value with empty first line - this should be valid
5076    let value = "\nblah\nmore";
5077    let result = para.try_set("Depends", value);
5078
5079    assert!(
5080        result.is_ok(),
5081        "try_set() should succeed for values with empty first line. Got: {:?}",
5082        result
5083    );
5084}
5085
5086#[test]
5087fn test_field_with_value_then_empty_continuation() {
5088    // Test that a field with a value on the first line followed by empty continuation is rejected
5089    let text = "Foo: bar\n \n";
5090    let parsed = Deb822::parse(text);
5091
5092    // This should have errors - empty continuation line after initial value
5093    assert!(
5094        !parsed.errors().is_empty(),
5095        "Field with value then empty continuation line should be rejected"
5096    );
5097
5098    // Verify we got the specific error about empty continuation line
5099    let has_empty_line_error = parsed
5100        .errors()
5101        .iter()
5102        .any(|e| e.contains("empty continuation line"));
5103    assert!(
5104        has_empty_line_error,
5105        "Should have error about empty continuation line"
5106    );
5107}
5108
5109#[test]
5110fn test_line_col() {
5111    let text = r#"Source: foo
5112Maintainer: Foo Bar <jelmer@jelmer.uk>
5113Section: net
5114
5115Package: foo
5116Architecture: all
5117Depends: libc6
5118Description: This is a description
5119 With details
5120"#;
5121    let deb822 = text.parse::<Deb822>().unwrap();
5122
5123    // Test paragraph line numbers
5124    let paras: Vec<_> = deb822.paragraphs().collect();
5125    assert_eq!(paras.len(), 2);
5126
5127    // First paragraph starts at line 0
5128    assert_eq!(paras[0].line(), 0);
5129    assert_eq!(paras[0].column(), 0);
5130
5131    // Second paragraph starts at line 4 (after the empty line)
5132    assert_eq!(paras[1].line(), 4);
5133    assert_eq!(paras[1].column(), 0);
5134
5135    // Test entry line numbers
5136    let entries: Vec<_> = paras[0].entries().collect();
5137    assert_eq!(entries[0].line(), 0); // Source: foo
5138    assert_eq!(entries[1].line(), 1); // Maintainer: ...
5139    assert_eq!(entries[2].line(), 2); // Section: net
5140
5141    // Test column numbers
5142    assert_eq!(entries[0].column(), 0); // Start of line
5143    assert_eq!(entries[1].column(), 0); // Start of line
5144
5145    // Test line_col() method
5146    assert_eq!(paras[1].line_col(), (4, 0));
5147    assert_eq!(entries[0].line_col(), (0, 0));
5148
5149    // Test multi-line entry
5150    let second_para_entries: Vec<_> = paras[1].entries().collect();
5151    assert_eq!(second_para_entries[3].line(), 7); // Description starts at line 7
5152}
5153
5154#[test]
5155fn test_deb822_snapshot_independence() {
5156    // Test that snapshot() creates an independent copy
5157    let text = r#"Source: foo
5158Maintainer: Joe <joe@example.com>
5159
5160Package: foo
5161Architecture: all
5162"#;
5163    let deb822 = text.parse::<Deb822>().unwrap();
5164    let snapshot = deb822.snapshot();
5165
5166    // Modify the original
5167    let mut para = deb822.paragraphs().next().unwrap();
5168    para.set("Source", "modified");
5169
5170    // Verify the snapshot is unchanged
5171    let snapshot_para = snapshot.paragraphs().next().unwrap();
5172    assert_eq!(snapshot_para.get("Source").as_deref(), Some("foo"));
5173
5174    // Modify the snapshot
5175    let mut snapshot_para = snapshot.paragraphs().next().unwrap();
5176    snapshot_para.set("Source", "snapshot-modified");
5177
5178    // Verify the original still has our first modification
5179    let para = deb822.paragraphs().next().unwrap();
5180    assert_eq!(para.get("Source").as_deref(), Some("modified"));
5181}
5182
5183#[test]
5184fn test_paragraph_snapshot_independence() {
5185    // Test that snapshot() creates an independent copy
5186    let text = "Package: foo\nArchitecture: all\n";
5187    let deb822 = text.parse::<Deb822>().unwrap();
5188    let mut para = deb822.paragraphs().next().unwrap();
5189    let mut snapshot = para.snapshot();
5190
5191    // Modify the original
5192    para.set("Package", "modified");
5193
5194    // Verify the snapshot is unchanged
5195    assert_eq!(snapshot.get("Package").as_deref(), Some("foo"));
5196
5197    // Modify the snapshot
5198    snapshot.set("Package", "snapshot-modified");
5199
5200    // Verify the original still has our first modification
5201    assert_eq!(para.get("Package").as_deref(), Some("modified"));
5202}
5203
5204#[test]
5205fn test_entry_snapshot_independence() {
5206    // Test that snapshot() creates an independent copy
5207    let text = "Package: foo\n";
5208    let deb822 = text.parse::<Deb822>().unwrap();
5209    let mut para = deb822.paragraphs().next().unwrap();
5210    let entry = para.entries().next().unwrap();
5211    let snapshot = entry.snapshot();
5212
5213    // Get values before modification
5214    let original_value = entry.value();
5215    let snapshot_value = snapshot.value();
5216
5217    // Both should start with the same value
5218    assert_eq!(original_value, "foo");
5219    assert_eq!(snapshot_value, "foo");
5220
5221    // Modify through the paragraph
5222    para.set("Package", "modified");
5223
5224    // Verify the entry reflects the change (since it points to the same paragraph)
5225    let entry_after = para.entries().next().unwrap();
5226    assert_eq!(entry_after.value(), "modified");
5227
5228    // But the snapshot entry should still have the original value
5229    // (it points to a different tree)
5230    assert_eq!(snapshot.value(), "foo");
5231}
5232
5233#[test]
5234fn test_snapshot_preserves_structure() {
5235    // Test that snapshot() preserves comments, whitespace, etc.
5236    let text = r#"# Comment
5237Source: foo
5238## Another comment
5239Maintainer: Joe <joe@example.com>
5240
5241Package: foo
5242Architecture: all
5243"#;
5244    let deb822 = text.parse::<Deb822>().unwrap();
5245    let snapshot = deb822.snapshot();
5246
5247    // Both should have the same structure
5248    assert_eq!(deb822.to_string(), snapshot.to_string());
5249
5250    // Verify they're independent
5251    let mut snapshot_para = snapshot.paragraphs().next().unwrap();
5252    snapshot_para.set("Source", "modified");
5253
5254    let original_para = deb822.paragraphs().next().unwrap();
5255    assert_eq!(original_para.get("Source").as_deref(), Some("foo"));
5256}
5257
5258#[test]
5259fn test_paragraph_text_range() {
5260    // Test that text_range() returns the correct range for a paragraph
5261    let text = r#"Source: foo
5262Maintainer: Joe <joe@example.com>
5263
5264Package: foo
5265Architecture: all
5266"#;
5267    let deb822 = text.parse::<Deb822>().unwrap();
5268    let paras: Vec<_> = deb822.paragraphs().collect();
5269
5270    // First paragraph
5271    let range1 = paras[0].text_range();
5272    let para1_text = &text[range1.start().into()..range1.end().into()];
5273    assert_eq!(
5274        para1_text,
5275        "Source: foo\nMaintainer: Joe <joe@example.com>\n"
5276    );
5277
5278    // Second paragraph
5279    let range2 = paras[1].text_range();
5280    let para2_text = &text[range2.start().into()..range2.end().into()];
5281    assert_eq!(para2_text, "Package: foo\nArchitecture: all\n");
5282}
5283
5284#[test]
5285fn test_paragraphs_in_range_single() {
5286    // Test finding a single paragraph in range
5287    let text = r#"Source: foo
5288
5289Package: bar
5290
5291Package: baz
5292"#;
5293    let deb822 = text.parse::<Deb822>().unwrap();
5294
5295    // Get range of first paragraph
5296    let first_para = deb822.paragraphs().next().unwrap();
5297    let range = first_para.text_range();
5298
5299    // Query paragraphs in that range
5300    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5301    assert_eq!(paras.len(), 1);
5302    assert_eq!(paras[0].get("Source").as_deref(), Some("foo"));
5303}
5304
5305#[test]
5306fn test_paragraphs_in_range_multiple() {
5307    // Test finding multiple paragraphs in range
5308    let text = r#"Source: foo
5309
5310Package: bar
5311
5312Package: baz
5313"#;
5314    let deb822 = text.parse::<Deb822>().unwrap();
5315
5316    // Create a range that covers first two paragraphs
5317    let range = rowan::TextRange::new(0.into(), 25.into());
5318
5319    // Query paragraphs in that range
5320    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5321    assert_eq!(paras.len(), 2);
5322    assert_eq!(paras[0].get("Source").as_deref(), Some("foo"));
5323    assert_eq!(paras[1].get("Package").as_deref(), Some("bar"));
5324}
5325
5326#[test]
5327fn test_paragraphs_in_range_partial_overlap() {
5328    // Test that paragraphs are included if they partially overlap with the range
5329    let text = r#"Source: foo
5330
5331Package: bar
5332
5333Package: baz
5334"#;
5335    let deb822 = text.parse::<Deb822>().unwrap();
5336
5337    // Create a range that starts in the middle of the second paragraph
5338    let range = rowan::TextRange::new(15.into(), 30.into());
5339
5340    // Should include the second paragraph since it overlaps
5341    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5342    assert!(paras.len() >= 1);
5343    assert!(paras
5344        .iter()
5345        .any(|p| p.get("Package").as_deref() == Some("bar")));
5346}
5347
5348#[test]
5349fn test_paragraphs_in_range_no_match() {
5350    // Test that empty iterator is returned when no paragraphs are in range
5351    let text = r#"Source: foo
5352
5353Package: bar
5354"#;
5355    let deb822 = text.parse::<Deb822>().unwrap();
5356
5357    // Create a range that's way beyond the document
5358    let range = rowan::TextRange::new(1000.into(), 2000.into());
5359
5360    // Should return empty iterator
5361    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5362    assert_eq!(paras.len(), 0);
5363}
5364
5365#[test]
5366fn test_paragraphs_in_range_all() {
5367    // Test finding all paragraphs when range covers entire document
5368    let text = r#"Source: foo
5369
5370Package: bar
5371
5372Package: baz
5373"#;
5374    let deb822 = text.parse::<Deb822>().unwrap();
5375
5376    // Create a range that covers the entire document
5377    let range = rowan::TextRange::new(0.into(), text.len().try_into().unwrap());
5378
5379    // Should return all paragraphs
5380    let paras: Vec<_> = deb822.paragraphs_in_range(range).collect();
5381    assert_eq!(paras.len(), 3);
5382}
5383
5384#[test]
5385fn test_paragraph_at_position() {
5386    // Test finding paragraph at a given text offset
5387    let text = r#"Package: foo
5388Version: 1.0
5389
5390Package: bar
5391Architecture: all
5392"#;
5393    let deb822 = text.parse::<Deb822>().unwrap();
5394
5395    // Position 5 is within first paragraph ("Package: foo")
5396    let para = deb822.paragraph_at_position(rowan::TextSize::from(5));
5397    assert!(para.is_some());
5398    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5399
5400    // Position 30 is within second paragraph
5401    let para = deb822.paragraph_at_position(rowan::TextSize::from(30));
5402    assert!(para.is_some());
5403    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5404
5405    // Position beyond document
5406    let para = deb822.paragraph_at_position(rowan::TextSize::from(1000));
5407    assert!(para.is_none());
5408}
5409
5410#[test]
5411fn test_paragraph_at_line() {
5412    // Test finding paragraph at a given line number
5413    let text = r#"Package: foo
5414Version: 1.0
5415
5416Package: bar
5417Architecture: all
5418"#;
5419    let deb822 = text.parse::<Deb822>().unwrap();
5420
5421    // Line 0 is in first paragraph
5422    let para = deb822.paragraph_at_line(0);
5423    assert!(para.is_some());
5424    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5425
5426    // Line 1 is also in first paragraph
5427    let para = deb822.paragraph_at_line(1);
5428    assert!(para.is_some());
5429    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5430
5431    // Line 3 is in second paragraph
5432    let para = deb822.paragraph_at_line(3);
5433    assert!(para.is_some());
5434    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5435
5436    // Line beyond document
5437    let para = deb822.paragraph_at_line(100);
5438    assert!(para.is_none());
5439}
5440
5441#[test]
5442fn test_entry_at_line_col() {
5443    // Test finding entry at a given line/column position
5444    let text = r#"Package: foo
5445Version: 1.0
5446Architecture: all
5447"#;
5448    let deb822 = text.parse::<Deb822>().unwrap();
5449
5450    // Line 0, column 0 is in "Package: foo"
5451    let entry = deb822.entry_at_line_col(0, 0);
5452    assert!(entry.is_some());
5453    assert_eq!(entry.unwrap().key(), Some("Package".to_string()));
5454
5455    // Line 1, column 0 is in "Version: 1.0"
5456    let entry = deb822.entry_at_line_col(1, 0);
5457    assert!(entry.is_some());
5458    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5459
5460    // Line 2, column 5 is in "Architecture: all"
5461    let entry = deb822.entry_at_line_col(2, 5);
5462    assert!(entry.is_some());
5463    assert_eq!(entry.unwrap().key(), Some("Architecture".to_string()));
5464
5465    // Position beyond document
5466    let entry = deb822.entry_at_line_col(100, 0);
5467    assert!(entry.is_none());
5468}
5469
5470#[test]
5471fn test_entry_at_line_col_multiline() {
5472    // Test finding entry in a multiline value
5473    let text = r#"Package: foo
5474Description: A package
5475 with a long
5476 description
5477Version: 1.0
5478"#;
5479    let deb822 = text.parse::<Deb822>().unwrap();
5480
5481    // Line 1 is the start of Description
5482    let entry = deb822.entry_at_line_col(1, 0);
5483    assert!(entry.is_some());
5484    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5485
5486    // Line 2 is continuation of Description
5487    let entry = deb822.entry_at_line_col(2, 1);
5488    assert!(entry.is_some());
5489    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5490
5491    // Line 3 is also continuation of Description
5492    let entry = deb822.entry_at_line_col(3, 1);
5493    assert!(entry.is_some());
5494    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5495
5496    // Line 4 is Version
5497    let entry = deb822.entry_at_line_col(4, 0);
5498    assert!(entry.is_some());
5499    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5500}
5501
5502#[test]
5503fn test_entries_in_range() {
5504    // Test finding entries in a paragraph within a range
5505    let text = r#"Package: foo
5506Version: 1.0
5507Architecture: all
5508"#;
5509    let deb822 = text.parse::<Deb822>().unwrap();
5510    let para = deb822.paragraphs().next().unwrap();
5511
5512    // Get first entry's range
5513    let first_entry = para.entries().next().unwrap();
5514    let range = first_entry.text_range();
5515
5516    // Query entries in that range - should get only first entry
5517    let entries: Vec<_> = para.entries_in_range(range).collect();
5518    assert_eq!(entries.len(), 1);
5519    assert_eq!(entries[0].key(), Some("Package".to_string()));
5520
5521    // Query with a range covering first two entries
5522    let range = rowan::TextRange::new(0.into(), 25.into());
5523    let entries: Vec<_> = para.entries_in_range(range).collect();
5524    assert_eq!(entries.len(), 2);
5525    assert_eq!(entries[0].key(), Some("Package".to_string()));
5526    assert_eq!(entries[1].key(), Some("Version".to_string()));
5527}
5528
5529#[test]
5530fn test_entries_in_range_partial_overlap() {
5531    // Test that entries with partial overlap are included
5532    let text = r#"Package: foo
5533Version: 1.0
5534Architecture: all
5535"#;
5536    let deb822 = text.parse::<Deb822>().unwrap();
5537    let para = deb822.paragraphs().next().unwrap();
5538
5539    // Create a range that starts in the middle of the second entry
5540    let range = rowan::TextRange::new(15.into(), 30.into());
5541
5542    let entries: Vec<_> = para.entries_in_range(range).collect();
5543    assert!(entries.len() >= 1);
5544    assert!(entries
5545        .iter()
5546        .any(|e| e.key() == Some("Version".to_string())));
5547}
5548
5549#[test]
5550fn test_entries_in_range_no_match() {
5551    // Test that empty iterator is returned when no entries match
5552    let text = "Package: foo\n";
5553    let deb822 = text.parse::<Deb822>().unwrap();
5554    let para = deb822.paragraphs().next().unwrap();
5555
5556    // Range beyond the paragraph
5557    let range = rowan::TextRange::new(1000.into(), 2000.into());
5558    let entries: Vec<_> = para.entries_in_range(range).collect();
5559    assert_eq!(entries.len(), 0);
5560}
5561
5562#[test]
5563fn test_entry_at_position() {
5564    // Test finding entry at a specific text offset
5565    let text = r#"Package: foo
5566Version: 1.0
5567Architecture: all
5568"#;
5569    let deb822 = text.parse::<Deb822>().unwrap();
5570    let para = deb822.paragraphs().next().unwrap();
5571
5572    // Position 5 is within "Package: foo"
5573    let entry = para.entry_at_position(rowan::TextSize::from(5));
5574    assert!(entry.is_some());
5575    assert_eq!(entry.unwrap().key(), Some("Package".to_string()));
5576
5577    // Position 15 is within "Version: 1.0"
5578    let entry = para.entry_at_position(rowan::TextSize::from(15));
5579    assert!(entry.is_some());
5580    assert_eq!(entry.unwrap().key(), Some("Version".to_string()));
5581
5582    // Position beyond paragraph
5583    let entry = para.entry_at_position(rowan::TextSize::from(1000));
5584    assert!(entry.is_none());
5585}
5586
5587#[test]
5588fn test_entry_at_position_multiline() {
5589    // Test finding entry in a multiline value
5590    let text = r#"Description: A package
5591 with a long
5592 description
5593"#;
5594    let deb822 = text.parse::<Deb822>().unwrap();
5595    let para = deb822.paragraphs().next().unwrap();
5596
5597    // Position 5 is within the Description entry
5598    let entry = para.entry_at_position(rowan::TextSize::from(5));
5599    assert!(entry.is_some());
5600    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5601
5602    // Position in continuation line should also find the Description entry
5603    let entry = para.entry_at_position(rowan::TextSize::from(30));
5604    assert!(entry.is_some());
5605    assert_eq!(entry.unwrap().key(), Some("Description".to_string()));
5606}
5607
5608#[test]
5609fn test_paragraph_at_position_at_boundary() {
5610    // Test paragraph_at_position at paragraph boundaries
5611    let text = "Package: foo\n\nPackage: bar\n";
5612    let deb822 = text.parse::<Deb822>().unwrap();
5613
5614    // Position 0 is start of first paragraph
5615    let para = deb822.paragraph_at_position(rowan::TextSize::from(0));
5616    assert!(para.is_some());
5617    assert_eq!(para.unwrap().get("Package").as_deref(), Some("foo"));
5618
5619    // Position at start of second paragraph
5620    let para = deb822.paragraph_at_position(rowan::TextSize::from(15));
5621    assert!(para.is_some());
5622    assert_eq!(para.unwrap().get("Package").as_deref(), Some("bar"));
5623}