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?
33fn main() {
34 for kb in [10usize, 100, 1000] {
35 let src = body(kb * 1024);
36 println!("=== {} KB ===", src.len() / 1024);
37
38 let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
39 let nodes = ed.nodes().unwrap();
40 let map = wysiwyg::build(
41 &nodes,
42 &src,
43 None,
44 false,
45 &wysiwyg::Surface::default(),
46 None,
47 );
48 println!(" ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
49
50 println!(" -- per edit (unavoidable today) --");
51 time("twig edit_range (reparse)", 5, || {
52 ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
53 });
54 // That loop left five `x`s in the editor, and every block below measures
55 // `ed` against `src` — spans from a document five bytes longer than the
56 // string they index. Re-parse so the two are the same document again.
57 //
58 // Untimed on purpose: this is the bench putting its fixture back, not a
59 // cost leaf pays. Skipping it used to end every run in a slice panic
60 // (`push_escaped_text`, walking a span past the end of a shorter source)
61 // and would otherwise have quietly measured a build over a mismatch.
62 let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
63 time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
64 time("wysiwyg::build", 5, || {
65 wysiwyg::build(
66 &nodes,
67 &src,
68 None,
69 false,
70 &wysiwyg::Surface::default(),
71 None,
72 )
73 .rows
74 .len()
75 });
76 // The source view's whole per-edit cost, next to the WYSIWYG view's, so
77 // the "cheaper view" claim in `source::build`'s docs is a measured one.
78 // It has no incremental path: this plus the marshal above is what a
79 // keystroke in `View::Source` pays.
80 time("source::build", 5, || {
81 source::build(&nodes, &src).runs().len()
82 });
83 {
84 // The incremental path with a warm cache and nothing changed: the
85 // floor cost the block cache adds even on a pure repaint — hash every
86 // block, clone every reused row, recollect stops. No subtree is
87 // marshalled (every block hits). The real keystroke win shows up in
88 // "Doc::insert + rebuild" below, which re-marshals only the edited
89 // block and reuses the rest.
90 let mut cache = wysiwyg::BlockCache::default();
91 let top = ed.child_spans(None).unwrap();
92 let _ = wysiwyg::build_cached(
93 &top,
94 &src,
95 None,
96 false,
97 &wysiwyg::Surface::default(),
98 None,
99 &mut cache,
100 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
101 );
102 time("wysiwyg::build_cached (all reused)", 5, || {
103 let top = ed.child_spans(None).unwrap();
104 wysiwyg::build_cached(
105 &top,
106 &src,
107 None,
108 false,
109 &wysiwyg::Surface::default(),
110 None,
111 &mut cache,
112 |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
113 )
114 .rows
115 .len()
116 });
117 }
118
119 println!(" -- claimed hot, actually noise --");
120 time("twig source_str() (full copy)", 5, || {
121 ed.source_str().unwrap().len()
122 });
123 let clean = src.clone();
124 time("dirty compare (full cmp)", 5, || src == clean);
125
126 println!(" -- what the GUI adds on a cache miss --");
127 time("clone every row's glyphs", 5, || {
128 map.rows
129 .iter()
130 .map(|r| r.glyphs.clone())
131 .collect::<Vec<_>>()
132 .len()
133 });
134 time("hash every glyph (cache key?)", 5, || {
135 let mut n = 0u64;
136 for r in &map.rows {
137 let mut h = std::collections::hash_map::DefaultHasher::new();
138 for g in &r.glyphs {
139 g.ch.hash(&mut h);
140 }
141 n ^= h.finish();
142 }
143 n
144 });
145
146 println!(" -- the whole path, as a frontend calls it --");
147 let mut p = std::env::temp_dir();
148 p.push(format!("leaf_bench_{kb}.md"));
149 std::fs::write(&p, &src).unwrap();
150 let mut d = Doc::open(p).unwrap();
151 d.view = View::Wysiwyg;
152 d.place_caret(src.len() / 2, false);
153 d.build_visual_unwrapped();
154 time("build_visual (cached: a repaint)", 200, || {
155 d.build_visual_unwrapped()
156 });
157 time("Doc::insert + rebuild (a keystroke)", 5, || {
158 d.insert("x");
159 d.build_visual_unwrapped();
160 });
161 println!();
162 }
163}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.