1use std::borrow::Cow;
2use std::ops::Deref;
3use std::sync::Arc;
4
5use ruff_diagnostics::SourceMap;
6use ruff_notebook::Notebook;
7use ruff_python_ast::PySourceType;
8use ruff_source_file::LineIndex;
9
10use crate::Db;
11use crate::files::{File, FilePath};
12use crate::system::System;
13
14#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)]
16pub fn source_text(db: &dyn Db, file: File) -> SourceText {
17 let path = file.path(db);
18 let _span = tracing::trace_span!("source_text", file = %path).entered();
19 let mut read_error = None;
20
21 if let Some(source) = file.source_text_override(db) {
22 return source.clone();
23 }
24
25 let kind = if is_notebook(db.system(), path) {
26 file.read_to_notebook(db)
27 .unwrap_or_else(|error| {
28 tracing::debug!("Failed to read notebook '{path}': {error}");
29
30 read_error = Some(SourceTextError::FailedToReadNotebook(error.to_string()));
31 Notebook::empty()
32 })
33 .into()
34 } else {
35 file.read_to_string(db)
36 .unwrap_or_else(|error| {
37 tracing::debug!("Failed to read file '{path}': {error}");
38
39 read_error = Some(SourceTextError::FailedToReadFile(error.to_string()));
40 String::new()
41 })
42 .into()
43 };
44
45 SourceText {
46 inner: Arc::new(SourceTextInner { kind, read_error }),
47 }
48}
49
50fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
51 let source_type = match path {
52 FilePath::System(path) => system.source_type(path),
53 FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
54 FilePath::Vendored(_) => return false,
55 };
56
57 let with_extension_fallback =
58 source_type.or_else(|| PySourceType::try_from_extension(path.extension()?));
59
60 with_extension_fallback == Some(PySourceType::Ipynb)
61}
62
63#[derive(Clone, Eq, PartialEq, get_size2::GetSize)]
69pub struct SourceText {
70 inner: Arc<SourceTextInner>,
71}
72
73impl SourceText {
74 pub fn as_str(&self) -> &str {
76 match &self.inner.kind {
77 SourceTextKind::Text(source) => source,
78 SourceTextKind::Notebook { notebook } => notebook.source_code(),
79 }
80 }
81
82 pub fn as_notebook(&self) -> Option<&Notebook> {
84 match &self.inner.kind {
85 SourceTextKind::Notebook { notebook } => Some(notebook),
86 SourceTextKind::Text(_) => None,
87 }
88 }
89
90 pub fn is_notebook(&self) -> bool {
92 matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
93 }
94
95 pub fn read_error(&self) -> Option<&SourceTextError> {
97 self.inner.read_error.as_ref()
98 }
99
100 #[must_use]
104 pub fn with_text(&self, new_text: String, source_map: &SourceMap) -> Self {
105 let new_kind = match &self.inner.kind {
106 SourceTextKind::Text(_) => SourceTextKind::Text(new_text),
107
108 SourceTextKind::Notebook { notebook } => {
109 let mut new_notebook = notebook.as_ref().clone();
110 new_notebook.update(source_map, new_text);
111 SourceTextKind::Notebook {
112 notebook: new_notebook.into(),
113 }
114 }
115 };
116
117 Self {
118 inner: Arc::new(SourceTextInner {
119 kind: new_kind,
120 read_error: self.inner.read_error.clone(),
121 }),
122 }
123 }
124
125 pub fn to_bytes(&self) -> Cow<'_, [u8]> {
126 match &self.inner.kind {
127 SourceTextKind::Text(source) => Cow::Borrowed(source.as_bytes()),
128 SourceTextKind::Notebook { notebook } => {
129 let mut output: Vec<u8> = Vec::new();
130 notebook
131 .write(&mut output)
132 .expect("writing to a Vec should never fail");
133
134 Cow::Owned(output)
135 }
136 }
137 }
138}
139
140impl Deref for SourceText {
141 type Target = str;
142
143 fn deref(&self) -> &str {
144 self.as_str()
145 }
146}
147
148impl std::fmt::Debug for SourceText {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 let mut dbg = f.debug_tuple("SourceText");
151
152 match &self.inner.kind {
153 SourceTextKind::Text(text) => {
154 dbg.field(text);
155 }
156 SourceTextKind::Notebook { notebook } => {
157 dbg.field(notebook);
158 }
159 }
160
161 dbg.finish()
162 }
163}
164
165#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
166struct SourceTextInner {
167 kind: SourceTextKind,
168 read_error: Option<SourceTextError>,
169}
170
171#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
172enum SourceTextKind {
173 Text(String),
174 Notebook {
175 #[get_size(ignore)]
178 notebook: Box<Notebook>,
179 },
180}
181
182impl From<String> for SourceTextKind {
183 fn from(value: String) -> Self {
184 SourceTextKind::Text(value)
185 }
186}
187
188impl From<Notebook> for SourceTextKind {
189 fn from(notebook: Notebook) -> Self {
190 SourceTextKind::Notebook {
191 notebook: Box::new(notebook),
192 }
193 }
194}
195
196#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, get_size2::GetSize)]
197pub enum SourceTextError {
198 #[error("Failed to read notebook: {0}`")]
199 FailedToReadNotebook(String),
200 #[error("Failed to read file: {0}")]
201 FailedToReadFile(String),
202}
203
204#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)]
206pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
207 let _span = tracing::trace_span!("line_index", ?file).entered();
208
209 let source = source_text(db, file);
210
211 LineIndex::from_source_text(&source)
212}
213
214#[cfg(test)]
215mod tests {
216 use salsa::EventKind;
217 use salsa::Setter as _;
218
219 use ruff_source_file::OneIndexed;
220 use ruff_text_size::TextSize;
221
222 use crate::files::system_path_to_file;
223 use crate::source::{line_index, source_text};
224 use crate::system::{DbWithWritableSystem as _, SystemPath};
225 use crate::tests::TestDb;
226
227 #[test]
228 fn re_runs_query_when_file_revision_changes() -> crate::system::Result<()> {
229 let mut db = TestDb::new();
230 let path = SystemPath::new("test.py");
231
232 db.write_file(path, "x = 10")?;
233
234 let file = system_path_to_file(&db, path).unwrap();
235
236 assert_eq!(source_text(&db, file).as_str(), "x = 10");
237
238 db.write_file(path, "x = 20").unwrap();
239
240 assert_eq!(source_text(&db, file).as_str(), "x = 20");
241
242 Ok(())
243 }
244
245 #[test]
246 fn text_is_cached_if_revision_is_unchanged() -> crate::system::Result<()> {
247 let mut db = TestDb::new();
248 let path = SystemPath::new("test.py");
249
250 db.write_file(path, "x = 10")?;
251
252 let file = system_path_to_file(&db, path).unwrap();
253
254 assert_eq!(source_text(&db, file).as_str(), "x = 10");
255
256 file.set_permissions(&mut db).to(Some(0o777));
258
259 db.clear_salsa_events();
260 assert_eq!(source_text(&db, file).as_str(), "x = 10");
261
262 let events = db.take_salsa_events();
263
264 assert!(
265 !events
266 .iter()
267 .any(|event| matches!(event.kind, EventKind::WillExecute { .. }))
268 );
269
270 Ok(())
271 }
272
273 #[test]
274 fn line_index_for_source() -> crate::system::Result<()> {
275 let mut db = TestDb::new();
276 let path = SystemPath::new("test.py");
277
278 db.write_file(path, "x = 10\ny = 20")?;
279
280 let file = system_path_to_file(&db, path).unwrap();
281 let index = line_index(&db, file);
282 let source = source_text(&db, file);
283
284 assert_eq!(index.line_count(), 2);
285 assert_eq!(
286 index.line_start(OneIndexed::from_zero_indexed(0), source.as_str()),
287 TextSize::new(0)
288 );
289
290 Ok(())
291 }
292
293 #[test]
294 fn notebook() -> crate::system::Result<()> {
295 let mut db = TestDb::new();
296
297 let path = SystemPath::new("test.ipynb");
298 db.write_file(
299 path,
300 r#"
301{
302 "cells": [{"cell_type": "code", "source": ["x = 10"], "metadata": {}, "outputs": []}],
303 "metadata": {
304 "kernelspec": {
305 "display_name": "Python (ruff)",
306 "language": "python",
307 "name": "ruff"
308 },
309 "language_info": {
310 "file_extension": ".py",
311 "mimetype": "text/x-python",
312 "name": "python",
313 "nbconvert_exporter": "python",
314 "pygments_lexer": "ipython3",
315 "version": "3.11.3"
316 }
317 },
318 "nbformat": 4,
319 "nbformat_minor": 4
320}"#,
321 )?;
322
323 let file = system_path_to_file(&db, path).unwrap();
324 let source = source_text(&db, file);
325
326 assert!(source.is_notebook());
327 assert_eq!(source.as_str(), "x = 10\n");
328 assert!(source.as_notebook().is_some());
329
330 Ok(())
331 }
332}