Skip to main content

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