Răsfoiți Sursa

enhance: undo redo use patches instead of full text

Tienson Qin 4 ani în urmă
părinte
comite
249bf680c1

+ 1 - 0
externs.js

@@ -50,3 +50,4 @@ dummy.values = function() {};
 // Do we really need those?
 dummy.filter = function() {};
 dummy.concat = function() {};
+dummy.diff_main = function() {};

+ 2 - 1
package.json

@@ -49,7 +49,8 @@
     },
     "dependencies": {
         "codemirror": "^5.58.1",
-        "diff": "^4.0.2",
+        "diff": "5.0.0",
+        "diff-match-patch": "^1.0.5",
         "fuzzysort": "^1.1.4",
         "gulp-cached": "^1.1.1",
         "ignore": "^5.1.8",

+ 1 - 10
src/main/frontend/db/model.cljs

@@ -266,16 +266,7 @@
   ([repo path]
    (when (and repo path)
      (when-let [conn (conn/get-files-conn repo)]
-       (->
-        (d/q
-         '[:find ?content
-           :in $ ?path
-           :where
-           [?file :file/path ?path]
-           [?file :file/content ?content]]
-         @conn
-         path)
-        ffirst)))))
+       (:file/content (d/entity (d/db conn) [:file/path path]))))))
 
 (defn get-block-by-uuid
   [uuid]

+ 22 - 3
src/main/frontend/diff.cljs

@@ -1,15 +1,34 @@
 (ns frontend.diff
   (:require [clojure.string :as string]
             ["diff" :as jsdiff]
+            ["diff-match-patch" :as diff-match-patch]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [cljs-bean.core :as bean]))
 
+;; TODO: replace with diff-match-patch
 (defn diff
   [s1 s2]
   (-> ((gobj/get jsdiff "diffLines") s1 s2)
       bean/->clj))
 
+(defonce dmp (diff-match-patch.))
+
+(defn diffs
+  [s1 s2]
+  (.diff_main dmp s1 s2 true))
+
+(defn get-patches
+  [s1 s2 diffs]
+  (.patch_make dmp s1 s2 diffs))
+
+(defn apply-patches!
+  [text patches]
+  (if (seq patches)
+    (let [result (.patch_apply dmp patches text)]
+      (nth result 0))
+    text))
+
 ;; (find-position "** hello _w_" "hello w")
 (defn find-position
   [markup text]
@@ -32,6 +51,6 @@
 
           :else
           (recur r1 t2 (inc i1) i2))))
-      (catch js/Error e
-        (log/error :diff/find-position {:error e})
-        (count markup))))
+    (catch js/Error e
+      (log/error :diff/find-position {:error e})
+      (count markup))))

+ 1 - 3
src/main/frontend/handler/editor.cljs

@@ -436,11 +436,9 @@
           remove-properties nil
           auto-save? false}
      :as opts}]
