Browse Source

Merge pull request #1072 from logseq/enhance/undo-redo

Enhance/undo redo
Tienson Qin 4 years ago
parent
commit
013b4fbd8f

+ 3 - 0
externs.js

@@ -50,6 +50,9 @@ dummy.values = function() {};
 // Do we really need those?
 dummy.filter = function() {};
 dummy.concat = function() {};
+dummy.diff_main = function() {};
+dummy.patch_make = function() {};
+dummy.patch_apply = function() {};
 
 /**
  * @typedef {{

+ 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",

+ 4 - 6
src/main/frontend/components/block.cljs

@@ -1147,7 +1147,7 @@
           (datetime-comp/date-picker nil nil ts)]))]))
 
 (rum/defc block-content < rum/reactive
-  [config {:block/keys [uuid title level body meta content marker dummy? page format repo children pre-block? properties collapsed? idx block-refs-count scheduled scheduled-ast deadline deadline-ast repeated?] :as block} edit-input-id block-id slide?]
+  [config {:block/keys [uuid title level body meta content marker dummy? page format repo children pre-block? properties collapsed? idx container block-refs-count scheduled scheduled-ast deadline deadline-ast repeated?] :as block} edit-input-id block-id slide?]
   (let [dragging? (rum/react *dragging?)
         attrs {:blockid       (str uuid)
                ;; FIXME: Click to copy a selection instead of click first and then copy
@@ -1165,7 +1165,8 @@
                                       (let [cursor-range (util/caret-range (gdom/getElement block-id))
                                             properties-hidden? (text/properties-hidden? properties)
                                             content (text/remove-level-spaces content format)
-                                            content (if properties-hidden? (text/remove-properties! content) content)]
+                                            content (if properties-hidden? (text/remove-properties! content) content)
+                                            block (db/pull [:block/uuid (:block/uuid block)])]
                                         (state/set-editing!
                                          edit-input-id
                                          content
@@ -1936,13 +1937,10 @@
                       (rest blocks)
                       blocks)
              first-id (:block/uuid (first blocks))]
-         (for [item blocks]
+         (for [[idx item] (medley/indexed blocks)]
            (let [item (-> (if (:block/dummy? item)
                             item
                             (dissoc item :block/meta)))
-                 item (if (= first-id (:block/uuid item))
-                        (assoc item :block/idx 0)
-                        item)
                  config (assoc config :block/uuid (:block/uuid item))]
              (rum/with-key
                (block-container config item)

+ 2 - 2
src/main/frontend/components/repo.cljs

@@ -35,8 +35,8 @@
         repos (util/distinct-by :url repos)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (if (seq repos)
-        [:div#repos
-         [:h1.title "All Repos"]
+        [:div#graphs
+         [:h1.title "All Graphs"]
 
          [:div.pl-1.content
           [:div.flex.flex-row.my-4

+ 16 - 6
src/main/frontend/components/settings.cljs

@@ -81,6 +81,7 @@
         enable-timetracking? (state/enable-timetracking?)
         current-repo (state/get-current-repo)
         enable-journals? (state/enable-journals? current-repo)
+        enable-git-auto-push? (state/enable-git-auto-push? current-repo)
         enable-block-time? (state/enable-block-time?)
         show-brackets? (state/show-brackets?)
         github-token (state/sub [:me :access-token])
@@ -193,12 +194,12 @@
                    (let [value (not enable-timetracking?)]
                      (config-handler/set-config! :feature/enable-timetracking? value))))
 
-         (toggle "enable_block_time"
-                 (t :settings-page/enable-block-time)
-                 enable-block-time?
-                 (fn []
-                   (let [value (not enable-block-time?)]
-                     (config-handler/set-config! :feature/enable-block-time? value))))
+         ;; (toggle "enable_block_time"
+         ;;         (t :settings-page/enable-block-time)
+         ;;         enable-block-time?
+         ;;         (fn []
+         ;;           (let [value (not enable-block-time?)]
+         ;;             (config-handler/set-config! :feature/enable-block-time? value))))
 
          (toggle "enable_journals"
                  (t :settings-page/enable-journals)
@@ -234,6 +235,15 @@
                                 :else
                                 (notification/show! "Please make sure the page exists!" :warning))))}]]]])
 
+         (when (string/starts-with? current-repo "https://")
+           (toggle "enable_git_auto_push"
+                  "Enable Git auto push"
+                  enable-git-auto-push?
+                  (fn []
+                    (let [value (not enable-git-auto-push?)]
+                      (config-handler/set-config! :git-auto-push value)))))
+
+
          [:hr]
 
          (when logged?

+ 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]

+ 1 - 1
src/main/frontend/db/react.cljs

@@ -291,7 +291,7 @@
                               (conn/get-files-conn repo-url)
                               (conn/get-conn repo-url false)))]
         (when (and (seq tx-data) (get-conn))
-          (let [tx-result (profile "Transact!" (d/transact! (get-conn) (vec tx-data)))
+          (let [tx-result (d/transact! (get-conn) (vec tx-data))
                 db (:db-after tx-result)
                 handler-keys (get-handler-keys handler-opts)]
             (doseq [handler-key handler-keys]

+ 0 - 5
src/main/frontend/dicts.cljs

@@ -307,7 +307,6 @@ title: How to take dummy notes?
         :graph "Graph"
         :publishing "Publishing"
         :export "Export public pages"
-        :all-repos "All repos"
         :all-graphs "All graphs"
         :all-pages "All pages"
         :all-files "All files"
@@ -524,7 +523,6 @@ title: How to take dummy notes?
         :new-file "Nouveau fichier"
         :graph "Graphe"
         :publishing "Publication"
-        :all-repos "Tous les répertoires"
         :all-pages "Toutes les pages"
         :all-files "Tous les fichiers"
         :all-journals "Tous les journaux"
@@ -789,7 +787,6 @@ title: How to take dummy notes?
            :graph "图谱"
            :publishing "发布"
            :export "导出公开页面"
-           :all-repos "所有库"
            :all-graphs "所有库"
            :all-pages "所有页面"
            :all-files "所有文件"
@@ -1048,7 +1045,6 @@ title: How to take dummy notes?
              :new-page "新頁面"
              :graph "圖譜"
              :publishing "發布/下載 HTML 文件"
-             :all-repos "所有庫"
              :all-pages "所有頁面"
              :all-files "所有文件"
              :my-publishing "My publishing"
@@ -1296,7 +1292,6 @@ title: How to take dummy notes?
         :search "Soek"
         :new-page "Nuwe bladsy"
         :graph "Grafiek"
-        :all-repos "Alle stoorplekke"
         :all-pages "Alle blaaie"
         :all-files "Alle lêers"
         :settings "Verstellings"

+ 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))))

+ 46 - 36
src/main/frontend/handler/editor.cljs

@@ -367,8 +367,8 @@
                              (get-edit-input-id-with-block-id id)
                              (str (subs id 0 (- (count id) 36)) block-id))
              block (or
-                    block
                     (db/pull [:block/uuid block-id])
+                    block
                     ;; dummy?
                     {:block/uuid block-id
                      :block/content ""})
@@ -431,17 +431,14 @@
    (save-block-if-changed! block value nil))
   ([{:block/keys [uuid content meta file page dummy? format repo pre-block? content ref-pages ref-blocks] :as block}
     value
-    {:keys [indent-left? custom-properties remove-properties rebuild-content? auto-save?]
+    {:keys [indent-left? custom-properties remove-properties rebuild-content? chan chan-callback]
      :or {rebuild-content? true
           custom-properties nil
-          remove-properties nil
-          auto-save? false}
+          remove-properties nil}
      :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))
@@ -458,6 +455,7 @@
                          {:page/original-name alias
                           :page/name (string/lower-case alias)})
                        (remove #{(:page/name page)} alias)))
+
          permalink-changed? (when (and pre-block? (:permalink old-properties))
                               (not= (:permalink old-properties)
                                     (:permalink new-properties)))
@@ -602,7 +600,9 @@
                {:key :block/change
                 :data (map (fn [block] (assoc block :block/page page)) blocks)}
                (let [new-content (new-file-content-indent-outdent block file-content value block-children-content new-end-pos indent-left?)]
-                 [[file-path new-content]])))
+                 [[file-path new-content]])
+               (when chan {:chan chan
+                           :chan-callback chan-callback})))
 
              (when (or (seq retract-refs) pre-block?)
                (ui-handler/re-render-root!))
@@ -1425,33 +1425,36 @@
     (save-block-aux! block value format {})))
 
 (defn save-current-block-when-idle!
-  []
-  (when-let [repo (state/get-current-repo)]
-    (when (and (state/input-idle? repo)
-               (not (state/get-editor-show-page-search?))
-               (not (state/get-editor-show-page-search-hashtag?))
-               (not (state/get-editor-show-block-search?)))
-      (state/set-editor-op! :auto-save)
-      (try
-        (let [input-id (state/get-edit-input-id)
-              block (state/get-edit-block)
-              db-block (when-let [block-id (:block/uuid block)]
-                         (db/pull [:block/uuid block-id]))
-              elem (and input-id (gdom/getElement input-id))
-              db-content (:block/content db-block)
-              db-content-without-heading (and db-content
-                                              (util/safe-subs db-content (:block/level db-block)))
-              value (and elem (gobj/get elem "value"))]
-          (when (and block value db-content-without-heading
-                     (or
-                      (not= (string/trim db-content-without-heading)
-                            (string/trim value))))
-            (let [cur-pos (util/get-input-pos elem)]
-              (save-block-aux! db-block value (:block/format db-block)
-                               {:auto-save? true}))))
-        (catch js/Error error
-          (log/error :save-block-failed error)))
-      (state/set-editor-op! nil))))
+  ([]
+   (save-current-block-when-idle! {}))
+  ([{:keys [check-idle? chan chan-callback]
+     :or {check-idle? true}}]
+   (when (nil? (state/get-editor-op))
+     (when-let [repo (state/get-current-repo)]
+       (when (and (if check-idle? (state/input-idle? repo) true)
+                  (not (state/get-editor-show-page-search?))
+                  (not (state/get-editor-show-page-search-hashtag?))
+                  (not (state/get-editor-show-block-search?)))
+         (state/set-editor-op! :auto-save)
+         (try
+           (let [input-id (state/get-edit-input-id)
+                 block (state/get-edit-block)
+                 db-block (when-let [block-id (:block/uuid block)]
+                            (db/pull [:block/uuid block-id]))
+                 elem (and input-id (gdom/getElement input-id))
+                 db-content (:block/content db-block)
+                 db-content-without-heading (and db-content
+                                                 (util/safe-subs db-content (:block/level db-block)))
+                 value (and elem (gobj/get elem "value"))]
+             (when (and block value db-content-without-heading
+                        (or
+                         (not= (string/trim db-content-without-heading)
+                               (string/trim value))))
+               (save-block-aux! db-block value (:block/format db-block) {:chan chan
+                                                                         :chan-callback chan-callback})))
+           (catch js/Error error
+             (log/error :save-block-failed error)))
+         (state/set-editor-op! nil))))))
 
 (defn on-up-down
   [state e up?]
@@ -2167,4 +2170,11 @@
 
 (defn periodically-save!
   []
-  (js/setInterval save-current-block-when-idle! 3000))
+  (js/setInterval save-current-block-when-idle! 500))
+
+(defn save!
+  []
+  (when-let [repo (state/get-current-repo)]
+    (save-current-block-when-idle! {:check-idle? false})
+    (when (string/starts-with? repo "https://") ; git repo
+      (repo-handler/auto-push!))))

+ 6 - 5
src/main/frontend/handler/extract.cljs

@@ -22,6 +22,7 @@
          (map string/lower-case)
          (distinct))))
 
+;; TODO: performance improvement
 (defn- extract-pages-and-blocks
   [repo-url format ast properties file content utf8-content journal? pages-fn]
   (try
@@ -55,8 +56,8 @@
                   (fn [page]
                     (let [page-file? (= page (string/lower-case file))
                           aliases (and (:alias properties)
-                                           (seq (remove #(= page %)
-                                                        (:alias properties))))
+                                       (seq (remove #(= page %)
+                                                    (:alias properties))))
                           journal-date-long (if journal?
                                               (date/journal-title->long (string/capitalize page)))
                           page-list (when-let [list-content (:list properties)]
@@ -93,14 +94,14 @@
                                       {:page/name page-name
                                        :page/alias aliases}
                                       {:page/name page-name})))
-                                 aliases))
+                                aliases))
 
                         (:tags properties)
                         (assoc :page/tags (let [tags (:tags properties)]
                                             (swap! ref-tags set/union (set tags))
                                             (map (fn [tag] {:page/name (string/lower-case tag)
-                                                           :page/original-name tag})
-                                              tags))))))
+                                                            :page/original-name tag})
+                                                 tags))))))
                   (->> (map first pages)
                        (remove nil?))))
           pages (concat

+ 61 - 54
src/main/frontend/handler/file.cljs

@@ -143,14 +143,15 @@
                              (assoc :file/created-at t)))])]
       (db/transact! repo-url tx))))
 
-;; TODO: better name to separate from reset-file!
+;; TODO: Remove this function in favor of `alter-files`
 (defn alter-file
   [repo path content {:keys [reset? re-render-root? add-history? update-status?]
                       :or {reset? true
                            re-render-root? false
                            add-history? true
                            update-status? false}}]
-  (let [original-content (db/get-file-no-sub repo path)]
+  (let [edit-block (state/get-edit-block)
+        original-content (db/get-file-no-sub repo path)]
     (if reset?
       (do
         (when-let [page-id (db/get-file-page-id path)]
@@ -188,68 +189,74 @@
                                    :path-params {:path path}}))))))
 
 (defn alter-files
-  [repo files {:keys [add-history? update-status? git-add-cb reset? update-db?]
+  [repo files {:keys [add-history? update-status? git-add-cb reset? update-db? chan chan-callback resolved-handler]
                :or {add-history? true
                     update-status? true
                     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)]
+      (let [chan-callback
+            (:chan-callback opts)]
+        (async/put! chan [repo files opts file->content])
+        (when chan-callback
+          (chan-callback))))))
 
 (defn alter-files-handler!
-  [repo files {:keys [add-history? update-status? git-add-cb reset?]
+  [repo files {:keys [add-history? update-status? git-add-cb reset? chan]
                :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)]
+                         (-> (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)
+                  (when chan
+                    (async/put! chan true))))
+        (p/catch (fn [error]
+                   (println "Alter files failed:")
+                   (js/console.error error)
+                   (async/put! chan false))))))
 
 (defn remove-file!
   [repo file]

+ 57 - 15
src/main/frontend/handler/history.cljs

@@ -1,7 +1,17 @@
 (ns frontend.handler.history
   (:require [frontend.state :as state]
+            [frontend.db :as db]
             [frontend.history :as history]
-            [frontend.handler.file :as file]))
+            [frontend.handler.file :as file]
+            [frontend.handler.editor :as editor]
+            [frontend.handler.ui :as ui-handler]
+            [promesa.core :as p]
+            [clojure.core.async :as async]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]
+            [dommy.core :as d]
+            [frontend.util :as util]
+            [medley.core :as medley]))
 
 (defn- default-undo
   []
@@ -11,22 +21,54 @@
   []
   (js/document.execCommand "redo" false nil))
 
+(defn restore-cursor!
+  [{:keys [block-container block-idx pos] :as state}]
+  (ui-handler/re-render-root!)
+  ;; get the element
+  (when (and block-container block-idx pos)
+    (when-let [container (gdom/getElement block-container)]
+      (let [blocks (d/by-class container "ls-block")
+            block-node (util/nth-safe (seq blocks) block-idx)
+            id (and block-node (gobj/get block-node "id"))]
+        (when id
+          (let [block-id (->> (take-last 36 id)
+                              (apply str))
+                block-uuid (when (util/uuid-string? block-id)
+                             (uuid block-id))]
+            (when block-uuid
+              (when-let [block (db/pull [:block/uuid block-uuid])]
+                (editor/edit-block! block pos
+                                    (:block/format block)
+                                    (:block/uuid block))))))))))
+
 (defn undo!
   []
-  (let [route (get-in (:route-match @state/state) [:data :name])]
-    (if (and (contains? #{:home :page :file} route)
-             (not (state/get-edit-input-id))
-             (state/get-current-repo))
-      (let [repo (state/get-current-repo)]
-        (history/undo! repo file/alter-file))
-      (default-undo))))
+  (when-not (state/get-editor-op)
+    (let [route (get-in (:route-match @state/state) [:data :name])]
+      (if (and (contains? #{:home :page :file} route)
+               (state/get-current-repo))
+        (let [repo (state/get-current-repo)
+              chan (async/promise-chan)
+              save-commited? (atom nil)
+              undo-fn (fn []
+                        (history/undo! repo file/alter-file restore-cursor!))]
+          (editor/save-current-block-when-idle! {:check-idle? false
+                                                 :chan chan
+                                                 :chan-callback (fn []
+                                                                  (reset! save-commited? true))})
+          (if @save-commited?
+            (async/go
+              (let [_ (async/<! chan)]
+                (undo-fn)))
+            (undo-fn)))
+        (default-undo)))))
 
 (defn redo!
   []
-  (let [route (get-in (:route-match @state/state) [:data :name])]
-    (if (and (contains? #{:home :page :file} route)
-             (not (state/get-edit-input-id))
-             (state/get-current-repo))
-      (let [repo (state/get-current-repo)]
-        (history/redo! repo file/alter-file))
-      (default-redo))))
+  (when-not (state/get-editor-op)
+    (let [route (get-in (:route-match @state/state) [:data :name])]
+     (if (and (contains? #{:home :page :file} route)
+              (state/get-current-repo))
+       (let [repo (state/get-current-repo)]
+         (history/redo! repo file/alter-file restore-cursor!))
+       (default-redo)))))

+ 2 - 1
src/main/frontend/handler/page.cljs

@@ -43,7 +43,8 @@
    (create! title {}))
   ([title {:keys [redirect?]
            :or {redirect? true}}]
-   (let [repo (state/get-current-repo)
+   (let [title (and title (string/trim title))
+         repo (state/get-current-repo)
          dir (util/get-repo-dir repo)
          journal-page? (date/valid-journal-title? title)
          directory (get-directory journal-page?)]

+ 67 - 48
src/main/frontend/handler/repo.cljs

@@ -23,7 +23,9 @@
             [frontend.ui :as ui]
             [clojure.string :as string]
             [frontend.dicts :as dicts]
-            [frontend.spec :as spec]))
+            [frontend.spec :as spec]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
@@ -109,38 +111,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!
   []
@@ -303,21 +305,34 @@
                  (remove nil?))))))))
 
 (defn transact-react-and-alter-file!
-  [repo tx transact-option files]
-  (spec/validate :repos/url repo)
-  (let [files (remove nil? files)
-        pages (->> (map db/get-file-page (map first files))
-                   (remove nil?))]
-    (db/transact-react!
-     repo
-     tx
-     transact-option)
-    (when (seq pages)
-      (let [children-tx (mapcat #(rebuild-page-blocks-children repo %) pages)]
-        (when (seq children-tx)
-          (db/transact! repo children-tx))))
-    (when (seq files)
-      (file-handler/alter-files repo files {}))))
+  ([repo tx transact-option files]
+   (transact-react-and-alter-file! repo tx transact-option files {}))
+  ([repo tx transact-option files opts]
+   (spec/validate :repos/url repo)
+   (let [files (remove nil? files)
+         pages (->> (map db/get-file-page (map first files))
+                    (remove nil?))]
+     (let [edit-block (state/get-edit-block)
+           edit-input-id (state/get-edit-input-id)
+           block-element (when edit-input-id (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block")))
+           {:keys [idx container]} (when block-element
+                                     (util/get-block-idx-inside-container block-element))]
+       (when (and idx container)
+         (state/set-state! :editor/last-edit-block {:block edit-block
+                                               :idx idx
+                                                    :container (gobj/get container "id")})))
+
+     (db/transact-react!
+      repo
+      tx
+      transact-option)
+     (when (seq pages)
+       (let [children-tx (mapcat #(rebuild-page-blocks-children repo %) pages)]
+         (when (seq children-tx)
+           (db/transact! repo children-tx))))
+     (when (seq files)
+       (file-handler/alter-files repo files opts))
+     )))
 
 (declare push)
 
@@ -644,3 +659,7 @@
 (defn get-repo-name
   [url]
   (last (string/split url #"/")))
+
+(defn auto-push!
+  []
+  (git-commit-and-push! "Logseq auto save"))

+ 94 - 33
src/main/frontend/history.cljs

@@ -1,53 +1,114 @@
-(ns frontend.history)
+(ns frontend.history
+  (:require [frontend.diff :as diff]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [promesa.core :as p]
+            ["/frontend/utils" :as utils]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]
+            [clojure.string :as string]
+            [frontend.util :as util]))
 
 ;; Undo && Redo that works with files
+
 ;; TODO:
-;; 1. undo-tree
-;; 2. db-only version, store transactions instead of file patches
+;; 1. preserve cursor positions when undo/redo
+;; 2. undo-tree
+;; 3. db-only version, store transactions instead of file patches
 
 ;; repo file -> contents transactions sequence
 (defonce history (atom {}))
 ;; 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]]
+;; tx [[file1-path patches] [file2-path patches]]
 (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]]
+                       (when (and old new)
+                         (let [diffs (diff/diffs new old)
+                              patches (diff/get-patches new old diffs)]
+                           [file patches {:old old
+                                          :new new}]))))
+                (remove nil?))]
+    (when (seq tx)
+      (let [last-edit-block (get @state/state :editor/last-edit-block)
+            tx (if last-edit-block
+                 {:data tx
+                  ;; other state
+                  :pos (state/get-edit-pos)
+                  ;; TODO: right sidebar, what if multiple buffers later?
+                  ;; :right-sidebar? false
+
+                  ;; block-id will be updated when parsing the content, so it's
+                  ;; not reliably.
+                  ;; :block-id (:block/uuid edit-block)
+                  :block-idx (:idx last-edit-block)
+                  :block-container (:container last-edit-block)}
+                 {:data tx})
+            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)))]
+          ;; TODO: auto-save the block and undo at the same time
+          ;; Should we use core.async channel to force serialization?
+          (swap! history assoc repo (conj txs tx))
+          (swap! history-idx assoc repo idx'))))))
+
+(defonce *undoing? (atom false))
 
 (defn undo!
-  [repo alter-file]
+  [repo alter-file restore-cursor]
   (let [idx (get @history-idx repo 0)]
-    (when (> idx 0)
+    (when (and (> idx 0) (false? @*undoing?))
       (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}))
-        (swap! history-idx assoc repo idx')))))
+            tx (get-in @history [repo idx'])
+            {:keys [data]} tx
+            _ (reset! *undoing? true)
+            promises (for [[path patches] data]
+                       (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})))]
+        (-> (p/all promises)
+            (p/then (fn []
+                      (db/clear-query-state!)
+                      (swap! history-idx assoc repo idx')
+                      (reset! *undoing? false)
+                      ;; restore cursor
+                      (when (> idx' 0)
+                        (let [prev-tx (get-in @history [repo (dec idx')])]
+                          (when restore-cursor (restore-cursor prev-tx)))))))))))
 
+(defonce *redoing? (atom false))
 (defn redo!
-  [repo alter-file]
+  [repo alter-file restore-cursor]
   (let [idx (get @history-idx repo 0)
         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}))
-        (swap! history-idx assoc repo (inc idx))))))
+    (when (and (> (count txs) idx) (false? @*redoing?))
+      (let [tx (get-in @history [repo idx])
+            _ (reset! *redoing? true)
+            promises (for [[path patches] (:data 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})))]
+        (-> (p/all promises)
+            (p/then (fn []
+                      (db/clear-query-state!)
+                      (swap! history-idx assoc repo (inc idx))
+                      (reset! *redoing? false)
+                      ;; restore cursor
+                      (when restore-cursor (restore-cursor tx)))))))))

+ 3 - 2
src/main/frontend/keyboards.cljs

@@ -93,8 +93,9 @@
    "t r" ui-handler/toggle-right-sidebar!
    "t e" state/toggle-new-block-shortcut!
    "s" route-handler/toggle-between-page-and-file!
-   "ctrl+c ctrl+s" (chord-aux search-handler/rebuild-indices!)
-   "ctrl+c ctrl+b" (chord-aux config-handler/toggle-ui-show-brackets!)})
+   "mod+s" (chord-aux editor-handler/save!)
+   "mod+c mod+s" (chord-aux search-handler/rebuild-indices!)
+   "mod+c mod+b" (chord-aux config-handler/toggle-ui-show-brackets!)})
 
 (defonce bind! (gobj/get mousetrap "bind"))
 

+ 1 - 1
src/main/frontend/routes.cljs

@@ -15,7 +15,7 @@
     {:name :home
      :view home/home}]
 
-   ["/repos"
+   ["/graphs"
     {:name :repos
      :view repo/repos}]
 

+ 20 - 4
src/main/frontend/state.cljs

@@ -183,10 +183,19 @@
   (not (false? (:feature/enable-journals?
                 (get (sub-config) repo)))))
 
+(defn enable-git-auto-push?
+  [repo]
+  (not (false? (:git-auto-push
+                (get (sub-config) repo)))))
+
 (defn enable-block-time?
   []
-  (true? (:feature/enable-block-time?
-          (get (sub-config) (get-current-repo)))))
+  ;; (true? (:feature/enable-block-time?
+  ;;         (get (sub-config) (get-current-repo))))
+
+  ;; Disable block timestamps for now, because it doesn't work with undo/redo
+  false
+  )
 
 ;; Enable by default
 (defn show-brackets?
@@ -631,7 +640,14 @@
 (defn set-editing!
   [edit-input-id content block cursor-range]
   (when edit-input-id
-    (let [content (or content "")]
+    (let [block-element (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block"))
+          {:keys [idx container]} (util/get-block-idx-inside-container block-element)
+          block (if (and idx container)
+                  (assoc block
+                         :block/idx idx
+                         :block/container (gobj/get container "id"))
+                  block)
+          content (or content "")]
       (swap! state
              (fn [state]
                (-> state
@@ -990,7 +1006,7 @@
     (or
      (when-let [last-time (get-in @state [:editor/last-input-time repo])]
        (let [now (util/time-ms)]
-         (>= (- now last-time) 3000)))
+         (>= (- now last-time) 1000)))
      ;; not in editing mode
      (not (get-edit-input-id)))))
 

+ 18 - 0
src/main/frontend/util.cljs

@@ -625,6 +625,13 @@
     (and node
          (rec-get-blocks-container (gobj/get node "parentNode")))))
 
+(defn rec-get-blocks-content-section
+  [node]
+  (if (and node (d/has-class? node "content"))
+    node
+    (and node
+         (rec-get-blocks-content-section (gobj/get node "parentNode")))))
+
 ;; Take the idea from https://stackoverflow.com/questions/4220478/get-all-dom-block-elements-for-selected-texts.
 ;; FIXME: Note that it might not works for IE.
 (defn get-selected-nodes
@@ -775,6 +782,17 @@
                   (recur (inc idx))))
               nil)))))))
 
+(defn get-block-idx-inside-container
+  [block-element]
+  (when block-element
+    (when-let [section (some-> (rec-get-blocks-content-section block-element)
+                          (d/parent))]
+      (let [blocks (d/by-class section "ls-block")
+            idx (when (seq blocks) (.indexOf (array-seq blocks) block-element))]
+        (when (and idx section)
+         {:idx idx
+          :container (gdom/getElement section "id")})))))
+
 (defn nth-safe [c i]
   (if (or (< i 0) (>= i (count c)))
     nil

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

@@ -178,3 +178,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"