language: Memoize value of has_edits_since for a given buffer version (#13656)

Piotr Osiewicz created

As a drive-by of https://github.com/zed-industries/zed/pull/13654, I've
noticed that the editor felt sluggish after I've undone the changes made
by the replacement. It turns out that we are repeatedly checking whether
there are any edits to estabilish dirty/conflict state of a buffer, even
though this operation is pure; this PR stores away the result of a
computation and refers to it before rerunning it.

Release Notes:

- Improve editor's performance with large undo histories

Change summary

crates/language/src/buffer.rs | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -31,6 +31,7 @@ use smallvec::SmallVec;
 use smol::future::yield_now;
 use std::{
     any::Any,
+    cell::Cell,
     cmp::{self, Ordering},
     collections::BTreeMap,
     ffi::OsStr,
@@ -113,6 +114,9 @@ pub struct Buffer {
     capability: Capability,
     has_conflict: bool,
     diff_base_version: usize,
+    /// Memoize calls to has_changes_since(saved_version).
+    /// The contents of a cell are (self.version, has_changes) at the time of a last call.
+    has_unsaved_edits: Cell<(clock::Global, bool)>,
 }
 
 /// An immutable, cheaply cloneable representation of a fixed
@@ -690,6 +694,7 @@ impl Buffer {
             reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
+            has_unsaved_edits: Cell::new((buffer.version(), false)),
             text: buffer,
             diff_base: diff_base
                 .map(|mut raw_diff_base| {
@@ -799,6 +804,8 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
+        self.has_unsaved_edits
+            .set((self.saved_version().clone(), false));
         self.has_conflict = false;
         self.saved_mtime = mtime;
         cx.emit(Event::Saved);
@@ -860,6 +867,8 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
+        self.has_unsaved_edits
+            .set((self.saved_version.clone(), false));
         self.text.set_line_ending(line_ending);
         self.saved_mtime = mtime;
         cx.emit(Event::Reloaded);
@@ -1516,10 +1525,25 @@ impl Buffer {
         self.end_transaction(cx)
     }
 
+    fn has_unsaved_edits(&self) -> bool {
+        let (last_version, has_unsaved_edits) = self.has_unsaved_edits.take();
+
+        if last_version == self.version {
+            self.has_unsaved_edits
+                .set((last_version, has_unsaved_edits));
+            return has_unsaved_edits;
+        }
+
+        let has_edits = self.has_edits_since(&self.saved_version);
+        self.has_unsaved_edits
+            .set((self.version.clone(), has_edits));
+        has_edits
+    }
+
     /// Checks if the buffer has unsaved changes.
     pub fn is_dirty(&self) -> bool {
         self.has_conflict
-            || self.has_edits_since(&self.saved_version)
+            || self.has_unsaved_edits()
             || self
                 .file
                 .as_ref()
@@ -1531,7 +1555,7 @@ impl Buffer {
     pub fn has_conflict(&self) -> bool {
         self.has_conflict
             || self.file.as_ref().map_or(false, |file| {
-                file.mtime() > self.saved_mtime && self.has_edits_since(&self.saved_version)
+                file.mtime() > self.saved_mtime && self.has_unsaved_edits()
             })
     }