-   (let [value value
-         repo (or repo (state/get-current-repo))
+   (let [repo (or repo (state/get-current-repo))
          e (db/entity repo [:block/uuid uuid])
          block (assoc (with-block-meta repo block)
-                      ;; (into {} ...) to fix the old data
                       :block/properties (into {} (:block/properties e)))
          format (or format (state/get-preferred-format))
          page (db/entity repo (:db/id page))

+ 51 - 50
src/main/frontend/handler/file.cljs

@@ -194,62 +194,63 @@
                     reset? false
                     update-db? true}
                :as opts}]
-  ;; update db
-  (when update-db?
-    (doseq [[path content] files]
-      (if reset?
-        (reset-file! repo path content)
-        (db/set-file-content! repo path content))))
+  ;; old file content
+  (let [file->content (let [paths (map first files)]
+                        (zipmap paths
+                                (map (fn [path] (db/get-file-no-sub repo path)) paths)))]
+    ;; update db
+    (when update-db?
+      (doseq [[path content] files]
+        (if reset?
+          (reset-file! repo path content)
+          (db/set-file-content! repo path content))))
 
-  (when-let [chan (state/get-file-write-chan)]
-    (async/put! chan [repo files opts])))
+    (when-let [chan (state/get-file-write-chan)]
+      (async/put! chan [repo files opts file->content]))))
 
 (defn alter-files-handler!
   [repo files {:keys [add-history? update-status? git-add-cb reset?]
                :or {add-history? true
                     update-status? true
-                    reset? false}}]
-  (p/let [file->content (let [paths (map first files)]
-                          (zipmap paths
-                                  (map (fn [path] (db/get-file-no-sub repo path)) paths)))]
-    (let [write-file-f (fn [[path content]]
-                         (let [original-content (get file->content path)]
-                           (-> (p/let [_ (fs/check-directory-permission! repo)]
-                                 (fs/write-file repo (util/get-repo-dir repo) path content
-                                                {:old-content original-content
-                                                 :last-modified-at (db/get-file-last-modified-at repo path)}))
-                               (p/catch (fn [error]
-                                          (log/error :write-file/failed {:path path
-                                                                         :content content
-                                                                         :error error}))))))
-          git-add-f (fn []
-                      (let [add-helper
-                            (fn []
-                              (map
-                               (fn [[path content]]
-                                 (git-handler/git-add repo path update-status?))
-                               files))]
-                        (-> (p/all (add-helper))
-                            (p/then (fn [_]
-                                      (when git-add-cb
-                                        (git-add-cb))))
-                            (p/catch (fn [error]
-                                       (println "Git add failed:")
-                                       (js/console.error error)))))
-                      (ui-handler/re-render-file!)
-                      (when add-history?
-                        (let [files-tx (mapv (fn [[path content]]
-                                               (let [original-content (get file->content path)]
-                                                 [path original-content content])) files)]
-                          (history/add-history! repo files-tx))))]
-      (-> (p/all (map write-file-f files))
-          (p/then (fn []
-                    (git-add-f)
-                    ;; TODO: save logseq/metadata
-))
-          (p/catch (fn [error]
-                     (println "Alter files failed:")
-                     (js/console.error error)))))))
+                    reset? false}} file->content]
+  (let [write-file-f (fn [[path content]]
+                       (let [original-content (get file->content path)]
+                         (prn {:original-content original-content})
+                         (-> (p/let [_ (fs/check-directory-permission! repo)]
+                               (fs/write-file repo (util/get-repo-dir repo) path content
+                                              {:old-content original-content
+                                               :last-modified-at (db/get-file-last-modified-at repo path)}))
+                             (p/catch (fn [error]
+                                        (log/error :write-file/failed {:path path
+                                                                       :content content
+                                                                       :error error}))))))
+        git-add-f (fn []
+                    (let [add-helper
+                          (fn []
+                            (map
+                             (fn [[path content]]
+                               (git-handler/git-add repo path update-status?))
+                             files))]
+                      (-> (p/all (add-helper))
+                          (p/then (fn [_]
+                                    (when git-add-cb
+                                      (git-add-cb))))
+                          (p/catch (fn [error]
+                                     (println "Git add failed:")
+                                     (js/console.error error)))))
+                    (ui-handler/re-render-file!)
+                    (when add-history?
+                      (let [files-tx (mapv (fn [[path content]]
+                                             (let [original-content (get file->content path)]
+                                               (prn "content changed? " (not= original-content content))
+                                               [path original-content content])) files)]
+                        (history/add-history! repo files-tx))))]
+    (-> (p/all (map write-file-f files))
+        (p/then (fn []
+                  (git-add-f)))
+        (p/catch (fn [error]
+                   (println "Alter files failed:")
+                   (js/console.error error))))))
 
 (defn remove-file!
   [repo file]

+ 32 - 32
src/main/frontend/handler/repo.cljs

@@ -109,38 +109,38 @@
   ([repo-url content]
    (spec/validate :repos/url repo-url)
    (when (state/enable-journals? repo-url)
-       (let [repo-dir (util/get-repo-dir repo-url)
-          format (state/get-preferred-format repo-url)
-          title (date/today)
-          file-name (date/journal-title->default title)
-          default-content (util/default-content-with-title format title false)
-          template (state/get-journal-template)
-          template (if (and template
-                            (not (string/blank? template)))
-                     template)
-          content (cond
-                    content
-                    content
-
-                    template
-                    (str default-content template)
-
-                    :else
-                    (util/default-content-with-title format title true))
-          path (str config/default-journals-directory "/" file-name "."
-                    (config/get-file-extension format))
-          file-path (str "/" path)
-          page-exists? (db/entity repo-url [:page/name (string/lower-case title)])
-          empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
-      (when (or empty-blocks?
-                (not page-exists?))
-        (p/let [_ (fs/check-directory-permission! repo-url)
-                _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
-                file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
-          (when-not file-exists?
-            (file-handler/reset-file! repo-url path content)
-            (ui-handler/re-render-root!)
-            (git-handler/git-add repo-url path))))))))
+     (let [repo-dir (util/get-repo-dir repo-url)
+           format (state/get-preferred-format repo-url)
+           title (date/today)
+           file-name (date/journal-title->default title)
+           default-content (util/default-content-with-title format title false)
+           template (state/get-journal-template)
+           template (if (and template
+                             (not (string/blank? template)))
+                      template)
+           content (cond
+                     content
+                     content
+
+                     template
+                     (str default-content template)
+
+                     :else
+                     (util/default-content-with-title format title true))
+           path (str config/default-journals-directory "/" file-name "."
+                     (config/get-file-extension format))
+           file-path (str "/" path)
+           page-exists? (db/entity repo-url [:page/name (string/lower-case title)])
+           empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
+       (when (or empty-blocks?
+                 (not page-exists?))
+         (p/let [_ (fs/check-directory-permission! repo-url)
+                 _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
+                 file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
+           (when-not file-exists?
+             (file-handler/reset-file! repo-url path content)
+             (ui-handler/re-render-root!)
+             (git-handler/git-add repo-url path))))))))
 
 (defn create-today-journal!
   []

+ 37 - 21
src/main/frontend/history.cljs

@@ -1,6 +1,10 @@
-(ns frontend.history)
+(ns frontend.history
+  (:require [frontend.diff :as diff]
+            [frontend.db :as db]
+            ["/frontend/utils" :as utils]))
 
 ;; Undo && Redo that works with files
+
 ;; TODO:
 ;; 1. undo-tree
 ;; 2. db-only version, store transactions instead of file patches
@@ -10,23 +14,30 @@
 ;; repo -> idx
 (defonce history-idx (atom {}))
 
-(defonce history-limit 100)
+(defonce history-limit 500)
 
 ;; TODO: replace with patches to reduce memory usage
 ;; tx [[file1-path original new] [file2-path original new]]
 (defn add-history!
   [repo tx]
-  (let [length (count (get @history repo))
-        idx (get @history-idx repo 0)]
-    (when (and (>= length history-limit)
-               (>= idx history-limit))
-      (swap! history assoc repo (vec (drop (/ history-limit 2) (get @history repo))))
-      (swap! history-idx assoc repo (dec (/ history-limit 2))))
-    (let [idx (get @history-idx repo 0)
-          idx' (inc idx)
-          txs (vec (take idx' (get @history repo)))]
-      (swap! history assoc repo (conj txs tx))
-      (swap! history-idx assoc repo idx'))))
+  (let [tx (->> tx
+                (remove (fn [[_ old new]] (= old new)))
+                (map (fn [[file old new]]
+                       (let [diffs (diff/diffs new old)
+                             patches (diff/get-patches new old diffs)]
+                         [file patches]))))]
+    (when (seq tx)
+      (let [length (count (get @history repo))
+            idx (get @history-idx repo 0)]
+        (when (and (>= length history-limit)
+                   (>= idx history-limit))
+          (swap! history assoc repo (vec (drop (/ history-limit 2) (get @history repo))))
+          (swap! history-idx assoc repo (dec (/ history-limit 2))))
+        (let [idx (get @history-idx repo 0)
+              idx' (inc idx)
+              txs (vec (take idx' (get @history repo)))]
+          (swap! history assoc repo (conj txs tx))
+          (swap! history-idx assoc repo idx'))))))
 
 (defn undo!
   [repo alter-file]
@@ -34,10 +45,12 @@
     (when (> idx 0)
       (let [idx' (dec idx)
             tx (get-in @history [repo idx'])]
-        (doseq [[path original-content _content] tx]
-          (alter-file repo path original-content
-                      {:add-history? false
-                       :re-render-root? true}))
+        (doseq [[path patches] tx]
+          (let [current-content (db/get-file-no-sub path)
+                original-content (diff/apply-patches! current-content patches)]
+            (alter-file repo path original-content
+                        {:add-history? false
+                         :re-render-root? true})))
         (swap! history-idx assoc repo idx')))))
 
 (defn redo!
@@ -46,8 +59,11 @@
         txs (get @history repo)]
     (when (> (count txs) idx)
       (let [tx (get-in @history [repo idx])]
-        (doseq [[path _original-content content] tx]
-          (alter-file repo path content
-                      {:add-history? false
-                       :re-render-root? true}))
+        (doseq [[path patches] tx]
+          (let [current-content (db/get-file-no-sub path)
+                reversed-patches (utils/reversePatch patches)
+                content (diff/apply-patches! current-content reversed-patches)]
+            (alter-file repo path content
+                        {:add-history? false
+                         :re-render-root? true})))
         (swap! history-idx assoc repo (inc idx))))))

+ 14 - 0
src/main/frontend/utils.js

@@ -173,3 +173,17 @@ export const triggerInputChange = (node, value = '', name = 'change') => {
     node.dispatchEvent(event)
   }
 }
+
+// Copied from https://github.com/google/diff-match-patch/issues/29#issuecomment-647627182
+export const reversePatch = patch => {
+  return patch.map(patchObj => ({
+    diffs: patchObj.diffs.map(([ op, val ]) => [
+      op * -1, // The money maker
+      val
+    ]),
+    start1: patchObj.start2,
+    start2: patchObj.start1,
+    length1: patchObj.length2,
+    length2: patchObj.length1
+  }));
+};

+ 9 - 4
yarn.lock

@@ -1644,10 +1644,15 @@ didyoumean@^1.2.1:
   resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff"
   integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=
 
-diff@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
-  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+diff-match-patch@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
+  integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
+
[email protected]:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
 
 diffie-hellman@^5.0.0:
   version "5.0.3"