Skip to main content

typst_syntax/
source.rs

1//! Source file management.
2
3use std::fmt::{self, Debug, Formatter};
4use std::ops::Range;
5use std::sync::Arc;
6
7use typst_utils::LazyHash;
8
9use crate::lines::Lines;
10use crate::reparser::reparse;
11use crate::{
12    FileId, LinkedNode, RootedPath, Span, SpanNumber, SubRange, SyntaxNode, VirtualPath,
13    VirtualRoot, parse,
14};
15
16/// A Typst source file containing the full source text, a mapping from byte
17/// indices to lines/columns, and the parsed syntax tree.
18///
19/// All line and column indices start at zero, just like byte indices. Only for
20/// user-facing display, you should add 1 to them.
21///
22/// Values of this type are cheap to clone and hash.
23#[derive(Clone, Hash)]
24pub struct Source(Arc<LazyHash<SourceInner>>);
25
26/// The internal representation of a [`Source`].
27#[derive(Clone, Hash)]
28struct SourceInner {
29    id: FileId,
30    root: SyntaxNode,
31    lines: Lines<String>,
32}
33
34impl Source {
35    /// Create a new source file.
36    pub fn new(id: FileId, text: String) -> Self {
37        let _scope = typst_timing::TimingScope::new("create source");
38        let mut root = parse(&text);
39        root.numberize(id, Span::FULL).unwrap();
40        Self(Arc::new(LazyHash::new(SourceInner { id, lines: Lines::new(text), root })))
41    }
42
43    /// Create a source file without a real id and path, usually for testing.
44    pub fn detached(text: impl Into<String>) -> Self {
45        Self::new(
46            RootedPath::new(VirtualRoot::Project, VirtualPath::new("main.typ").unwrap())
47                .intern(),
48            text.into(),
49        )
50    }
51
52    /// Create a new source file with an already created syntax tree.
53    pub fn with_root(id: FileId, text: String, root: SyntaxNode) -> Self {
54        Self(Arc::new(LazyHash::new(SourceInner { id, lines: Lines::new(text), root })))
55    }
56
57    /// The root node of the file's untyped syntax tree.
58    pub fn root(&self) -> &SyntaxNode {
59        &self.0.root
60    }
61
62    /// The id of the source file.
63    pub fn id(&self) -> FileId {
64        self.0.id
65    }
66
67    /// The whole source as a string slice.
68    pub fn text(&self) -> &str {
69        self.0.lines.text()
70    }
71
72    /// An acceleration structure for conversion of UTF-8, UTF-16 and
73    /// line/column indices.
74    pub fn lines(&self) -> &Lines<String> {
75        &self.0.lines
76    }
77
78    /// Fully replace the source text.
79    ///
80    /// This performs a naive (suffix/prefix-based) diff of the old and new text
81    /// to produce the smallest single edit that transforms old into new and
82    /// then calls [`edit`](Self::edit) with it.
83    ///
84    /// Returns the range in the new source that was ultimately reparsed.
85    pub fn replace(&mut self, new: &str) -> Range<usize> {
86        let _scope = typst_timing::TimingScope::new("replace source");
87
88        let Some((prefix, suffix)) = self.0.lines.replacement_range(new) else {
89            return 0..0;
90        };
91
92        let old = self.text();
93        let replace = prefix..old.len() - suffix;
94        let with = &new[prefix..new.len() - suffix];
95        self.edit(replace, with)
96    }
97
98    /// Edit the source file by replacing the given range.
99    ///
100    /// Returns the range in the new source that was ultimately reparsed.
101    ///
102    /// The method panics if the `replace` range is out of bounds.
103    #[track_caller]
104    pub fn edit(&mut self, replace: Range<usize>, with: &str) -> Range<usize> {
105        let inner = &mut **Arc::make_mut(&mut self.0);
106
107        // Update the text and lines.
108        inner.lines.edit(replace.clone(), with);
109
110        // Incrementally reparse the replaced range.
111        reparse(&mut inner.root, inner.lines.text(), replace, with.len())
112    }
113
114    /// Find the node with the given span.
115    ///
116    /// Returns `None` if the span does not point into this source file.
117    pub fn find(&self, span: Span) -> Option<LinkedNode<'_>> {
118        if span.id() != Some(self.id()) {
119            return None;
120        }
121        LinkedNode::new(self.root()).find(span)
122    }
123
124    /// Get the byte range for the given span number (and optional sub-range) in
125    /// this file.
126    ///
127    /// The main way to get a [`SpanNumber`] is by unpacking a span with
128    /// [`Span::get`], but it's likely easier to use `WorldExt::range` instead.
129    pub fn range(
130        &self,
131        num: SpanNumber,
132        sub_range: Option<SubRange>,
133    ) -> Option<Range<usize>> {
134        let overall = LinkedNode::new(self.root()).find_number(num)?.range();
135        if let Some(sub_range) = sub_range {
136            let range = sub_range.to_absolute(overall.start);
137            assert!(range.end <= overall.end);
138            Some(range)
139        } else {
140            Some(overall)
141        }
142    }
143}
144
145impl Debug for Source {
146    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
147        write!(f, "Source({:?})", self.id().vpath())
148    }
149}
150
151impl AsRef<str> for Source {
152    fn as_ref(&self) -> &str {
153        self.text()
154    }
155}
156
157#[cfg(test)]
158mod test {
159    use super::Source;
160    use crate::{LinkedNode, Side, Span, SubRange};
161
162    #[test]
163    fn test_source_sub_ranges() {
164        let text = "= head <label>";
165        let source = Source::detached(text);
166        let get = |span: Span, sub_range| {
167            let num = crate::SpanNumber(span.number());
168            &text[source.range(num, sub_range).unwrap()]
169        };
170        let head = LinkedNode::new(source.root()).leaf_at(2, Side::After).unwrap().span();
171        assert_eq!(get(head, None), "head");
172        assert_eq!(get(head, SubRange::new(1, 3)), "ea");
173        assert_eq!(get(head, SubRange::new(0, 1)), "h");
174        assert_eq!(get(head, SubRange::new(0, 4)), "head");
175        assert_eq!(get(head, SubRange::new(3, 4)), "d");
176        let root = source.root().span();
177        assert_eq!(get(root, None), text);
178        assert_eq!(get(root, SubRange::new(3, 10)), "ead <la");
179        assert_eq!(get(root, SubRange::new(0, 10)), "= head <la");
180        assert_eq!(get(root, SubRange::new(3, 14)), "ead <label>");
181    }
182}