649 lines
23 KiB
Rust
649 lines
23 KiB
Rust
//! Accepted graphical and source state, committed and restored together.
|
|
//! Editor drafts and filesystem receipts belong to their respective adapters.
|
|
use oa_core::{
|
|
Command, Id, ModelError, Project, language, prepare_command,
|
|
source_project::{self, CompiledSourceProject, ProjectLimits, SourceBundle, SourceDiagnostic},
|
|
validation::Severity,
|
|
};
|
|
use std::{fmt, sync::Arc};
|
|
|
|
const HISTORY_LIMIT: usize = 100;
|
|
const HISTORY_BYTES: usize = 32 * 1024 * 1024;
|
|
|
|
struct HistoryEntry {
|
|
snapshot: Arc<CompiledSourceProject>,
|
|
bytes: usize,
|
|
}
|
|
|
|
fn estimated_bytes(compiled: &CompiledSourceProject) -> usize {
|
|
compiled
|
|
.bundle
|
|
.files
|
|
.values()
|
|
.fold(0usize, |sum, text| sum.saturating_add(text.len()))
|
|
.saturating_add(
|
|
serde_json::to_vec(&compiled.decoded.project).map_or(usize::MAX, |bytes| bytes.len()),
|
|
)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum DocumentError {
|
|
Model(ModelError),
|
|
Source(Vec<SourceDiagnostic>),
|
|
}
|
|
|
|
impl DocumentError {
|
|
pub fn diagnostics(&self) -> &[SourceDiagnostic] {
|
|
match self {
|
|
Self::Model(_) => &[],
|
|
Self::Source(diagnostics) => diagnostics,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DocumentError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Model(error) => error.fmt(formatter),
|
|
Self::Source(diagnostics) => {
|
|
if let Some(first) = diagnostics.first() {
|
|
write!(
|
|
formatter,
|
|
"{}:{}:{}: {}",
|
|
first.path,
|
|
first.diagnostic.line,
|
|
first.diagnostic.column,
|
|
first.diagnostic.message
|
|
)
|
|
} else {
|
|
formatter.write_str("Source validation failed.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DocumentError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Self::Model(error) => Some(error),
|
|
Self::Source(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ModelError> for DocumentError {
|
|
fn from(error: ModelError) -> Self {
|
|
Self::Model(error)
|
|
}
|
|
}
|
|
|
|
impl From<Vec<SourceDiagnostic>> for DocumentError {
|
|
fn from(diagnostics: Vec<SourceDiagnostic>) -> Self {
|
|
Self::Source(diagnostics)
|
|
}
|
|
}
|
|
|
|
pub struct Document {
|
|
current: Arc<CompiledSourceProject>,
|
|
past: Vec<HistoryEntry>,
|
|
future: Vec<HistoryEntry>,
|
|
current_bytes: usize,
|
|
saved: Arc<SourceBundle>,
|
|
revision: u64,
|
|
limits: ProjectLimits,
|
|
}
|
|
|
|
impl Document {
|
|
/// A valid generated workspace starts clean, like a new semantic Session.
|
|
pub fn from_project(project: Project, limits: ProjectLimits) -> Result<Self, DocumentError> {
|
|
oa_core::validate_candidate(&project)?;
|
|
let document = Self::from_bundle(source_project::format_bundle(&project), limits)?;
|
|
if document.project() != &project {
|
|
return Err(DocumentError::Model(ModelError(
|
|
"Generated source does not reproduce the supplied project.".into(),
|
|
)));
|
|
}
|
|
Ok(document)
|
|
}
|
|
|
|
/// Retain the exact input as the saved baseline. If compilation inserts
|
|
/// missing IDs, those accepted source changes still need saving.
|
|
pub fn from_bundle(bundle: SourceBundle, limits: ProjectLimits) -> Result<Self, DocumentError> {
|
|
let current = Arc::new(source_project::compile_bundle(&bundle, limits)?);
|
|
let current_bytes = estimated_bytes(¤t);
|
|
Ok(Self {
|
|
current,
|
|
current_bytes,
|
|
past: Vec::new(),
|
|
future: Vec::new(),
|
|
saved: Arc::new(bundle),
|
|
revision: 0,
|
|
limits,
|
|
})
|
|
}
|
|
|
|
pub fn project(&self) -> &Project {
|
|
&self.current.decoded.project
|
|
}
|
|
|
|
pub fn bundle(&self) -> &SourceBundle {
|
|
&self.current.bundle
|
|
}
|
|
|
|
pub fn compiled(&self) -> &CompiledSourceProject {
|
|
&self.current
|
|
}
|
|
|
|
pub fn revision(&self) -> u64 {
|
|
self.revision
|
|
}
|
|
|
|
pub fn dirty(&self) -> bool {
|
|
self.bundle() != self.saved.as_ref()
|
|
}
|
|
|
|
pub fn can_undo(&self) -> bool {
|
|
!self.past.is_empty()
|
|
}
|
|
|
|
pub fn can_redo(&self) -> bool {
|
|
!self.future.is_empty()
|
|
}
|
|
|
|
/// Capture this before asynchronous saving. Completion must return the same
|
|
/// snapshot, rather than marking whatever is current at completion as saved.
|
|
pub fn snapshot(&self) -> Arc<CompiledSourceProject> {
|
|
self.current.clone()
|
|
}
|
|
|
|
/// Records the bytes actually saved, even when later edits remain current.
|
|
/// The I/O adapter owns document identity and save completion ordering.
|
|
pub fn mark_saved(&mut self, snapshot: Arc<CompiledSourceProject>) {
|
|
self.saved = Arc::new(snapshot.bundle.clone());
|
|
}
|
|
|
|
pub fn execute(&mut self, command: Command) -> Result<Option<Id>, DocumentError> {
|
|
let candidate = prepare_command(self.project(), &command)?;
|
|
// Equal model values can still carry different record provenance, such
|
|
// as deleting and recreating an identical record with a reused UUID.
|
|
// Only the resulting model/source pair determines whether this is a no-op.
|
|
let compiled = source_project::update_bundle_with_identity_map(
|
|
&self.current,
|
|
&candidate.project,
|
|
&candidate.identity_map,
|
|
self.limits,
|
|
)?;
|
|
self.commit(compiled);
|
|
Ok(candidate.selected)
|
|
}
|
|
|
|
pub fn apply_bundle(&mut self, bundle: SourceBundle) -> Result<bool, DocumentError> {
|
|
let compiled = source_project::compile_bundle(&bundle, self.limits)?;
|
|
Ok(self.commit(compiled))
|
|
}
|
|
|
|
/// Public compiled fields are convenient data, not a validation capability.
|
|
/// Rebuild diagnostics/index and reject a stale or altered model/index pair.
|
|
pub fn apply_compiled(
|
|
&mut self,
|
|
compiled: CompiledSourceProject,
|
|
) -> Result<bool, DocumentError> {
|
|
let checked = source_project::compile_bundle(&compiled.bundle, self.limits)?;
|
|
if checked.bundle != compiled.bundle
|
|
|| checked.decoded.project != compiled.decoded.project
|
|
|| checked.index != compiled.index
|
|
{
|
|
return Err(DocumentError::Source(vec![SourceDiagnostic {
|
|
path: compiled.bundle.entry,
|
|
diagnostic: language::Diagnostic {
|
|
severity: Severity::Error,
|
|
code: "stale_source_index".into(),
|
|
object_id: None,
|
|
field: None,
|
|
line: 1,
|
|
column: 1,
|
|
message: "Compile the current documents before applying this source snapshot."
|
|
.into(),
|
|
span: language::Span::default(),
|
|
},
|
|
related: Vec::new(),
|
|
}]));
|
|
}
|
|
Ok(self.commit(checked))
|
|
}
|
|
|
|
pub fn undo(&mut self) -> bool {
|
|
let Some(previous) = self.past.pop() else {
|
|
return false;
|
|
};
|
|
self.future.push(HistoryEntry {
|
|
snapshot: std::mem::replace(&mut self.current, previous.snapshot),
|
|
bytes: std::mem::replace(&mut self.current_bytes, previous.bytes),
|
|
});
|
|
self.revision = self.revision.saturating_add(1);
|
|
true
|
|
}
|
|
|
|
pub fn redo(&mut self) -> bool {
|
|
let Some(next) = self.future.pop() else {
|
|
return false;
|
|
};
|
|
self.past.push(HistoryEntry {
|
|
snapshot: std::mem::replace(&mut self.current, next.snapshot),
|
|
bytes: std::mem::replace(&mut self.current_bytes, next.bytes),
|
|
});
|
|
self.revision = self.revision.saturating_add(1);
|
|
true
|
|
}
|
|
|
|
fn commit(&mut self, compiled: CompiledSourceProject) -> bool {
|
|
if compiled.bundle == self.current.bundle {
|
|
return false;
|
|
}
|
|
let bytes = estimated_bytes(&compiled);
|
|
self.past.push(HistoryEntry {
|
|
snapshot: std::mem::replace(&mut self.current, Arc::new(compiled)),
|
|
bytes: std::mem::replace(&mut self.current_bytes, bytes),
|
|
});
|
|
self.future.clear();
|
|
let mut retained = self.past.iter().fold(self.current_bytes, |sum, entry| {
|
|
sum.saturating_add(entry.bytes)
|
|
});
|
|
while !self.past.is_empty() && (self.past.len() > HISTORY_LIMIT || retained > HISTORY_BYTES)
|
|
{
|
|
retained = retained.saturating_sub(self.past.remove(0).bytes);
|
|
}
|
|
self.revision = self.revision.saturating_add(1);
|
|
true
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use oa_core::{Kind, Layer};
|
|
|
|
fn document() -> Document {
|
|
Document::from_project(Project::empty(), ProjectLimits::default()).unwrap()
|
|
}
|
|
|
|
fn comment(bundle: &SourceBundle, text: &str) -> SourceBundle {
|
|
let mut changed = bundle.clone();
|
|
changed
|
|
.files
|
|
.get_mut(&changed.entry)
|
|
.unwrap()
|
|
.push_str(text);
|
|
changed
|
|
}
|
|
|
|
#[test]
|
|
fn creation_selects_the_single_uuid_persisted_in_source() {
|
|
let mut document = document();
|
|
let id = document
|
|
.execute(Command::AddElement {
|
|
kind: Kind::Activity,
|
|
layer: Layer::Oa,
|
|
name: "Assess incident".into(),
|
|
parent: None,
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(document.project().elements.len(), 1);
|
|
assert_eq!(document.project().elements[&id].name, "Assess incident");
|
|
let location = &document.compiled().index.declarations[&id];
|
|
let declaration =
|
|
&document.bundle().files[&location.path][location.span.start..location.span.end];
|
|
assert!(declaration.contains(&id.to_string()));
|
|
let reopened =
|
|
source_project::compile_bundle(document.bundle(), ProjectLimits::default()).unwrap();
|
|
assert_eq!(reopened.decoded.project, *document.project());
|
|
assert_eq!(document.revision(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_compile_command_and_writeback_preserve_state_and_redo() {
|
|
let bundle = source_project::format_bundle(&Project::empty());
|
|
let bytes = bundle.files.values().map(String::len).sum::<usize>();
|
|
let mut limits = ProjectLimits::default();
|
|
limits.model.max_source_bytes = bytes + 100;
|
|
let mut document = Document::from_bundle(bundle, limits).unwrap();
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// preserved future\n"))
|
|
.unwrap();
|
|
assert!(document.undo());
|
|
let before = document.snapshot();
|
|
let revision = document.revision();
|
|
let history = (document.past.len(), document.future.len());
|
|
|
|
let malformed = comment(document.bundle(), "?");
|
|
assert!(document.apply_bundle(malformed).is_err());
|
|
assert!(document.execute(Command::Delete(Id::new_v4())).is_err());
|
|
let error = document
|
|
.execute(Command::Rename(document.project().id, "x".repeat(200)))
|
|
.unwrap_err();
|
|
assert!(!error.diagnostics().is_empty());
|
|
assert!(error.to_string().contains(&document.bundle().entry));
|
|
assert!(Arc::ptr_eq(&before, &document.snapshot()));
|
|
assert_eq!(document.revision(), revision);
|
|
assert_eq!((document.past.len(), document.future.len()), history);
|
|
assert!(!document.dirty());
|
|
assert!(document.redo());
|
|
}
|
|
|
|
#[test]
|
|
fn comment_and_semantic_history_restore_exact_bytes_and_branch_together() {
|
|
let mut document = document();
|
|
let initial = document.snapshot();
|
|
let annotated = comment(
|
|
document.bundle(),
|
|
"\r\n// Project rationale: preserve me.\r\n",
|
|
);
|
|
assert!(document.apply_bundle(annotated.clone()).unwrap());
|
|
assert_eq!(document.project(), &initial.decoded.project);
|
|
assert_eq!(document.revision(), 1);
|
|
document
|
|
.execute(Command::Rename(
|
|
document.project().id,
|
|
"Rescue workspace".into(),
|
|
))
|
|
.unwrap();
|
|
let renamed = document.snapshot();
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &annotated);
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &initial.bundle);
|
|
assert!(!document.dirty());
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &annotated);
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &renamed.bundle);
|
|
assert!(document.undo());
|
|
let branch = comment(document.bundle(), "// A different source-only decision.\n");
|
|
assert!(document.apply_bundle(branch.clone()).unwrap());
|
|
assert!(!document.can_redo());
|
|
assert_eq!(document.project(), &initial.decoded.project);
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &annotated);
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &branch);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_cross_file_reparent_and_source_edit_share_exact_history() {
|
|
let bundle = SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: [
|
|
(
|
|
"project.arcadia".into(),
|
|
"arcadia 3; project rescue \"Rescue\" { include \"oa/source.arcadia\"; include \"oa/target.arcadia\"; }\n".into(),
|
|
),
|
|
(
|
|
"oa/source.arcadia".into(),
|
|
"// Source module\r\noa_activity respond \"Respond\" {\r\n // Assessment rationale follows this activity.\r\n oa_activity assess \"Assess\" {}\r\n}\r\n".into(),
|
|
),
|
|
(
|
|
"oa/target.arcadia".into(),
|
|
"// Destination module\noa_activity coordinate \"Coordinate\" {}\n".into(),
|
|
),
|
|
].into(),
|
|
};
|
|
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
|
document.mark_saved(document.snapshot());
|
|
let original = document.snapshot();
|
|
let id = |alias: &str| {
|
|
original
|
|
.decoded
|
|
.project
|
|
.elements
|
|
.values()
|
|
.find(|element| element.alias == alias)
|
|
.unwrap()
|
|
.id
|
|
};
|
|
let child = id("assess");
|
|
let destination = id("coordinate");
|
|
document
|
|
.execute(Command::Reparent {
|
|
element: child,
|
|
parent: Some(destination),
|
|
})
|
|
.unwrap();
|
|
let moved = document.snapshot();
|
|
assert_eq!(
|
|
document.project().elements[&child].parent,
|
|
Some(destination)
|
|
);
|
|
assert_eq!(
|
|
document.compiled().index.declarations[&child].path,
|
|
"oa/target.arcadia"
|
|
);
|
|
assert!(
|
|
document.bundle().files["oa/target.arcadia"]
|
|
.contains("// Assessment rationale follows this activity.")
|
|
);
|
|
assert!(
|
|
!document.bundle().files["oa/source.arcadia"]
|
|
.contains("// Assessment rationale follows this activity.")
|
|
);
|
|
let edited = comment(document.bundle(), "// Reviewed containment decision.\n");
|
|
assert!(document.apply_bundle(edited.clone()).unwrap());
|
|
assert_eq!(document.project(), &moved.decoded.project);
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &moved.bundle);
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &original.bundle);
|
|
assert_eq!(document.project(), &original.decoded.project);
|
|
assert!(!document.dirty());
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &moved.bundle);
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &edited);
|
|
let accepted = document.snapshot();
|
|
let invalid = comment(document.bundle(), "invalid_declaration;");
|
|
assert!(document.apply_bundle(invalid).is_err());
|
|
assert!(Arc::ptr_eq(&accepted, &document.snapshot()));
|
|
let reopened =
|
|
Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap();
|
|
assert_eq!(reopened.project(), document.project());
|
|
assert_eq!(reopened.bundle(), document.bundle());
|
|
assert!(!reopened.dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn recreated_equal_record_does_not_inherit_deleted_source_ownership() {
|
|
let bundle = SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: [
|
|
(
|
|
"project.arcadia".into(),
|
|
"arcadia 3; project rescue \"Rescue\" { include \"authored.arcadia\"; }\n"
|
|
.into(),
|
|
),
|
|
(
|
|
"authored.arcadia".into(),
|
|
"// Rationale for the original record.\noa_activity assess \"Assess\" {}\n"
|
|
.into(),
|
|
),
|
|
]
|
|
.into(),
|
|
};
|
|
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
|
document.mark_saved(document.snapshot());
|
|
let before = document.snapshot();
|
|
let element = document.project().elements.values().next().unwrap().clone();
|
|
let id = element.id;
|
|
document
|
|
.execute(Command::Batch(vec![
|
|
Command::Delete(id),
|
|
Command::CreateElement(element),
|
|
]))
|
|
.unwrap();
|
|
assert_eq!(document.project(), &before.decoded.project);
|
|
assert_ne!(
|
|
document.compiled().index.declarations[&id].path,
|
|
"authored.arcadia"
|
|
);
|
|
assert!(
|
|
document.bundle().files["authored.arcadia"]
|
|
.contains("// Rationale for the original record.")
|
|
);
|
|
assert!(!document.bundle().files["authored.arcadia"].contains("oa_activity"));
|
|
assert!(document.dirty());
|
|
assert_eq!(document.revision(), 1);
|
|
let recreated = document.snapshot();
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &before.bundle);
|
|
assert!(!document.dirty());
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &recreated.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn asynchronous_save_marks_its_snapshot_without_cleaning_later_edits() {
|
|
let mut document = document();
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// Saved rationale.\n"))
|
|
.unwrap();
|
|
let saving = document.snapshot();
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// Later unsaved rationale.\n"))
|
|
.unwrap();
|
|
document.mark_saved(saving);
|
|
assert!(document.dirty());
|
|
assert!(document.undo());
|
|
assert!(!document.dirty());
|
|
assert!(document.redo());
|
|
assert!(document.dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_op_command_or_source_apply_preserves_history_revision_and_redo() {
|
|
let mut document = document();
|
|
assert!(!document.undo());
|
|
assert!(!document.redo());
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// Future edit.\n"))
|
|
.unwrap();
|
|
assert!(document.undo());
|
|
let before = document.snapshot();
|
|
let revision = document.revision();
|
|
document
|
|
.execute(Command::Rename(
|
|
document.project().id,
|
|
document.project().name.clone(),
|
|
))
|
|
.unwrap();
|
|
assert!(!document.apply_bundle(document.bundle().clone()).unwrap());
|
|
assert!(
|
|
!document
|
|
.apply_compiled(document.compiled().clone())
|
|
.unwrap()
|
|
);
|
|
assert!(Arc::ptr_eq(&before, &document.snapshot()));
|
|
assert_eq!(document.revision(), revision);
|
|
assert!(document.can_redo());
|
|
assert!(!document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn altered_compiled_model_or_index_is_rejected_without_mutation() {
|
|
let mut document = document();
|
|
let before = document.snapshot();
|
|
let mut forged = document.compiled().clone();
|
|
forged.decoded.project.name = "Not in the source".into();
|
|
assert!(document.apply_compiled(forged).is_err());
|
|
let mut forged = document.compiled().clone();
|
|
forged.index.declarations.clear();
|
|
let error = document.apply_compiled(forged).unwrap_err();
|
|
assert_eq!(error.diagnostics()[0].diagnostic.code, "stale_source_index");
|
|
assert!(Arc::ptr_eq(&before, &document.snapshot()));
|
|
assert_eq!(document.revision(), 0);
|
|
assert!(!document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn omitted_id_stabilization_remains_dirty_until_exact_source_is_saved() {
|
|
let bundle = SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: [(
|
|
"project.arcadia".into(),
|
|
"arcadia 3; project rescue \"Rescue\" {}\n".into(),
|
|
)]
|
|
.into(),
|
|
};
|
|
let mut document = Document::from_bundle(bundle.clone(), ProjectLimits::default()).unwrap();
|
|
assert!(document.dirty());
|
|
assert_ne!(document.bundle(), &bundle);
|
|
let id = document.project().id;
|
|
assert!(!document.apply_bundle(document.bundle().clone()).unwrap());
|
|
assert_eq!(document.project().id, id);
|
|
document.mark_saved(document.snapshot());
|
|
assert!(!document.dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn accepted_history_keeps_the_latest_hundred_complete_snapshots() {
|
|
let mut document = document();
|
|
for change in 0..105 {
|
|
document
|
|
.apply_bundle(comment(
|
|
document.bundle(),
|
|
&format!("// Change {change}.\n"),
|
|
))
|
|
.unwrap();
|
|
}
|
|
assert_eq!(document.past.len(), HISTORY_LIMIT);
|
|
let mut undone = 0;
|
|
while document.undo() {
|
|
undone += 1;
|
|
}
|
|
assert_eq!(undone, HISTORY_LIMIT);
|
|
assert_eq!(
|
|
document.bundle().files[&document.bundle().entry]
|
|
.matches("// Change")
|
|
.count(),
|
|
5
|
|
);
|
|
while document.redo() {}
|
|
assert_eq!(document.past.len(), HISTORY_LIMIT);
|
|
assert_eq!(document.revision(), 305);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_project_is_rejected_before_format_can_normalize_it() {
|
|
let mut project = Project::empty();
|
|
project.schema_version = 17;
|
|
assert!(Document::from_project(project, ProjectLimits::default()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn byte_budget_trims_oldest_history_without_discarding_a_large_current() {
|
|
let mut document = document();
|
|
let rationale = format!("// {}\n", "x".repeat(11 * 1024 * 1024));
|
|
document
|
|
.apply_bundle(comment(document.bundle(), &rationale))
|
|
.unwrap();
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// Second version\n"))
|
|
.unwrap();
|
|
document
|
|
.apply_bundle(comment(document.bundle(), "// Third version\n"))
|
|
.unwrap();
|
|
assert_eq!(document.past.len(), 1);
|
|
let before = document.snapshot();
|
|
assert!(document.undo());
|
|
assert!(!document.can_undo());
|
|
assert!(document.redo());
|
|
assert!(Arc::ptr_eq(&before, &document.snapshot()));
|
|
let large = format!("// {}\n", "y".repeat(22 * 1024 * 1024));
|
|
document
|
|
.apply_bundle(comment(document.bundle(), &large))
|
|
.unwrap();
|
|
assert!(document.current_bytes > HISTORY_BYTES);
|
|
assert!(!document.can_undo());
|
|
assert!(document.bundle().files[&document.bundle().entry].ends_with(&large));
|
|
}
|
|
}
|