Editable source files

This commit is contained in:
Laurenz 2021-08-16 14:33:32 +02:00
parent 7e6e7e928c
commit ba6b91e2ee
5 changed files with 86 additions and 25 deletions

View File

@ -51,9 +51,6 @@ pub fn eval(
Ok(Module { scope: ctx.scopes.top, template })
}
/// Caches evaluated modules.
pub type ModuleCache = HashMap<SourceId, Module>;
/// An evaluated module, ready for importing or execution.
#[derive(Debug, Clone, PartialEq)]
pub struct Module {
@ -71,10 +68,10 @@ pub struct EvalContext<'a> {
pub sources: &'a mut SourceStore,
/// Stores decoded images.
pub images: &'a mut ImageStore,
/// Caches evaluated modules.
pub modules: &'a mut ModuleCache,
/// The stack of imported files that led to evaluation of the current file.
pub route: Vec<SourceId>,
/// Caches imported modules.
pub modules: HashMap<SourceId, Module>,
/// The active scopes.
pub scopes: Scopes<'a>,
/// The expression map for the currently built template.
@ -88,8 +85,8 @@ impl<'a> EvalContext<'a> {
loader: ctx.loader.as_ref(),
sources: &mut ctx.sources,
images: &mut ctx.images,
modules: &mut ctx.modules,
route: vec![source],
modules: HashMap::new(),
scopes: Scopes::new(Some(&ctx.std)),
map: ExprMap::new(),
}

View File

@ -140,7 +140,7 @@ impl FontStore {
/// Get a reference to a loaded face.
///
/// This panics if no face with this id was loaded. This function should
/// This panics if no face with this `id` was loaded. This function should
/// only be called with ids returned by this store's
/// [`select()`](Self::select) method.
#[track_caller]

View File

@ -79,7 +79,7 @@ impl ImageStore {
/// Get a reference to a loaded image.
///
/// This panics if no image with this id was loaded. This function should
/// This panics if no image with this `id` was loaded. This function should
/// only be called with ids returned by this store's [`load()`](Self::load)
/// method.
#[track_caller]

View File

@ -50,7 +50,7 @@ pub mod util;
use std::rc::Rc;
use crate::diag::TypResult;
use crate::eval::{ModuleCache, Scope};
use crate::eval::Scope;
use crate::exec::State;
use crate::font::FontStore;
use crate::image::ImageStore;
@ -70,8 +70,6 @@ pub struct Context {
pub fonts: FontStore,
/// Stores decoded images.
pub images: ImageStore,
/// Caches evaluated modules.
pub modules: ModuleCache,
/// Caches layouting artifacts.
#[cfg(feature = "layout-cache")]
pub layouts: LayoutCache,
@ -144,7 +142,6 @@ impl ContextBuilder {
fonts: FontStore::new(Rc::clone(&loader)),
images: ImageStore::new(Rc::clone(&loader)),
loader,
modules: ModuleCache::new(),
#[cfg(feature = "layout-cache")]
layouts: LayoutCache::new(),
std: self.std.unwrap_or(library::new()),

View File

@ -100,9 +100,18 @@ impl SourceStore {
id
}
/// Edit a source file by replacing the given range.
///
/// This panics if no source file with this `id` exists or if the `replace`
/// range is out of bounds for the source file identified by `id`.
#[track_caller]
pub fn edit(&mut self, id: SourceId, replace: Range<usize>, with: &str) {
self.sources[id.0 as usize].edit(replace, with);
}
/// Get a reference to a loaded source file.
///
/// This panics if no source file with this id was loaded. This function
/// This panics if no source file with this `id` exists. This function
/// should only be called with ids returned by this store's
/// [`load()`](Self::load) and [`provide()`](Self::provide) methods.
#[track_caller]
@ -126,17 +135,7 @@ impl SourceFile {
/// Create a new source file.
pub fn new(id: SourceId, path: &Path, src: String) -> Self {
let mut line_starts = vec![0];
let mut s = Scanner::new(&src);
while let Some(c) = s.eat() {
if is_newline(c) {
if c == '\r' {
s.eat_if('\n');
}
line_starts.push(s.index());
}
}
line_starts.extend(newlines(&src));
Self {
id,
path: path.normalize(),
@ -230,6 +229,27 @@ impl SourceFile {
}
Some(range.start + (line.len() - chars.as_str().len()))
}
/// Edit the source file by replacing the given range.
///
/// This panics if the `replace` range is out of bounds.
pub fn edit(&mut self, replace: Range<usize>, with: &str) {
let start = replace.start;
self.src.replace_range(replace, with);
// Remove invalidated line starts.
let line = self.byte_to_line(start).unwrap();
self.line_starts.truncate(line + 1);
// Handle adjoining of \r and \n.
if self.src[.. start].ends_with('\r') && with.starts_with('\n') {
self.line_starts.pop();
}
// Recalculate the line starts after the edit.
self.line_starts
.extend(newlines(&self.src[start ..]).map(|idx| start + idx));
}
}
impl AsRef<str> for SourceFile {
@ -238,6 +258,24 @@ impl AsRef<str> for SourceFile {
}
}
/// The indices at which lines start (right behind newlines).
///
/// The beginning of the string (index 0) is not returned.
fn newlines(string: &str) -> impl Iterator<Item = usize> + '_ {
let mut s = Scanner::new(string);
std::iter::from_fn(move || {
while let Some(c) = s.eat() {
if is_newline(c) {
if c == '\r' {
s.eat_if('\n');
}
return Some(s.index());
}
}
None
})
}
#[cfg(feature = "codespan-reporting")]
impl<'a> Files<'a> for SourceStore {
type FileId = SourceId;
@ -297,7 +335,7 @@ mod tests {
#[test]
fn test_source_file_new() {
let source = SourceFile::detached(TEST);
assert_eq!(source.line_starts, vec![0, 7, 15, 18]);
assert_eq!(source.line_starts, [0, 7, 15, 18]);
}
#[test]
@ -340,4 +378,33 @@ mod tests {
roundtrip(&source, 12);
roundtrip(&source, 21);
}
#[test]
fn test_source_file_edit() {
#[track_caller]
fn test(prev: &str, range: Range<usize>, with: &str, after: &str) {
let mut source = SourceFile::detached(prev);
let result = SourceFile::detached(after);
source.edit(range, with);
assert_eq!(source.src, result.src);
assert_eq!(source.line_starts, result.line_starts);
}
// Test inserting at the begining.
test("abc\n", 0 .. 0, "hi\n", "hi\nabc\n");
test("\nabc", 0 .. 0, "hi\r", "hi\r\nabc");
// Test editing in the middle.
test(TEST, 4 .. 16, "", "ä\tc❌i\rjkl");
// Test appending.
test("abc\ndef", 7 .. 7, "hi", "abc\ndefhi");
test("abc\ndef\n", 8 .. 8, "hi", "abc\ndef\nhi");
// Test appending with adjoining \r and \n.
test("abc\ndef\r", 8 .. 8, "\nghi", "abc\ndef\r\nghi");
// Test removing everything.
test(TEST, 0 .. 21, "", "");
}
}