Skip to main content

iced_markdown_editor/
doc.rs

1//! Rich document model following the CommonMark structure.
2//!
3//! The document is a flat list of leaf blocks (paragraphs, headings, code
4//! blocks, HTML blocks, thematic breaks, or source lines in source mode).
5//! Each block carries the path of container blocks it lives in (block quotes
6//! and list items), outermost first. Two blocks are in the same container
7//! when the container ids are equal. Every character carries its own inline
8//! style, like a word processor. Positions are `(block, character offset)`.
9//!
10//! Special characters inside inline content:
11//! * `'\n'`  hard line break
12//! * [`SOFT`] soft line break (displayed as a space)
13//! * [`OBJ`]  placeholder for an empty link text or an empty image alt
14
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// Soft line break (a line ending inside a paragraph).
19pub const SOFT: char = '\u{2028}';
20/// Placeholder for an empty link text / empty image description.
21pub const OBJ: char = '\u{FFFC}';
22
23static NEXT_ID: AtomicU64 = AtomicU64::new(1);
24
25/// A new unique id for containers and lists.
26pub fn next_id() -> u64 {
27    NEXT_ID.fetch_add(1, Ordering::Relaxed)
28}
29
30/// Link or image target.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct Link {
33    pub url: String,
34    pub title: Option<String>,
35}
36
37impl Link {
38    pub fn new(url: impl Into<String>, title: Option<String>) -> Arc<Self> {
39        Arc::new(Self {
40            url: url.into(),
41            title: title.filter(|title| !title.is_empty()),
42        })
43    }
44}
45
46/// Inline formatting of a single character.
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
48pub struct Style {
49    pub bold: bool,
50    pub italic: bool,
51    pub strike: bool,
52    pub code: bool,
53    pub link: Option<Arc<Link>>,
54    /// The character is part of an image description (alt text).
55    pub image: Option<Arc<Link>>,
56    /// Raw inline HTML.
57    pub html: bool,
58}
59
60/// Toggleable inline formatting flags.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Flag {
63    Bold,
64    Italic,
65    Strike,
66    Code,
67}
68
69impl Style {
70    pub fn get(&self, flag: Flag) -> bool {
71        match flag {
72            Flag::Bold => self.bold,
73            Flag::Italic => self.italic,
74            Flag::Strike => self.strike,
75            Flag::Code => self.code,
76        }
77    }
78
79    pub fn set(&mut self, flag: Flag, value: bool) {
80        match flag {
81            Flag::Bold => self.bold = value,
82            Flag::Italic => self.italic = value,
83            Flag::Strike => self.strike = value,
84            Flag::Code => self.code = value,
85        }
86    }
87}
88
89/// A list shared by its items.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct ListInfo {
92    pub id: u64,
93    pub ordered: bool,
94    /// Number of the first item (ordered lists).
95    pub start: u64,
96    /// Tight lists have no blank lines between their items.
97    pub tight: bool,
98}
99
100impl ListInfo {
101    pub fn new(ordered: bool, start: u64) -> Self {
102        Self {
103            id: next_id(),
104            ordered,
105            start,
106            tight: true,
107        }
108    }
109}
110
111/// A container block.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Container {
114    Quote {
115        id: u64,
116    },
117    Item {
118        list: ListInfo,
119        id: u64,
120        /// Task list item (GFM): `Some(checked)`.
121        task: Option<bool>,
122    },
123}
124
125impl Container {
126    pub fn quote() -> Self {
127        Container::Quote { id: next_id() }
128    }
129
130    pub fn item(list: ListInfo) -> Self {
131        Self::task_item(list, None)
132    }
133
134    pub fn task_item(list: ListInfo, task: Option<bool>) -> Self {
135        Container::Item {
136            list,
137            id: next_id(),
138            task,
139        }
140    }
141
142    /// The same container instance (same quote / same list item).
143    pub fn same(&self, other: &Container) -> bool {
144        match (self, other) {
145            (Container::Quote { id: a }, Container::Quote { id: b }) => a == b,
146            (
147                Container::Item {
148                    list: la, id: a, ..
149                },
150                Container::Item {
151                    list: lb, id: b, ..
152                },
153            ) => la.id == lb.id && a == b,
154            _ => false,
155        }
156    }
157
158    pub fn list(&self) -> Option<ListInfo> {
159        match self {
160            Container::Item { list, .. } => Some(*list),
161            Container::Quote { .. } => None,
162        }
163    }
164
165    pub fn task(&self) -> Option<bool> {
166        match self {
167            Container::Item { task, .. } => *task,
168            Container::Quote { .. } => None,
169        }
170    }
171
172    pub fn is_quote(&self) -> bool {
173        matches!(self, Container::Quote { .. })
174    }
175}
176
177/// Leaf block kind.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum Leaf {
180    Paragraph,
181    Heading(u8),
182    Code,
183    Html,
184    Rule,
185    /// A line of Markdown source (source mode).
186    Line,
187}
188
189impl Leaf {
190    pub fn is_text(self) -> bool {
191        matches!(self, Leaf::Paragraph | Leaf::Heading(_))
192    }
193
194    /// Code and HTML blocks: line breaks stay inside the block.
195    pub fn is_raw(self) -> bool {
196        matches!(self, Leaf::Code | Leaf::Html)
197    }
198
199    /// No inline formatting (code, HTML, source lines).
200    pub fn is_plain(self) -> bool {
201        matches!(self, Leaf::Code | Leaf::Html | Leaf::Line)
202    }
203}
204
205/// Paragraph-level formatting as offered in the toolbar and the API.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum BlockKind {
208    Paragraph,
209    Heading(u8),
210    /// Block quote around the paragraph.
211    Quote,
212    /// Bullet list item.
213    Bullet,
214    /// Numbered list item.
215    Ordered,
216    /// Task list item (`- [ ]`, GFM).
217    Task,
218    Code,
219    Rule,
220    /// Raw HTML block.
221    Html,
222}
223
224impl BlockKind {
225    pub fn is_list(self) -> bool {
226        matches!(
227            self,
228            BlockKind::Bullet | BlockKind::Ordered | BlockKind::Task
229        )
230    }
231}
232
233#[derive(Debug, Clone)]
234pub struct Block {
235    pub containers: Vec<Container>,
236    pub leaf: Leaf,
237    pub chars: Vec<char>,
238    pub styles: Vec<Style>,
239    /// Info string of a fenced code block, e.g. `rust` or `python title="x"`.
240    pub info: Option<Arc<str>>,
241}
242
243impl Block {
244    pub fn new(containers: Vec<Container>, leaf: Leaf) -> Self {
245        Self {
246            containers,
247            leaf,
248            chars: Vec::new(),
249            styles: Vec::new(),
250            info: None,
251        }
252    }
253
254    pub fn len(&self) -> usize {
255        self.chars.len()
256    }
257
258    pub fn is_empty(&self) -> bool {
259        self.chars.is_empty()
260    }
261
262    pub fn push(&mut self, c: char, style: Style) {
263        self.chars.push(c);
264        self.styles.push(style);
265    }
266
267    pub fn text(&self) -> String {
268        self.chars.iter().collect()
269    }
270
271    /// The language: first word of the info string.
272    pub fn language(&self) -> Option<&str> {
273        self.info
274            .as_deref()
275            .and_then(|info| info.split_whitespace().next())
276            .filter(|language| !language.is_empty())
277    }
278
279    /// The innermost container.
280    pub fn inner(&self) -> Option<&Container> {
281        self.containers.last()
282    }
283
284    /// Offset of the first character of the line containing `offset`
285    /// (lines are separated by `\n` inside the block).
286    pub fn line_start(&self, offset: usize) -> usize {
287        let offset = offset.min(self.chars.len());
288        self.chars[..offset]
289            .iter()
290            .rposition(|c| *c == '\n')
291            .map(|i| i + 1)
292            .unwrap_or(0)
293    }
294
295    /// Start offsets of all lines in the block.
296    pub fn line_starts(&self) -> Vec<usize> {
297        let mut starts = vec![0];
298        for (i, c) in self.chars.iter().enumerate() {
299            if *c == '\n' {
300                starts.push(i + 1);
301            }
302        }
303        starts
304    }
305
306    /// The user-facing kind of this block.
307    pub fn kind(&self) -> BlockKind {
308        match self.leaf {
309            Leaf::Heading(level) => BlockKind::Heading(level),
310            Leaf::Code => BlockKind::Code,
311            Leaf::Rule => BlockKind::Rule,
312            Leaf::Html => BlockKind::Html,
313            Leaf::Line => BlockKind::Paragraph,
314            Leaf::Paragraph => match self.inner() {
315                Some(Container::Item { task: Some(_), .. }) => BlockKind::Task,
316                Some(Container::Item { list, .. }) if list.ordered => BlockKind::Ordered,
317                Some(Container::Item { .. }) => BlockKind::Bullet,
318                Some(Container::Quote { .. }) => BlockKind::Quote,
319                None => BlockKind::Paragraph,
320            },
321        }
322    }
323}
324
325/// A caret position.
326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
327pub struct Pos {
328    pub block: usize,
329    pub offset: usize,
330}
331
332impl Pos {
333    pub fn new(block: usize, offset: usize) -> Self {
334        Self { block, offset }
335    }
336}
337
338/// A selection. When `anchor == head` it is a plain caret.
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
340pub struct Selection {
341    pub anchor: Pos,
342    pub head: Pos,
343}
344
345impl Selection {
346    pub fn caret(pos: Pos) -> Self {
347        Self {
348            anchor: pos,
349            head: pos,
350        }
351    }
352
353    pub fn is_collapsed(&self) -> bool {
354        self.anchor == self.head
355    }
356
357    pub fn range(&self) -> (Pos, Pos) {
358        if self.anchor <= self.head {
359            (self.anchor, self.head)
360        } else {
361            (self.head, self.anchor)
362        }
363    }
364}
365
366pub fn is_word_char(c: char) -> bool {
367    c.is_alphanumeric() || c == '_' || c == '\'' || c == '\u{2019}'
368}
369
370fn is_break(c: char) -> bool {
371    c == '\n' || c == SOFT
372}
373
374#[derive(Debug, Clone)]
375pub struct Document {
376    pub blocks: Vec<Block>,
377}
378
379impl Default for Document {
380    fn default() -> Self {
381        Self {
382            blocks: vec![Block::new(Vec::new(), Leaf::Paragraph)],
383        }
384    }
385}
386
387impl Document {
388    pub fn start(&self) -> Pos {
389        Pos::new(0, 0)
390    }
391
392    pub fn end(&self) -> Pos {
393        let last = self.blocks.len() - 1;
394        Pos::new(last, self.blocks[last].len())
395    }
396
397    pub fn clamp(&self, pos: Pos) -> Pos {
398        let block = pos.block.min(self.blocks.len() - 1);
399        Pos::new(block, pos.offset.min(self.blocks[block].len()))
400    }
401
402    pub fn word_count(&self) -> usize {
403        self.blocks
404            .iter()
405            .map(|block| {
406                block
407                    .text()
408                    .split(|c: char| c.is_whitespace() || c == OBJ)
409                    .filter(|word| !word.is_empty())
410                    .count()
411            })
412            .sum()
413    }
414
415    /// Per-block character ranges `(block, start, end)` covered by `a..b`.
416    pub fn spans(&self, a: Pos, b: Pos) -> Vec<(usize, usize, usize)> {
417        (a.block..=b.block)
418            .map(|index| {
419                let start = if index == a.block { a.offset } else { 0 };
420                let end = if index == b.block {
421                    b.offset
422                } else {
423                    self.blocks[index].len()
424                };
425                (index, start, end.max(start))
426            })
427            .collect()
428    }
429
430    /// Index of the first block of the container `depth` of `block`
431    /// (walking back while the previous block shares it).
432    pub fn container_start(&self, block: usize, depth: usize) -> usize {
433        let container = self.blocks[block].containers[depth];
434        let mut index = block;
435        while index > 0 {
436            let previous = &self.blocks[index - 1];
437            if previous
438                .containers
439                .get(depth)
440                .is_some_and(|other| other.same(&container))
441            {
442                index -= 1;
443            } else {
444                break;
445            }
446        }
447        index
448    }
449
450    /// Index one past the last block of the container `depth` of `block`.
451    pub fn container_end(&self, block: usize, depth: usize) -> usize {
452        let container = self.blocks[block].containers[depth];
453        let mut index = block + 1;
454        while index < self.blocks.len()
455            && self.blocks[index]
456                .containers
457                .get(depth)
458                .is_some_and(|other| other.same(&container))
459        {
460            index += 1;
461        }
462        index
463    }
464
465    /// Sets the task state of the list item at `depth` of `block` (on all
466    /// blocks of that item). `None` turns it into a normal list item.
467    pub fn set_task(&mut self, block: usize, depth: usize, task: Option<bool>) {
468        let start = self.container_start(block, depth);
469        let end = self.container_end(block, depth);
470        for block in &mut self.blocks[start..end] {
471            if let Some(Container::Item { task: value, .. }) = block.containers.get_mut(depth) {
472                *value = task;
473            }
474        }
475    }
476
477    // ----------------------------------------------------------------
478    // Editing
479    // ----------------------------------------------------------------
480
481    /// Deletes `a..b` and returns the resulting caret position.
482    pub fn delete_range(&mut self, a: Pos, b: Pos) -> Pos {
483        if a >= b {
484            return a;
485        }
486
487        if a.block == b.block {
488            let block = &mut self.blocks[a.block];
489            block.chars.drain(a.offset..b.offset);
490            block.styles.drain(a.offset..b.offset);
491        } else {
492            let tail_chars: Vec<char> = self.blocks[b.block].chars[b.offset..].to_vec();
493            let tail_styles: Vec<Style> = self.blocks[b.block].styles[b.offset..].to_vec();
494
495            {
496                let first = &mut self.blocks[a.block];
497                first.chars.truncate(a.offset);
498                first.styles.truncate(a.offset);
499                first.chars.extend(tail_chars);
500                first.styles.extend(tail_styles);
501                if first.leaf == Leaf::Rule && !first.chars.is_empty() {
502                    first.leaf = Leaf::Paragraph;
503                }
504            }
505
506            self.blocks.drain(a.block + 1..=b.block);
507        }
508
509        a
510    }
511
512    /// Inserts a single character without any paragraph handling.
513    pub fn insert_char(&mut self, at: Pos, c: char, style: &Style) -> Pos {
514        let block = &mut self.blocks[at.block];
515        if block.leaf == Leaf::Rule {
516            block.leaf = Leaf::Paragraph;
517        }
518        let style = if block.leaf.is_plain() {
519            Style::default()
520        } else {
521            style.clone()
522        };
523        block.chars.insert(at.offset, c);
524        block.styles.insert(at.offset, style);
525        Pos::new(at.block, at.offset + 1)
526    }
527
528    /// Inserts text. Newlines create new paragraphs (or line breaks
529    /// inside code and HTML blocks).
530    pub fn insert_text(&mut self, at: Pos, text: &str, style: &Style) -> Pos {
531        let mut pos = at;
532        for c in text.chars() {
533            match c {
534                '\r' => {}
535                '\n' => {
536                    if self.blocks[pos.block].leaf.is_raw() {
537                        pos = self.insert_char(pos, '\n', style);
538                    } else {
539                        pos = self.split_block(pos);
540                    }
541                }
542                '\t' => pos = self.insert_char(pos, '\t', style),
543                c if c.is_control() => {}
544                c => pos = self.insert_char(pos, c, style),
545            }
546        }
547        pos
548    }
549
550    /// Inserts `count` spaces at `offset` of `block` (unstyled).
551    pub fn insert_spaces(&mut self, block: usize, offset: usize, count: usize) {
552        let block = &mut self.blocks[block];
553        let offset = offset.min(block.chars.len());
554        block
555            .chars
556            .splice(offset..offset, std::iter::repeat(' ').take(count));
557        block.styles.splice(
558            offset..offset,
559            std::iter::repeat(Style::default()).take(count),
560        );
561    }
562
563    /// Splits the block at `at` (the Enter key). In a list the new block
564    /// becomes a new list item (an unchecked task in a task list); later
565    /// blocks of the old item move into it.
566    pub fn split_block(&mut self, at: Pos) -> Pos {
567        let old = &self.blocks[at.block];
568        let len = old.len();
569        let leaf = match old.leaf {
570            Leaf::Heading(_) if at.offset >= len => Leaf::Paragraph,
571            Leaf::Rule => Leaf::Paragraph,
572            other => other,
573        };
574        let info = if leaf == Leaf::Code {
575            old.info.clone()
576        } else {
577            None
578        };
579        let mut containers = old.containers.clone();
580        let item_depth = containers
581            .iter()
582            .rposition(|c| matches!(c, Container::Item { .. }));
583        let mut moved: Option<(usize, Container, Container)> = None;
584        if let Some(depth) = item_depth.filter(|depth| *depth + 1 == containers.len()) {
585            if let Container::Item { list, task, .. } = containers[depth] {
586                let old_item = containers[depth];
587                let new_item = Container::task_item(list, task.map(|_| false));
588                containers[depth] = new_item;
589                moved = Some((depth, old_item, new_item));
590            }
591        }
592
593        let block = &mut self.blocks[at.block];
594        let chars = block.chars.split_off(at.offset);
595        let styles = block.styles.split_off(at.offset);
596
597        self.blocks.insert(
598            at.block + 1,
599            Block {
600                containers,
601                leaf,
602                chars,
603                styles,
604                info,
605            },
606        );
607
608        // Blocks that followed in the old item now belong to the new one.
609        if let Some((depth, old_item, new_item)) = moved {
610            let mut index = at.block + 2;
611            while index < self.blocks.len()
612                && self.blocks[index]
613                    .containers
614                    .get(depth)
615                    .is_some_and(|c| c.same(&old_item))
616            {
617                self.blocks[index].containers[depth] = new_item;
618                index += 1;
619            }
620        }
621
622        Pos::new(at.block + 1, 0)
623    }
624
625    /// Joins `block` onto the previous block (Backspace at block start).
626    pub fn merge_with_previous(&mut self, block: usize) -> Pos {
627        if block == 0 || block >= self.blocks.len() {
628            return Pos::new(block.min(self.blocks.len() - 1), 0);
629        }
630
631        if self.blocks[block - 1].leaf == Leaf::Rule {
632            self.blocks.remove(block - 1);
633            return Pos::new(block - 1, 0);
634        }
635
636        let current = self.blocks.remove(block);
637        let previous = &mut self.blocks[block - 1];
638        let offset = previous.len();
639        let plain = previous.leaf.is_plain();
640        previous.chars.extend(current.chars);
641        if plain {
642            previous
643                .styles
644                .extend(std::iter::repeat(Style::default()).take(current.styles.len()));
645        } else {
646            previous.styles.extend(current.styles);
647        }
648        Pos::new(block - 1, offset)
649    }
650
651    // ----------------------------------------------------------------
652    // Formatting
653    // ----------------------------------------------------------------
654
655    pub fn all_have(&self, a: Pos, b: Pos, flag: Flag) -> bool {
656        let mut any = false;
657        for (index, start, end) in self.spans(a, b) {
658            let block = &self.blocks[index];
659            if block.leaf.is_plain() {
660                continue;
661            }
662            for (c, style) in block.chars[start..end]
663                .iter()
664                .zip(&block.styles[start..end])
665            {
666                if is_break(*c) {
667                    continue;
668                }
669                any = true;
670                if !style.get(flag) {
671                    return false;
672                }
673            }
674        }
675        any
676    }
677
678    pub fn set_flag(&mut self, a: Pos, b: Pos, flag: Flag, value: bool) {
679        for (index, start, end) in self.spans(a, b) {
680            if self.blocks[index].leaf.is_plain() {
681                continue;
682            }
683            for style in &mut self.blocks[index].styles[start..end] {
684                if !style.html {
685                    style.set(flag, value);
686                }
687            }
688        }
689    }
690
691    pub fn set_link(&mut self, a: Pos, b: Pos, link: Option<Arc<Link>>) {
692        for (index, start, end) in self.spans(a, b) {
693            if self.blocks[index].leaf.is_plain() {
694                continue;
695            }
696            for style in &mut self.blocks[index].styles[start..end] {
697                if !style.html {
698                    style.link = link.clone();
699                }
700            }
701        }
702    }
703
704    pub fn link_at(&self, pos: Pos) -> Option<Arc<Link>> {
705        self.blocks[pos.block]
706            .styles
707            .get(pos.offset)
708            .and_then(|style| style.link.clone())
709    }
710
711    /// The style that newly typed text receives at `pos`.
712    pub fn typing_style(&self, pos: Pos) -> Style {
713        let block = &self.blocks[pos.block];
714        if block.leaf.is_plain() || block.styles.is_empty() {
715            return Style::default();
716        }
717
718        let mut style = if pos.offset == 0 {
719            let mut style = block.styles[0].clone();
720            style.link = None;
721            style
722        } else {
723            let mut style = block.styles[pos.offset - 1].clone();
724            let next_link = block
725                .styles
726                .get(pos.offset)
727                .and_then(|next| next.link.clone());
728            if style.link != next_link {
729                style.link = None;
730            }
731            style
732        };
733        // Typing never extends an image description or raw HTML.
734        style.image = None;
735        style.html = false;
736        style
737    }
738
739    pub fn plain_text(&self, a: Pos, b: Pos) -> String {
740        let mut out = String::new();
741        for (index, start, end) in self.spans(a, b) {
742            for c in &self.blocks[index].chars[start..end] {
743                match *c {
744                    SOFT => out.push(' '),
745                    OBJ => {}
746                    c => out.push(c),
747                }
748            }
749            if index != b.block {
750                out.push('\n');
751            }
752        }
753        out
754    }
755
756    // ----------------------------------------------------------------
757    // Navigation
758    // ----------------------------------------------------------------
759
760    pub fn prev_pos(&self, pos: Pos) -> Pos {
761        if pos.offset > 0 {
762            Pos::new(pos.block, pos.offset - 1)
763        } else if pos.block > 0 {
764            Pos::new(pos.block - 1, self.blocks[pos.block - 1].len())
765        } else {
766            pos
767        }
768    }
769
770    pub fn next_pos(&self, pos: Pos) -> Pos {
771        if pos.offset < self.blocks[pos.block].len() {
772            Pos::new(pos.block, pos.offset + 1)
773        } else if pos.block + 1 < self.blocks.len() {
774            Pos::new(pos.block + 1, 0)
775        } else {
776            pos
777        }
778    }
779
780    pub fn word_left(&self, pos: Pos) -> Pos {
781        if pos.offset == 0 {
782            return self.prev_pos(pos);
783        }
784        let chars = &self.blocks[pos.block].chars;
785        let mut offset = pos.offset;
786        while offset > 0 && chars[offset - 1].is_whitespace() {
787            offset -= 1;
788        }
789        if offset > 0 && is_word_char(chars[offset - 1]) {
790            while offset > 0 && is_word_char(chars[offset - 1]) {
791                offset -= 1;
792            }
793        } else if offset > 0 {
794            offset -= 1;
795        }
796        Pos::new(pos.block, offset)
797    }
798
799    pub fn word_right(&self, pos: Pos) -> Pos {
800        let chars = &self.blocks[pos.block].chars;
801        let len = chars.len();
802        if pos.offset >= len {
803            return self.next_pos(pos);
804        }
805        let mut offset = pos.offset;
806        if is_word_char(chars[offset]) {
807            while offset < len && is_word_char(chars[offset]) {
808                offset += 1;
809            }
810        } else if !chars[offset].is_whitespace() {
811            offset += 1;
812        }
813        while offset < len && chars[offset].is_whitespace() {
814            offset += 1;
815        }
816        Pos::new(pos.block, offset)
817    }
818
819    /// The word around `pos` (used by double click and formatting).
820    pub fn word_at(&self, pos: Pos) -> (Pos, Pos) {
821        let chars = &self.blocks[pos.block].chars;
822        let mut start = pos.offset.min(chars.len());
823        let mut end = start;
824        while start > 0 && is_word_char(chars[start - 1]) {
825            start -= 1;
826        }
827        while end < chars.len() && is_word_char(chars[end]) {
828            end += 1;
829        }
830        if start == end && end < chars.len() {
831            end += 1;
832        }
833        (Pos::new(pos.block, start), Pos::new(pos.block, end))
834    }
835}