pub struct SourceMap { /* private fields */ }Expand description
The source view’s styling, as non-overlapping runs in ascending order.
Gaps between runs are Role::Body — the map stores only what differs from
plain text, so an ordinary prose document is a handful of runs rather than
one per byte.
Built by build and cached on the Doc against its
revision; a frontend reads it through SourceMap::style_at for a one-shot
question, or SourceMap::edges_in when it is already walking lines in
order and wants to know where the styling changes.
Implementations§
Source§impl SourceMap
impl SourceMap
Sourcepub fn runs(&self) -> &[StyledRun]
pub fn runs(&self) -> &[StyledRun]
The styled runs, ascending and non-overlapping. Bytes between them are
Style::default.
Examples found in repository?
34fn main() {
35 for kb in [10usize, 100, 1000] {
36 let src = body(kb * 1024);
37 println!("=== {} KB ===", src.len() / 1024);
38
39 let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40 let nodes = ed.nodes().unwrap();
41 let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42 println!(" ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44 println!(" -- per edit (unavoidable today) --");
45 time("twig edit_range (reparse)", 5, || {
46 ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47 });
48 // That loop left five `x`s in the editor, and every block below measures
49 // `ed` against `src` — spans from a document five bytes longer than the
50 // string they index. Re-parse so the two are the same document again.
51 //
52 // Untimed on purpose: this is the bench putting its fixture back, not a
53 // cost leaf pays. Skipping it used to end every run in a slice panic
54 // (`push_escaped_text`, walking a span past the end of a shorter source)
55 // and would otherwise have quietly measured a build over a mismatch.
56 let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
57 time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
58 time("wysiwyg::build", 5, || {
59 wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
60 .rows
61 .len()
62 });
63 // The source view's whole per-edit cost, next to the WYSIWYG view's, so
64 // the "cheaper view" claim in `source::build`'s docs is a measured one.
65 // It has no incremental path: this plus the marshal above is what a
66 // keystroke in `View::Source` pays.
67 time("source::build", 5, || {
68 source::build(&nodes, &src).runs().len()
69 });
70 {
71 // The incremental path with a warm cache and nothing changed: the
72 // floor cost the block cache adds even on a pure repaint — hash every
73 // block, clone every reused row, recollect stops. No subtree is
74 // marshalled (every block hits). The real keystroke win shows up in
75 // "Doc::insert + rebuild" below, which re-marshals only the edited
76 // block and reuses the rest.
77 let mut cache = wysiwyg::BlockCache::default();
78 let top = ed.child_spans(None).unwrap();
79 let _ = wysiwyg::build_cached(
80 &top,
81 &src,
82 None,
83 false,
84 &HashMap::new(),
85 None,
86 &mut cache,
87 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
88 );
89 time("wysiwyg::build_cached (all reused)", 5, || {
90 let top = ed.child_spans(None).unwrap();
91 wysiwyg::build_cached(
92 &top,
93 &src,
94 None,
95 false,
96 &HashMap::new(),
97 None,
98 &mut cache,
99 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
100 )
101 .rows
102 .len()
103 });
104 }
105
106 println!(" -- claimed hot, actually noise --");
107 time("twig source_str() (full copy)", 5, || {
108 ed.source_str().unwrap().len()
109 });
110 let clean = src.clone();
111 time("dirty compare (full cmp)", 5, || src == clean);
112
113 println!(" -- what the GUI adds on a cache miss --");
114 time("clone every row's glyphs", 5, || {
115 map.rows
116 .iter()
117 .map(|r| r.glyphs.clone())
118 .collect::<Vec<_>>()
119 .len()
120 });
121 time("hash every glyph (cache key?)", 5, || {
122 let mut n = 0u64;
123 for r in &map.rows {
124 let mut h = std::collections::hash_map::DefaultHasher::new();
125 for g in &r.glyphs {
126 g.ch.hash(&mut h);
127 }
128 n ^= h.finish();
129 }
130 n
131 });
132
133 println!(" -- the whole path, as a frontend calls it --");
134 let mut p = std::env::temp_dir();
135 p.push(format!("leaf_bench_{kb}.md"));
136 std::fs::write(&p, &src).unwrap();
137 let mut d = Doc::open(p).unwrap();
138 d.view = View::Wysiwyg;
139 d.place_caret(src.len() / 2, false);
140 d.build_visual_unwrapped();
141 time("build_visual (cached: a repaint)", 200, || {
142 d.build_visual_unwrapped()
143 });
144 time("Doc::insert + rebuild (a keystroke)", 5, || {
145 d.insert("x");
146 d.build_visual_unwrapped();
147 });
148 println!();
149 }
150}Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Whether the map styles nothing — a document with no markup in it, or one that has not been built yet.
Sourcepub fn style_at(&self, offset: usize) -> Style
pub fn style_at(&self, offset: usize) -> Style
The style covering source byte offset, or Style::default where no
run does.
A binary search, for a caller asking about one offset. A painter walking
the document in order should use edges_in instead and
ask once per run rather than once per byte.
Sourcepub fn edges_in(&self, range: Range<usize>, out: &mut Vec<usize>)
pub fn edges_in(&self, range: Range<usize>, out: &mut Vec<usize>)
Append every styling boundary strictly inside range to out, in
ascending order — the offsets where a painter has to break a span
because the style changes there.
Both edges of every overlapping run, since a run that starts inside the range and one that ends inside it are equally a place the color changes. The range’s own ends are left to the caller, which already has them.