typst_syntax/
source.rs

1//! Source file management.
2
3use std::fmt::{self, Debug, Formatter};
4use std::hash::{Hash, Hasher};
5use std::ops::Range;
6use std::sync::Arc;
7
8use typst_utils::LazyHash;
9
10use crate::lines::Lines;
11use crate::reparser::reparse;
12use crate::{FileId, LinkedNode, Span, SyntaxNode, VirtualPath, parse};
13
14/// A source file.
15///
16/// All line and column indices start at zero, just like byte indices. Only for
17/// user-facing display, you should add 1 to them.
18///
19/// Values of this type are cheap to clone and hash.
20#[derive(Clone)]
21pub struct Source(Arc<Repr>);
22
23/// The internal representation.
24#[derive(Clone)]
25struct Repr {
26    id: FileId,
27    root: LazyHash<SyntaxNode>,
28    lines: LazyHash<Lines<String>>,
29}
30
31impl Source {
32    /// Create a new source file.
33    pub fn new(id: FileId, text: String) -> Self {
34        let _scope = typst_timing::TimingScope::new("create source");
35        let mut root = parse(&text);
36        root.numberize(id, Span::FULL).unwrap();
37        Self(Arc::new(Repr {
38            id,
39            lines: LazyHash::new(Lines::new(text)),
40            root: LazyHash::new(root),
41        }))
42    }
43
44    /// Create a source file without a real id and path, usually for testing.
45    pub fn detached(text: impl Into<String>) -> Self {
46        Self::new(FileId::new(None, VirtualPath::new("main.typ")), text.into())
47    }
48
49    /// The root node of the file's untyped syntax tree.
50    pub fn root(&self) -> &SyntaxNode {
51        &self.0.root
52    }
53
54    /// The id of the source file.
55    pub fn id(&self) -> FileId {
56        self.0.id
57    }
58
59    /// The whole source as a string slice.
60    pub fn text(&self) -> &str {
61        self.0.lines.text()
62    }
63
64    /// An acceleration structure for conversion of UTF-8, UTF-16 and
65    /// line/column indices.
66    pub fn lines(&self) -> &Lines<String> {
67        &self.0.lines
68    }
69
70    /// Fully replace the source text.
71    ///
72    /// This performs a naive (suffix/prefix-based) diff of the old and new text
73    /// to produce the smallest single edit that transforms old into new and
74    /// then calls [`edit`](Self::edit) with it.
75    ///
76    /// Returns the range in the new source that was ultimately reparsed.
77    pub fn replace(&mut self, new: &str) -> Range<usize> {
78        let _scope = typst_timing::TimingScope::new("replace source");
79
80        let Some((prefix, suffix)) = self.0.lines.replacement_range(new) else {
81            return 0..0;
82        };
83
84        let old = self.text();
85        let replace = prefix..old.len() - suffix;
86        let with = &new[prefix..new.len() - suffix];
87        self.edit(replace, with)
88    }
89
90    /// Edit the source file by replacing the given range.
91    ///
92    /// Returns the range in the new source that was ultimately reparsed.
93    ///
94    /// The method panics if the `replace` range is out of bounds.
95    #[track_caller]
96    pub fn edit(&mut self, replace: Range<usize>, with: &str) -> Range<usize> {
97        let inner = Arc::make_mut(&mut self.0);
98
99        // Update the text and lines.
100        inner.lines.edit(replace.clone(), with);
101
102        // Incrementally reparse the replaced range.
103        reparse(&mut inner.root, inner.lines.text(), replace, with.len())
104    }
105
106    /// Find the node with the given span.
107    ///
108    /// Returns `None` if the span does not point into this source file.
109    pub fn find(&self, span: Span) -> Option<LinkedNode<'_>> {
110        LinkedNode::new(self.root()).find(span)
111    }
112
113    /// Get the byte range for the given span in this file.
114    ///
115    /// Returns `None` if the span does not point into this source file.
116    ///
117    /// Typically, it's easier to use `WorldExt::range` instead.
118    pub fn range(&self, span: Span) -> Option<Range<usize>> {
119        Some(self.find(span)?.range())
120    }
121}
122
123impl Debug for Source {
124    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
125        write!(f, "Source({:?})", self.id().vpath())
126    }
127}
128
129impl Hash for Source {
130    fn hash<H: Hasher>(&self, state: &mut H) {
131        self.0.id.hash(state);
132        self.0.lines.hash(state);
133        self.0.root.hash(state);
134    }
135}
136
137impl AsRef<str> for Source {
138    fn as_ref(&self) -> &str {
139        self.text()
140    }
141}