Browse Source

Merge branch 'master' into feat/db

Tienson Qin 2 years ago
parent
commit
23d918dcc2

+ 1 - 0
deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -213,6 +213,7 @@
   (second (re-find #"(?:\.)(\w+)[^.]*$" path-or-file-name)))
 
 (defn get-format
+  "File path to format keyword, :org, :markdown, etc."
   [file]
   (when file
     (normalize-format (keyword (some-> (path->file-ext file) string/lower-case)))))

+ 1 - 1
deps/graph-parser/test/logseq/graph_parser/cli_test.cljs

@@ -47,7 +47,7 @@
   (fs/mkdirSync (path/join dir "journals"))
   (fs/mkdirSync (path/join dir "pages")))
 
-(deftest ^:focus build-graph-files
+(deftest build-graph-files
   (create-logseq-graph "tmp/test-graph")
   ;; Create files that are recognized
   (fs/writeFileSync "tmp/test-graph/pages/foo.md" "")

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -519,7 +519,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.9;
+				MARKETING_VERSION = 0.9.10;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -546,7 +546,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.9;
+				MARKETING_VERSION = 0.9.10;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -571,7 +571,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.9;
+				MARKETING_VERSION = 0.9.10;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -598,7 +598,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.9;
+				MARKETING_VERSION = 0.9.10;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 3 - 3
package.json

@@ -90,8 +90,8 @@
         "@capawesome/capacitor-background-task": "^2.0.0",
         "@excalidraw/excalidraw": "0.12.0",
         "@hugotomazi/capacitor-navigation-bar": "^2.0.0",
-        "@logseq/capacitor-file-sync": "0.0.24",
-        "@logseq/diff-merge": "^0.0.2",
+        "@logseq/capacitor-file-sync": "0.0.30",
+        "@logseq/diff-merge": "0.1.0",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@radix-ui/colors": "^0.1.8",
         "@sentry/react": "^6.18.2",
@@ -117,7 +117,7 @@
         "highlight.js": "10.4.1",
         "ignore": "5.1.8",
         "jszip": "3.8.0",
-        "mldoc": "^1.5.5",
+        "mldoc": "1.5.7",
         "path": "0.12.7",
         "path-complete-extname": "1.0.0",
         "pixi-graph-fork": "0.2.0",

+ 1 - 1
resources/package.json

@@ -37,7 +37,7 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.70",
+    "@logseq/rsapi": "0.0.73",
     "electron-deeplink": "1.0.10",
     "abort-controller": "3.0.0",
     "fastify": "latest",

+ 1 - 0
shadow-cljs.edn

@@ -38,6 +38,7 @@
                            frontend.modules.instrumentation.sentry/SENTRY-DSN #shadow/env "LOGSEQ_SENTRY_DSN"
                            frontend.modules.instrumentation.posthog/POSTHOG-TOKEN #shadow/env "LOGSEQ_POSTHOG_TOKEN"
                            frontend.config/ENABLE-PLUGINS #shadow/env ["ENABLE_PLUGINS" :as :bool :default true]
+                           ;; Set to switch file sync server to dev, set this to false in `yarn watch`
                            frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]
                            frontend.config/TEST #shadow/env ["LOGSEQ_CI" :as :bool :default false]
                            frontend.config/REVISION #shadow/env ["LOGSEQ_REVISION" :default "dev"]} ;; set by git-revision-hook

+ 3 - 0
src/electron/electron/file_sync_rsapi.cljs

@@ -26,6 +26,9 @@
 (defn delete-local-files [graph-uuid base-path file-paths]
   (rsapi/deleteLocalFiles graph-uuid base-path (clj->js file-paths)))
 
+(defn fetch-remote-files [graph-uuid base-path file-paths token]
+  (rsapi/fetchRemoteFiles graph-uuid base-path (clj->js file-paths) token))
+
 (defn update-local-files [graph-uuid base-path file-paths token]
   (rsapi/updateLocalFiles graph-uuid base-path (clj->js file-paths) token))
 

+ 7 - 0
src/electron/electron/handler.cljs

@@ -97,6 +97,10 @@
     (catch :default _e
       false)))
 
+(defmethod handle :copyFile [_window [_ _repo from-path to-path]]
+  (logger/info ::copy-file from-path to-path)
+  (fs-extra/copy from-path to-path))
+
 (defmethod handle :writeFile [window [_ repo path content]]
   (let [^js Buf (.-Buffer buffer)
         ^js content (if (instance? js/ArrayBuffer content)
@@ -719,6 +723,9 @@
 (defmethod handle :delete-local-files [_ args]
   (apply rsapi/delete-local-files (rest args)))
 
+(defmethod handle :fetch-remote-files [_ args]
+  (apply rsapi/fetch-remote-files (rest args)))
+
 (defmethod handle :update-local-files [_ args]
   (apply rsapi/update-local-files (rest args)))
 

+ 10 - 3
src/main/frontend/extensions/html_parser.cljs

@@ -245,7 +245,7 @@
                                 " |")
 
                            (_ :guard #(contains? #{:aside :center :figure :figcaption :fieldset :footer :header} %))
-                           (export-hiccup x)
+                           (throw (js/Error. (str "HTML->Hiccup: " tag " not supported yet")))
 
                            :ul (map-join children :list? true)
                            :ol (map-join children :list? true)
@@ -280,12 +280,19 @@
                      (goog.string.unescapeEntities f)
                      f)) hiccup))
 
+(defn- remove-ending-dash-lines
+  [s]
+  (if (string? s)
+    (string/replace s #"(\n*-\s*\n*)*$" "")
+    s))
+
 (defn convert
   [format html]
   (when-not (string/blank? html)
     (let [hiccup (hickory/as-hiccup (hickory/parse html))
-          decoded-hiccup (html-decode-hiccup hiccup)]
-      (hiccup->doc format decoded-hiccup))))
+          decoded-hiccup (html-decode-hiccup hiccup)
+          result (hiccup->doc format decoded-hiccup)]
+      (remove-ending-dash-lines result))))
 
 (comment
   ;; | Syntax      | Description | Test Text     |``

+ 22 - 17
src/main/frontend/fs.cljs

@@ -137,6 +137,22 @@
             new-fpath (path/path-join repo-dir new-path)]
         (protocol/rename! (get-fs old-fpath) repo old-fpath new-fpath)))))
 
+(defn stat
+  ([fpath]
+   (protocol/stat (get-fs fpath) fpath))
+  ([dir path]
+   (let [fpath (path/path-join dir path)]
+     (protocol/stat (get-fs dir) fpath))))
+
+(defn mkdir-if-not-exists
+  [dir]
+  (when dir
+    (util/p-handle
+     (stat dir)
+     (fn [_stat])
+     (fn [_error]
+       (mkdir-recur! dir)))))
+
 (defn copy!
   "Only used by Logseq Sync"
   [repo old-path new-path]
@@ -149,15 +165,13 @@
           (map #(if (or (util/electron?) (mobile-util/native-platform?))
                   %
                   (str (config/get-repo-dir repo) "/" %))
-               [old-path new-path])]
-      (protocol/copy! (get-fs old-path) repo old-path new-path))))
+               [old-path new-path])
+          new-dir (path/dirname new-path)]
+      (p/do!
+       (mkdir-if-not-exists new-dir)
+       (protocol/copy! (get-fs old-path) repo old-path new-path)))))
+
 
-(defn stat
-  ([fpath]
-   (protocol/stat (get-fs fpath) fpath))
-  ([dir path]
-   (let [fpath (path/path-join dir path)]
-     (protocol/stat (get-fs dir) fpath))))
 
 (defn open-dir
   [dir]
@@ -192,15 +206,6 @@
   [dir]
   (protocol/unwatch-dir! (get-fs dir) dir))
 
-(defn mkdir-if-not-exists
-  [dir]
-  (when dir
-    (util/p-handle
-     (stat dir)
-     (fn [_stat])
-     (fn [_error]
-       (mkdir! dir)))))
-
 ;; FIXME: counterintuitive return value
 (defn create-if-not-exists
   "Create a file if it doesn't exist. return false on written, true on already exists"

+ 116 - 17
src/main/frontend/fs/diff_merge.cljs

@@ -1,24 +1,17 @@
 (ns frontend.fs.diff-merge
   "Implementation of text (file) based content diff & merge for conflict resolution"
-  (:require ["@logseq/diff-merge" :refer [Differ attach_uuids]]
+  (:require ["@logseq/diff-merge" :refer [attach_uuids Differ Merger]]
+            [cljs-bean.core :as bean]
+            [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
             [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.utf8 :as utf8]
-            [cljs-bean.core :as bean]
-            [frontend.db.utils :as db-utils]
-            [frontend.db.model :as db-model]))
-
-;; (defn diff-merge
-;;   "N-ways diff & merge
-;;    Accept: blocks
-;;    https://github.com/logseq/diff-merge/blob/44546f2427f20bd417b898c8ba7b7d10a9254774/lib/mldoc.ts#L17-L22
-;;    https://github.com/logseq/diff-merge/blob/85ca7e9bf7740d3880ed97d535a4f782a963395d/lib/merge.ts#L40"
-;;   [base & branches]
-;;   ()
-;;   (let [merger (Merger.)]
-;;     (.mergeBlocks merger (bean/->js base) (bean/->js branches))))
-
-(defn diff 
+            [clojure.string :as string]))
+
+
+(defn diff
   "2-ways diff
    Accept: blocks in the struct with the required info
    Please refer to the `Block` struct in the link below
@@ -46,6 +39,7 @@
                     blocks levels)]
     blocks))
 
+;; TODO: Switch to ast->diff-blocks-alt
 ;; Diverged from gp-block/extract-blocks for decoupling
 ;; The process of doing 2 way diff is like:
 ;; 1. Given a base ver. of page (AST in DB), and a branch ver. of page (externally modified file content)
@@ -71,7 +65,7 @@
               pos-meta (assoc pos-meta :end_pos end-pos)]
           (cond
             (gp-block/heading-block? block)
-            (let [content (gp-block/get-block-content encoded-content block format pos-meta block-pattern)]
+            (let [content (gp-block/get-block-content encoded-content (second block) format pos-meta block-pattern)]
               (recur (conj headings {:body  content
                                      :level (:level (second block))
                                      :uuid  (:id properties)})
@@ -87,6 +81,7 @@
             (recur headings (rest blocks) properties (:end_pos pos-meta))))
         (if (empty? properties)
           (reverse headings)
+          ;; Add pre-blocks
           (let [[block _] (first blocks)
                 pos-meta {:start_pos 0 :end_pos end-pos}
                 content (gp-block/get-block-content encoded-content block format pos-meta block-pattern)
@@ -95,3 +90,107 @@
                    :level 1
                    :uuid uuid}
                   (reverse headings))))))))
+
+
+(defn- get-sub-content-from-pos-meta
+  "Replace gp-block/get-block-content, return bare content, without any trim"
+  [raw-content pos-meta]
+  (let [{:keys [start_pos end_pos]} pos-meta]
+    (utf8/substring raw-content start_pos end_pos)))
+
+;; Diverged from ast->diff-blocks
+;; Add :meta :raw-body to the block
+(defn- ast->diff-blocks-alt
+  "Prepare the blocks for diff-merge
+   blocks: ast of blocks
+   content: corresponding raw content"
+  [blocks content format {:keys [user-config block-pattern]}]
+  {:pre [(string? content) (contains? #{:markdown :org} format)]}
+  (let [utf8-encoded-content (utf8/encode content)]
+    (loop [headings []
+           blocks (reverse blocks)
+           properties {}
+           end-pos (.-length utf8-encoded-content)]
+      (cond
+        (seq blocks)
+        (let [[block pos-meta] (first blocks)
+              ;; fix start_pos for properties
+              fixed-pos-meta (assoc pos-meta :end_pos end-pos)]
+          (cond
+            (gp-block/heading-block? block)
+            (let [content (gp-block/get-block-content utf8-encoded-content (second block) format fixed-pos-meta block-pattern)
+                  content-raw (get-sub-content-from-pos-meta utf8-encoded-content fixed-pos-meta)]
+              (recur (conj headings {:body  content
+                                     :meta  {:raw-body (string/trimr content-raw)}
+                                     :level (:level (second block))
+                                     :uuid  (:id properties)})
+                     (rest blocks)
+                     {}
+                     (:start_pos fixed-pos-meta))) ;; The current block's start pos is the next block's end pos
+
+            (gp-property/properties-ast? block)
+            (let [new-props (:properties (gp-block/extract-properties (second block) (assoc user-config :format format)))]
+              ;; sending the current end pos to next, as it's not finished yet
+              ;; supports multiple properties sub-block possible in future
+              (recur headings (rest blocks) (merge properties new-props) (:end_pos fixed-pos-meta)))
+
+            :else
+            (recur headings (rest blocks) properties (:end_pos fixed-pos-meta))))
+
+        (empty? properties)
+        (reverse headings)
+
+        ;; Add pre-blocks
+        :else ;; ??? unreachable
+        (let [[block _] (first blocks)
+              pos-meta {:start_pos 0 :end_pos end-pos}
+              content (gp-block/get-block-content utf8-encoded-content block format pos-meta block-pattern)
+              content-raw (get-sub-content-from-pos-meta utf8-encoded-content pos-meta)
+              uuid (:id properties)]
+          (cons {:body content
+                 :meta {:raw-body (string/trimr content-raw)}
+                 :level 1
+                 :uuid uuid}
+                (reverse headings)))))))
+
+(defn- rebuild-content
+  "translate [[[op block]]] to merged content"
+  [_base-diffblocks diffs _format]
+  ;; [[[0 {:body "attrib:: xxx", :level 1, :uuid nil}] ...] ...]
+  (let  [ops-fn (fn [ops]
+                  (map (fn [[op {:keys [meta]}]]
+                         (when (or (= op 0) (= op 1)) ;; equal or insert
+                           (:raw-body meta)))
+                       ops))]
+    (->> diffs
+         (mapcat ops-fn)
+         (filter seq)
+         (string/join "\n"))))
+
+(defn three-way-merge
+  [base income current format]
+  (let [->ast (fn [text] (if (= format :org)
+                           (gp-mldoc/->edn text (gp-mldoc/default-config :org))
+                           (gp-mldoc/->edn text (gp-mldoc/default-config :markdown))))
+        options (if (= format :org)
+                  {:block-pattern "*"}
+                  {:block-pattern "-"})
+        merger (Merger.)
+        base-ast (->ast base)
+        base-diffblocks (ast->diff-blocks-alt base-ast base format options)
+        income-ast (->ast income)
+        income-diffblocks (ast->diff-blocks-alt income-ast income format options)
+        current-ast (->ast current)
+        current-diffblocks (ast->diff-blocks-alt current-ast current format options)
+        branch-diffblocks [income-diffblocks current-diffblocks]
+        merged (.mergeBlocks merger (bean/->js base-diffblocks) (bean/->js branch-diffblocks))
+        ;; For extracting diff-merge test cases
+        ;; _ (prn "input:")
+        ;; _ (prn (js/JSON.stringify (bean/->js base-diffblocks)))
+        ;; _ (prn (js/JSON.stringify (bean/->js branch-diffblocks)))
+        ;; _ (prn "logseq diff merge version: " version)
+        ;; _ (prn "output:")
+        ;; _ (prn (js/JSON.stringify merged))
+        merged-diff (bean/->clj merged)
+        merged-content (rebuild-content base-diffblocks merged-diff format)]
+    merged-content))

+ 3 - 1
src/main/frontend/fs/node.cljs

@@ -118,7 +118,9 @@
 
   (rename! [_this _repo old-path new-path]
     (ipc/ipc "rename" old-path new-path))
-
+  ;; copy with overwrite, without confirmation
+  (copy! [_this repo old-path new-path]
+    (ipc/ipc "copyFile" repo old-path new-path))
   (stat [_this fpath]
     (-> (ipc/ipc "stat" fpath)
         (p/then bean/->clj)))

+ 189 - 43
src/main/frontend/fs/sync.cljs

@@ -1,41 +1,45 @@
 (ns frontend.fs.sync
   "Main ns for providing file sync functionality"
-  (:require [cljs-http.client :as http]
+  (:require ["@capawesome/capacitor-background-task" :refer [BackgroundTask]]
+            ["path" :as node-path]
+            [cljs-http.client :as http]
+            [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [cljs-time.format :as tf]
-            [cljs-time.coerce :as tc]
-            [cljs.core.async :as async :refer [go timeout go-loop offer! poll! chan <! >!]]
+            [cljs.core.async :as async :refer [<! >! chan go go-loop offer!
+                                               poll! timeout]]
             [cljs.core.async.impl.channels]
             [cljs.core.async.interop :refer [p->c]]
             [cljs.spec.alpha :as s]
+            [clojure.pprint :as pp]
             [clojure.set :as set]
             [clojure.string :as string]
-            [clojure.pprint :as pp]
             [electron.ipc :as ipc]
-            [goog.string :as gstring]
             [frontend.config :as config]
+            [frontend.context.i18n :refer [t]]
+            [frontend.db :as db]
             [frontend.debug :as debug]
+            [frontend.diff :as diff]
+            [frontend.encrypt :as encrypt]
+            [frontend.fs :as fs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
+            [frontend.fs.diff-merge :as diff-merge]
+            [frontend.handler.file :as file-handler]
+            [frontend.handler.notification :as notification]
             [frontend.handler.user :as user]
-            [frontend.state :as state]
             [frontend.mobile.util :as mobile-util]
+            [frontend.pubsub :as pubsub]
+            [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.util.persist-var :as persist-var]
             [frontend.util.fs :as fs-util]
-            [frontend.handler.notification :as notification]
-            [frontend.context.i18n :refer [t]]
-            [frontend.diff :as diff]
-            [frontend.db :as db]
-            [frontend.fs :as fs]
-            [frontend.encrypt :as encrypt]
-            [frontend.pubsub :as pubsub]
+            [frontend.util.persist-var :as persist-var]
+            [goog.string :as gstring]
+            [lambdaisland.glogi :as log]
+            [logseq.common.path :as path]
             [logseq.graph-parser.util :as gp-util]
             [medley.core :refer [dedupe-by]]
-            [rum.core :as rum]
             [promesa.core :as p]
-            [lambdaisland.glogi :as log]
-            [frontend.fs.capacitor-fs :as capacitor-fs]
-            ["@capawesome/capacitor-background-task" :refer [BackgroundTask]]
-            ["path" :as node-path]))
+            [rum.core :as rum]))
 
 ;;; ### Commentary
 ;; file-sync related local files/dirs:
@@ -606,7 +610,7 @@
 (defn diff-file-metadata-sets
   "Find the `FileMetadata`s that exists in s1 and does not exist in s2,
   compare by path+checksum+last-modified,
-  if s1.path = s2.path & s1.checksum <> s2.checksum & s1.last-modified > s2.last-modified
+  if s1.path = s2.path & s1.checksum <> s2.checksum
   (except some default created files),
   keep this `FileMetadata` in result"
   [s1 s2]
@@ -632,7 +636,11 @@
                (and (contains? ignore-default-value-files path)
                     (#{config/config-default-content-md5 empty-custom-css-md5} (:etag %)))
                false
-               (< last-modified (:last-modified %))
+               ;; special handling for css & edn files
+               (and
+                (or (string/ends-with? lower-case-path ".css")
+                    (string/ends-with? lower-case-path ".edn"))
+                (< last-modified (:last-modified %)))
                true)
             s2)
          result
@@ -719,6 +727,7 @@
   (<get-local-all-files-meta [this graph-uuid base-path] "get all local files' metadata")
   (<rename-local-file [this graph-uuid base-path from to])
   (<update-local-files [this graph-uuid base-path filepaths] "remote -> local")
+  (<fetch-remote-files [this graph-uuid base-path filepaths] "remote -> local version-db")
   (<download-version-files [this graph-uuid base-path filepaths])
   (<delete-local-files [this graph-uuid base-path filepaths])
   (<update-remote-files [this graph-uuid base-path filepaths local-txid] "local -> remote, return err or txid")
@@ -850,6 +859,12 @@
       (<! (<rsapi-cancel-all-requests))
       (let [token (<! (<get-token this))]
         (<! (p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token))))))
+  (<fetch-remote-files [this graph-uuid base-path filepaths]
+    (go
+      (<! (<rsapi-cancel-all-requests))
+      (let [token (<! (<get-token this))]
+        (<! (p->c (ipc/ipc "fetch-remote-files" graph-uuid base-path filepaths token))))))
+
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
       (let [token (<! (<get-token this))
@@ -944,7 +959,16 @@
                                                                      :basePath base-path
                                                                      :filePaths filepaths'
                                                                      :token token})))))))
-
+  (<fetch-remote-files [this graph-uuid base-path filepaths]
+    (go
+      (let [token (<! (<get-token this))
+            r (<! (<retry-rsapi
+                   #(p->c (.fetchRemoteFiles mobile-util/file-sync
+                                             (clj->js {:graphUUID graph-uuid
+                                                       :basePath base-path
+                                                       :filePaths filepaths
+                                                       :token token})))))]
+        (js->clj (.-value r)))))
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
       (let [token (<! (<get-token this))
@@ -980,12 +1004,13 @@
             r
             (get (js->clj r) "txid"))))))
 
-  (<delete-remote-files [this graph-uuid _base-path filepaths local-txid]
+  (<delete-remote-files [this graph-uuid base-path filepaths local-txid]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
       (go
         (let [token (<! (<get-token this))
               r (<! (p->c (.deleteRemoteFiles mobile-util/file-sync
                                               (clj->js {:graphUUID graph-uuid
+                                                        :basePath base-path
                                                         :filePaths normalized-filepaths
                                                         :txid local-txid
                                                         :token token}))))]
@@ -1461,7 +1486,10 @@
       (let [rpath (relative-path filetxn)
             repo (state/get-current-repo)
             repo-dir (config/get-repo-dir repo)
-            content (<! (p->c (fs/read-file repo-dir rpath)))]
+            content (<! (p->c (-> (fs/file-exists? repo-dir rpath)
+                                  (p/then (fn [exists?]
+                                            (when exists?
+                                              (fs/read-file repo-dir rpath)))))))]
         (and (seq origin-db-content)
              (or (nil? content)
                  (some :removed (diff/diff origin-db-content content))))))))
@@ -1539,6 +1567,117 @@
                            delete-filetxns)]
     (set (concat update-file-items rename-file-items delete-file-items))))
 
+(defn- <apply-remote-deletion
+  "Apply remote deletion, if the file is not deleted locally, delete it locally.
+   if the file is changed locally, leave the changed part.
+
+   To replace <delete-local-files"
+  [graph-uuid base-path relative-paths]
+  (go
+    (p->c (p/all (->> relative-paths
+                      (map (fn [rpath]
+                             (p/let [base-file (path/path-join "logseq/version-files/base" rpath)
+                                     current-change-file rpath
+                                     format (gp-util/get-format current-change-file)
+                                     repo (state/get-current-repo)
+                                     repo-dir (config/get-repo-dir repo)
+                                     base-exists? (fs/file-exists? repo-dir base-file)]
+                               (if base-exists?
+                                 (p/let [base-content (fs/read-file repo-dir base-file)
+                                         current-content (-> (fs/read-file repo-dir current-change-file)
+                                                             (p/catch (fn [_] nil)))]
+                                   (if (= base-content current-content)
+                                     ;; base-content == current-content, delete current-change-file
+                                     (p/do!
+                                      (<delete-local-files rsapi graph-uuid base-path [rpath])
+                                      (fs/unlink! repo (path/path-join repo-dir base-file) {}))
+                                     ;; base-content != current-content, merge, do not delete
+                                     (p/let [merged-content (diff-merge/three-way-merge base-content "" current-content format)]
+                                       (fs/write-file! repo repo-dir current-change-file merged-content {:skip-compare? true})
+                                       (file-handler/alter-file repo current-change-file merged-content {:re-render-root? true
+                                                                                                         :from-disk? true
+                                                                                                         :fs/event :fs/remote-file-change}))))
+
+                                 ;; no base-version, use legacy approach, delete it
+                                 (<delete-local-files rsapi graph-uuid base-path [rpath]))))))))))
+
+(defn- <fetch-remote-and-update-local-files
+  [graph-uuid base-path relative-paths]
+  (go
+    (let [fetched-file-rpaths-or-ex (<! (<fetch-remote-files rsapi graph-uuid base-path relative-paths))]
+      (if (instance? ExceptionInfo fetched-file-rpaths-or-ex)
+        fetched-file-rpaths-or-ex
+        (<!
+         (p->c (p/all (->> fetched-file-rpaths-or-ex
+                           (map (fn [rpath]
+                                  (p/let [incoming-file (path/path-join "logseq/version-files/incoming" rpath)
+                                          base-file (path/path-join "logseq/version-files/base" rpath)
+                                          current-change-file rpath
+                                          format (gp-util/get-format current-change-file)
+                                          repo (state/get-current-repo)
+                                          repo-dir (config/get-repo-dir repo)
+                                          base-exists? (fs/file-exists? repo-dir base-file)]
+                                    (cond
+                                      base-exists?
+                                      (p/let [base-content (fs/read-file repo-dir base-file)
+                                              current-content (-> (fs/read-file repo-dir current-change-file)
+                                                                  (p/catch (fn [_] nil)))
+                                              incoming-content (fs/read-file repo-dir incoming-file)]
+                                        (if (= base-content current-content)
+                                          (do
+                                            (prn "[diff-merge]base=current, write directly")
+                                            (p/do!
+                                             (fs/copy! repo
+                                                       (path/path-join repo-dir incoming-file)
+                                                       (path/path-join repo-dir current-change-file))
+                                             (fs/copy! repo
+                                                       (path/path-join repo-dir incoming-file)
+                                                       (path/path-join repo-dir base-file))
+                                             (file-handler/alter-file repo current-change-file incoming-content {:re-render-root? true
+                                                                                                                 :from-disk? true
+                                                                                                                 :fs/event :fs/remote-file-change})))
+                                          (do
+                                            (prn "[diff-merge]base!=current, 3-way merge")
+                                            (p/let [current-content (or current-content "")
+                                                    incoming-content (fs/read-file repo-dir incoming-file)
+                                                    merged-content (diff-merge/three-way-merge base-content incoming-content current-content format)]
+                                              (when (seq merged-content)
+                                                (p/do!
+                                                 (fs/write-file! repo repo-dir current-change-file merged-content {:skip-compare? true})
+                                                 (file-handler/alter-file repo current-change-file merged-content {:re-render-root? true
+                                                                                                                   :from-disk? true
+                                                                                                                   :fs/event :fs/remote-file-change})))))))
+
+                                      :else
+                                      (do
+                                        (prn "[diff-merge]no base found, use empty content as base, avoid loosing data")
+                                        (p/let [current-content (-> (fs/read-file repo-dir current-change-file)
+                                                                    (p/catch (fn [_] nil)))
+                                                current-content (or current-content "")
+                                                incoming-content (fs/read-file repo-dir incoming-file)
+                                                merged-content (diff-merge/three-way-merge "" current-content incoming-content format)]
+                                          (if (= incoming-content merged-content)
+                                            (p/do!
+                                             (fs/copy! repo
+                                                       (path/path-join repo-dir incoming-file)
+                                                       (path/path-join repo-dir current-change-file))
+                                             (fs/copy! repo
+                                                       (path/path-join repo-dir incoming-file)
+                                                       (path/path-join repo-dir base-file))
+                                             (file-handler/alter-file repo current-change-file merged-content {:re-render-root? true
+                                                                                                               :from-disk? true
+                                                                                                               :fs/event :fs/remote-file-change}))
+
+                                          ;; else
+                                            (p/do!
+                                             (fs/write-file! repo repo-dir current-change-file merged-content {:skip-compare? true})
+                                             (file-handler/alter-file repo current-change-file merged-content {:re-render-root? true
+                                                                                                               :from-disk? true
+                                                                                                               :fs/event :fs/remote-file-change})
+                                             (file-handler/alter-file repo current-change-file merged-content {:re-render-root? true
+                                                                                                               :from-disk? true
+                                                                                                               :fs/event :fs/remote-file-change})))))))))))))))))
+
 (defn- apply-filetxns
   [*sync-state graph-uuid base-path filetxns *paused]
   (go
@@ -1574,7 +1713,7 @@
                 (swap! *sync-state sync-state--remove-recent-remote->local-files
                        [recent-remote->local-file-item])))))
 
-        (let [update-local-files-ch (<update-local-files rsapi graph-uuid base-path (map relative-path filetxns))
+        (let [update-local-files-ch (<fetch-remote-and-update-local-files graph-uuid base-path (map relative-path filetxns))
               r (<! (<with-pause update-local-files-ch *paused))]
           (doseq [[filetxn origin-db-content] txn->db-content-vec]
             (when (<! (need-add-version-file? filetxn origin-db-content))
@@ -1592,7 +1731,7 @@
         (if (<! (<local-file-not-exist? graph-uuid rsapi base-path (relative-path filetxn)))
           ;; not exist, ignore
           true
-          (let [r (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path filetxn)]))]
+          (let [r (<! (<apply-remote-deletion graph-uuid base-path [(relative-path filetxn)]))]
             (if (and (instance? ExceptionInfo r)
                      (string/index-of (str (ex-cause r)) "No such file or directory"))
               true
@@ -2714,20 +2853,20 @@
 ;;; ### put all stuff together
 
 (defrecord ^:large-vars/cleanup-todo
-  SyncManager [user-uuid graph-uuid base-path *sync-state
-               ^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
-               ^:mutable ratelimit-local-changes-chan
-               *txid *txid-for-get-deletion-log
-               ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
-               ^:mutable ops-chan ^:mutable app-awake-from-sleep-chan
+    SyncManager [user-uuid graph-uuid base-path *sync-state
+              ^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
+              ^:mutable ratelimit-local-changes-chan
+              *txid *txid-for-get-deletion-log
+              ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
+              ^:mutable ops-chan ^:mutable app-awake-from-sleep-chan
                ;; control chans
-               private-full-sync-chan private-remote->local-sync-chan
-               private-remote->local-full-sync-chan private-pause-resume-chan]
+              private-full-sync-chan private-remote->local-sync-chan
+              private-remote->local-full-sync-chan private-pause-resume-chan]
   Object
   (schedule [this next-state args reason]
     {:pre [(s/valid? ::state next-state)]}
     (println (str "[SyncManager " graph-uuid "]")
-             (and state (name state)) "->" (and next-state (name next-state)) :reason reason :local-txid @*txid :now (tc/to-string (t/now)))
+             (and state (name state)) "->" (and next-state (name next-state)) :reason reason :local-txid @*txid :args args :now (tc/to-string (t/now)))
     (set! state next-state)
     (swap! *sync-state sync-state--update-state next-state)
     (go
@@ -2925,7 +3064,7 @@
                                       :epoch      (tc/to-epoch (t/now))}})
             (.schedule this ::idle nil nil))))))
 
-  (remote->local-full-sync [this _]
+  (remote->local-full-sync [this {:keys [retry-count]}]
     (go
       (let [{:keys [succ unknown stop pause]}
             (<! (<sync-remote->local-all-files! remote->local-syncer))]
@@ -2952,12 +3091,19 @@
                               :data  {:graph-uuid graph-uuid
                                       :exp        unknown
                                       :epoch      (tc/to-epoch (t/now))}})
-            (let [next-state (if (string/includes? (str (ex-cause unknown)) "404 Not Found")
-                               ;; TODO: this should never happen
-                               ::stop
-                               ;; if any other exception occurred, re-exec remote->local-full-sync
-                               ::remote->local-full-sync)]
-              (.schedule this next-state nil nil)))))))
+            (let [next-state
+                  (cond
+                    (string/includes? (str (ex-cause unknown)) "404 Not Found")
+                    ;; TODO: this should never happen
+                    ::stop
+                    (> retry-count 3)
+                    ::stop
+
+                    :else ;; if any other exception occurred, re-exec remote->local-full-sync
+                    ::remote->local-full-sync)]
+              (.schedule this next-state
+                         (when (= ::remote->local-full-sync next-state) {:retry-count (inc retry-count)})
+                         nil)))))))
 
   (remote->local [this _next-state {remote-val :remote}]
     (go
@@ -3002,7 +3148,7 @@
       (let [distincted-local-changes (distinct-file-change-events local-changes)
             _ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distincted-local-changes))
             change-events-partitions
-                                     (sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
+            (sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
             _ (put-sync-event! {:event :start
                                 :data  {:type       :local->remote
                                         :graph-uuid graph-uuid

+ 2 - 2
src/main/frontend/handler/common/file.cljs

@@ -72,8 +72,8 @@
    Decide how to treat the parsed file based on the file's triggering event
    options - 
      :fs/reset-event - the event that triggered the file update
-       :fs/local-file-change - file changed on local disk
-       :fs/remote-file-change - file changed on remote"
+     :fs/local-file-change - file changed on local disk
+     :fs/remote-file-change - file changed on remote"
   [repo-url file-path content {:fs/keys [event] :as options}]
   (let [db-conn (db/get-db repo-url false)]
     (case event

+ 1 - 1
src/main/frontend/handler/paste.cljs

@@ -248,7 +248,7 @@
       (cond
         (and (string/blank? text) (string/blank? html))
         ;; When both text and html are blank, paste file if exists.
-        ;; NOTE: util/stop is not called here if no file is provided, 
+        ;; NOTE: util/stop is not called here if no file is provided,
         ;; so the default paste behavior of the native platform will be used.
         (when has-files?
           (paste-file-if-exists id e))

+ 143 - 30
src/test/frontend/fs/diff_merge_test.cljs

@@ -1,12 +1,13 @@
 (ns frontend.fs.diff-merge-test
-  (:require [cljs.test :refer [deftest are is]]
-            [logseq.db :as ldb]
-            [logseq.graph-parser :as graph-parser]
+  (:require [cljs-bean.core :as bean]
+            [cljs.test :refer [are deftest is]]
+            [frontend.db.conn :as conn]
             [frontend.fs.diff-merge :as fs-diff]
             [frontend.handler.common.file :as file-common-handler]
-            [frontend.db.conn :as conn]
+            [logseq.db :as ldb]
+            [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [cljs-bean.core :as bean]))
+            [logseq.graph-parser.text :as text]))
 
 (defn test-db->diff-blocks
   "A hijacked version of db->diff-blocks for testing.
@@ -28,18 +29,18 @@
 :ID:       72289d9a-eb2f-427b-ad97-b605a4b8c59b
 :END:
 #+tItLe: Well parsed!"
-[{:body ":PROPERTIES:\n:ID:       72289d9a-eb2f-427b-ad97-b605a4b8c59b\n:END:\n#+tItLe: Well parsed!" 
-  :uuid "72289d9a-eb2f-427b-ad97-b605a4b8c59b" 
+[{:body ":PROPERTIES:\n:ID:       72289d9a-eb2f-427b-ad97-b605a4b8c59b\n:END:\n#+tItLe: Well parsed!"
+  :uuid "72289d9a-eb2f-427b-ad97-b605a4b8c59b"
   :level 1}]
-    
+
     "#+title: Howdy"
     [{:body "#+title: Howdy" :uuid nil :level 1}]
-    
+
     ":PROPERTIES:
 :fiction: [[aldsjfklsda]]
 :END:\n#+title: Howdy"
-    [{:body ":PROPERTIES:\n:fiction: [[aldsjfklsda]]\n:END:\n#+title: Howdy" 
-      :uuid nil 
+    [{:body ":PROPERTIES:\n:fiction: [[aldsjfklsda]]\n:END:\n#+title: Howdy"
+      :uuid nil
       :level 1}]))
 
 (deftest db<->ast-diff-blocks-test
@@ -68,13 +69,23 @@
    {:body "b" :uuid nil :level 2}
    {:body "c" :uuid nil :level 3}]
 
+"- a
+\t- b
+\t\t- c
+\t\t  multiline
+- d"
+[{:body "a" :uuid nil :level 1}
+ {:body "b" :uuid nil :level 2}
+ {:body "c\nmultiline" :uuid nil :level 3}
+ {:body "d" :uuid nil :level 1}]
+
   "## hello
 \t- world
 \t\t- nice
 \t\t\t- nice
 \t\t\t- bingo
 \t\t\t- world"
-  [{:body "## hello" :uuid nil :level 2}
+  [{:body "## hello" :uuid nil :level 1}
    {:body "world" :uuid nil :level 2}
    {:body "nice" :uuid nil :level 3}
    {:body "nice" :uuid nil :level 4}
@@ -92,23 +103,97 @@
 \t- i
 - j"
   [{:body "# a" :uuid nil :level 1}
-   {:body "## b" :uuid nil :level 2}
-   {:body "### c" :uuid nil :level 3}
-   {:body "#### d" :uuid nil :level 4}
-   {:body "### e" :uuid nil :level 3}
+   {:body "## b" :uuid nil :level 1}
+   {:body "### c" :uuid nil :level 1}
+   {:body "#### d" :uuid nil :level 1}
+   {:body "### e" :uuid nil :level 1}
    {:body "f" :uuid nil :level 1}
    {:body "g" :uuid nil :level 2}
    {:body "h" :uuid nil :level 3}
    {:body "i" :uuid nil :level 2}
    {:body "j" :uuid nil :level 1}]
-  
+
     "- a\n  id:: 63e25526-3612-4fb1-8cf9-f66db1254a58
 \t- b
 \t\t- c"
-[{:body "a\n id:: 63e25526-3612-4fb1-8cf9-f66db1254a58" 
+[{:body "a\nid:: 63e25526-3612-4fb1-8cf9-f66db1254a58"
   :uuid "63e25526-3612-4fb1-8cf9-f66db1254a58" :level 1}
  {:body "b" :uuid nil :level 2}
- {:body "c" :uuid nil :level 3}]))
+ {:body "c" :uuid nil :level 3}]
+  
+  "alias:: ⭐️\nicon:: ⭐️"
+[{:body "alias:: ⭐️\nicon:: ⭐️", :level 1, :uuid nil}]))
+
+(defn text->diffblocks-alt
+  [text]
+  (-> (gp-mldoc/->edn text (gp-mldoc/default-config :markdown))
+      (#'fs-diff/ast->diff-blocks-alt text :markdown {:block-pattern "-"})))
+
+(deftest md->ast->diff-blocks-alt-test
+  (are [text diff-blocks]
+       (= (text->diffblocks-alt text)
+          diff-blocks)
+    "- a
+\t- b
+\t\t- c"
+    [{:body "a" :uuid nil :level 1 :meta {:raw-body "- a"}}
+     {:body "b" :uuid nil :level 2 :meta {:raw-body "\t- b"}}
+     {:body "c" :uuid nil :level 3 :meta {:raw-body "\t\t- c"}}]
+
+    "- a
+\t- b
+\t\t- c
+\t\t  multiline
+- d"
+    [{:body "a" :uuid nil :level 1 :meta {:raw-body "- a"}}
+     {:body "b" :uuid nil :level 2 :meta {:raw-body "\t- b"}}
+     {:body "c\nmultiline" :uuid nil :level 3 :meta {:raw-body "\t\t- c\n\t\t  multiline"}}
+     {:body "d" :uuid nil :level 1 :meta {:raw-body "- d"}}]
+
+    "## hello
+\t- world
+\t\t- nice
+\t\t\t- nice
+\t\t\t- bingo
+\t\t\t- world"
+    [{:body "## hello" :uuid nil :level 1 :meta {:raw-body "## hello"}}
+     {:body "world" :uuid nil :level 2 :meta {:raw-body "\t- world"}}
+     {:body "nice" :uuid nil :level 3 :meta {:raw-body "\t\t- nice"}}
+     {:body "nice" :uuid nil :level 4 :meta {:raw-body "\t\t\t- nice"}}
+     {:body "bingo" :uuid nil :level 4 :meta {:raw-body "\t\t\t- bingo"}}
+     {:body "world" :uuid nil :level 4 :meta {:raw-body "\t\t\t- world"}}]
+
+    "# a
+## b
+### c
+#### d
+### e
+- f
+\t- g
+\t\t- h
+\t- i
+- j"
+    [{:body "# a" :uuid nil :level 1 :meta {:raw-body "# a"}}
+     {:body "## b" :uuid nil :level 1 :meta {:raw-body "## b"}}
+     {:body "### c" :uuid nil :level 1 :meta {:raw-body "### c"}}
+     {:body "#### d" :uuid nil :level 1 :meta {:raw-body "#### d"}}
+     {:body "### e" :uuid nil :level 1 :meta {:raw-body "### e"}}
+     {:body "f" :uuid nil :level 1 :meta {:raw-body "- f"}}
+     {:body "g" :uuid nil :level 2 :meta {:raw-body "\t- g"}}
+     {:body "h" :uuid nil :level 3 :meta {:raw-body "\t\t- h"}}
+     {:body "i" :uuid nil :level 2 :meta {:raw-body "\t- i"}}
+     {:body "j" :uuid nil :level 1 :meta {:raw-body "- j"}}]
+
+    "- a\n  id:: 63e25526-3612-4fb1-8cf9-f66db1254a58
+\t- b
+\t\t- c"
+    [{:body "a\nid:: 63e25526-3612-4fb1-8cf9-f66db1254a58"
+      :uuid "63e25526-3612-4fb1-8cf9-f66db1254a58" :level 1 :meta {:raw-body "- a\n  id:: 63e25526-3612-4fb1-8cf9-f66db1254a58"}}
+     {:body "b" :uuid nil :level 2 :meta {:raw-body "\t- b"}}
+     {:body "c" :uuid nil :level 3 :meta {:raw-body "\t\t- c"}}]
+    
+    "alias:: ⭐️\nicon:: ⭐️"
+    [{:body "alias:: ⭐️\nicon:: ⭐️", :meta {:raw-body "alias:: ⭐️\nicon:: ⭐️"}, :level 1, :uuid nil}]))
 
 (deftest diff-test
   (are [text1 text2 diffs]
@@ -127,11 +212,14 @@
 \t\t\t- nice
 \t\t\t- bingo
 \t\t\t- world"
-    [[[-1 {:body "## hello"
-          :level 2
+    ;; Empty op, because no insertion op before the first base block required
+    ;; See https://github.com/logseq/diff-merge#usage
+    [[]
+     [[-1 {:body "## hello"
+          :level 1
           :uuid nil}]
       [1  {:body "## Halooooo"
-          :level 2
+          :level 1
           :uuid nil}]]
      [[0 {:body "world"
          :level 2
@@ -148,7 +236,7 @@
      [[0 {:body "world"
          :level 4
          :uuid nil}]]]
-    
+
     "## hello
 \t- world
 \t  id:: 63e25526-3612-4fb1-8cf9-abcd12354abc
@@ -162,16 +250,19 @@
 \t\t\t- nice
 \t\t\t- bingo
 \t\t\t- world"
-[[[-1 {:body "## hello"
-       :level 2
+;; Empty op, because no insertion op before the first base block required
+;; See https://github.com/logseq/diff-merge#usage
+[[]
+ [[-1 {:body "## hello"
+       :level 1
        :uuid nil}]
   [1  {:body "## Halooooo"
-       :level 2
+       :level 1
        :uuid nil}]
   [1 {:body "world"
       :level 2
       :uuid nil}]]
- [[-1 {:body "world\n  id:: 63e25526-3612-4fb1-8cf9-abcd12354abc"
+ [[-1 {:body "world\nid:: 63e25526-3612-4fb1-8cf9-abcd12354abc"
       :level 2
       :uuid "63e25526-3612-4fb1-8cf9-abcd12354abc"}]]
  [[0 {:body "nice"
@@ -217,7 +308,7 @@
 
       "bar"
       [{:body "ghi\nid:: 11451411-1111-1111-1111-111111111111" :uuid  "11451411-1111-1111-1111-111111111111" :level 1}
-       {:body "jkl\nid:: 63241234-1234-1234-1234-123412341234" :uuid  "63241234-1234-1234-1234-123412341234" :level 2}]) 
+       {:body "jkl\nid:: 63241234-1234-1234-1234-123412341234" :uuid  "63241234-1234-1234-1234-123412341234" :level 2}])
 
     (are [page-name text new-uuids] (= (let [old-blks (test-db->diff-blocks conn page-name)
                                              new-blks (text->diffblocks text)
@@ -254,7 +345,7 @@
     [[["Properties" [["TiTlE" "Howdy" []]]] nil]]
     "#+title: Howdy"
     [{:body "#+title: Howdy", :level 1, :uuid nil}])
-  
+
   (are [ast text diff-blocks]
        (= (fs-diff/ast->diff-blocks ast text :org {:block-pattern "-" :user-config {:property-pages/enabled? true}})
           diff-blocks)
@@ -332,7 +423,7 @@
       foo-new-content
       "foo-persist"
       (fn [db-uuids] (conj db-uuids nil))
-      
+
       ;; Prepend a new line to bar
       (gp-mldoc/->edn new-bar-content (gp-mldoc/default-config :markdown))
       new-bar-content
@@ -358,3 +449,25 @@
       (gp-mldoc/->edn foo-new-content (gp-mldoc/default-config :markdown))
       foo-new-content
       "foo-error-cap")))
+
+(deftest test-remove-indentation-spaces
+  (is (= "" (gp-mldoc/remove-indentation-spaces "" 0 false)))
+  (is (= "" (gp-mldoc/remove-indentation-spaces "" 3 true)))
+
+  (is (= "- nice\n  happy" (gp-mldoc/remove-indentation-spaces "\t\t\t- nice\n\t\t\t  happy" 3 true)))
+  (is (= "\t\t\t- nice\n  happy" (gp-mldoc/remove-indentation-spaces "\t\t\t- nice\n\t\t\t  happy" 3 false)))
+  (is (= "\t\t\t- nice\n\t\t\t  happy" (gp-mldoc/remove-indentation-spaces "\t\t\t- nice\n\t\t\t  happy" 0 true))))
+
+(deftest test-remove-level-spaces
+  ;; Test when `format` is nil
+  (is (= "nice\n\t\t\t  good" (text/remove-level-spaces "\t\t\t- nice\n\t\t\t  good" :markdown "-")))
+  (is (= "- nice" (text/remove-level-spaces "\t\t\t- nice" :markdown "")))
+  (is (= "nice" (text/remove-level-spaces "\t\t\t- nice" :markdown "-"))))
+
+(deftest test-three-way-merge
+  (is (= (fs-diff/three-way-merge
+          "- a\n  id:: 648ab5e6-5e03-4c61-95d4-dd904a0a007f\n- b"
+          "- a\n  id:: 648ab5e6-5e03-4c61-95d4-dd904a0a007f\n  aaa:: 111\n- b"
+          "- c"
+          :markdown)
+         "- a\n  id:: 648ab5e6-5e03-4c61-95d4-dd904a0a007f\n  aaa:: 111\n- c")))

+ 5 - 3
src/test/frontend/fs/sync_test.cljs

@@ -27,17 +27,19 @@
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
 
-    #{}
+    #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
     #{(sync/->FileMetadata 1 22 "3" 4 6 nil nil nil)}
 
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
-    #{(sync/->FileMetadata 1 22 "3" 4 4 nil nil nil) (sync/->FileMetadata 1 22 "3" 44 5 nil nil nil)}
+    #{(sync/->FileMetadata 1 22 "3" 4 4 nil nil nil) 
+      (sync/->FileMetadata 1 22 "3" 44 5 nil nil nil)}
 
     #{}
     #{(sync/->FileMetadata 1 2 "3" 4 5 nil nil nil)}
-    #{(sync/->FileMetadata 1 2 "3" 4 4 nil nil nil) (sync/->FileMetadata 1 2 "3" 4 6 nil nil nil)}
+    #{(sync/->FileMetadata 1 2 "3" 4 4 nil nil nil) 
+      (sync/->FileMetadata 1 2 "3" 4 6 nil nil nil)}
 
     )
   )

+ 40 - 40
static/yarn.lock

@@ -400,47 +400,47 @@
   resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
   integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
 
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-darwin-arm64/-/rsapi-darwin-arm64-0.0.70.tgz#9f0214d0d61081d287dee2d75c33b272270c82f1"
-  integrity sha512-N3wNhAwyoZuXu8O83y69vTzDxolciPpeoMJAmkokXq0+3L8+74lRm9+d/Rn2xzRoXwdPyogvRvG75HpG+vC2Ww==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-darwin-x64/-/rsapi-darwin-x64-0.0.70.tgz#270940af25f168a463d0e4eee68613aabe84a837"
-  integrity sha512-ZsvZymdUDMrRM4QYKG8yOBJn8CDpZhWu6CrwHX8FfXyCJfglZT9V4KPt97ortAyyTlnKpr8/flYTd9mOs+eUcg==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-freebsd-x64/-/rsapi-freebsd-x64-0.0.70.tgz#8785dc753068074d16e7de17115d5c004a1ded4c"
-  integrity sha512-jkUXBlm+Z4VTJUtqenwTV4THFww2lNnHkfXDyFS4Zghnby2Cg/jDe/eUwVvgUIY9fPDTivkwWwx37SLtgMCIsA==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-linux-arm64-gnu/-/rsapi-linux-arm64-gnu-0.0.70.tgz#5a25ef4cdd758a04a625a7d265674681314d5c7c"
-  integrity sha512-OXDwjfpT/oKrLEucnVxeIu1hIdAO4B371uA/33qCH8pNVCPNZffSITGfgPKY0dXJ3zT2i5s5+Z06AJMcsjiReg==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-linux-x64-gnu/-/rsapi-linux-x64-gnu-0.0.70.tgz#e453448bfadfd195df02f0c3b148b815010b61ea"
-  integrity sha512-rvCjREn2uHLESR3+UAgoKfkST7ysge9L4m4pol5eABczPB4BNiEGy0xpB3ML3/M/8t9Iaw9LcLdq2en80FM3fA==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi-win32-x64-msvc/-/rsapi-win32-x64-msvc-0.0.70.tgz#c855a9e0aed512ae8c465c55ec698855f888deed"
-  integrity sha512-wRNBJblaZjX8G1cl7Nvn8b8uMuItqaHkrblyfGzsoEOzALyJIZB95zrJvC2tk8Kc47sriK/KzH16+bW/OBxbOA==
-
-"@logseq/[email protected]0":
-  version "0.0.70"
-  resolved "https://registry.yarnpkg.com/@logseq/rsapi/-/rsapi-0.0.70.tgz#4eca5606318b2ab8c92611dc301cbb722281878c"
-  integrity sha512-w6eSfCLrewZ/8qE+b/GF8YIdvm+4vDurz/haHAR2qmscsXsZr7XoYOcT2eC2FcHyYYforzAppBvxpK9t7FO2ZA==
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-darwin-arm64/-/rsapi-darwin-arm64-0.0.73.tgz#4753f05e2dc70f37a31dfb7440e9b2601acb7696"
+  integrity sha512-TvFwh3/fnwRAZwourk1UUgptcNN7FqmOcL07F373M2rWhTIOba/u1eBwjW9wNMRcXtgLWacaXzBmC4ENvAKDpg==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-darwin-x64/-/rsapi-darwin-x64-0.0.73.tgz#44180532e3ccf62a7fe597e385ee85a700b14bea"
+  integrity sha512-tnOSgQ1qgerwDTnR5v1TMbHnVXVabzZf9i3bxon0y9ItwlIY7j7OW80+1AfNoHu9Fbo/e+3bFg5Muq3/Gm7Yrw==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-freebsd-x64/-/rsapi-freebsd-x64-0.0.73.tgz#3ff4f720eb05d9ffd32a5391ddf33034a05f42e0"
+  integrity sha512-YAjggQHzVCZYhXDybR/37Y5czTQbYDE5A5piwsg09Xt4/m5REFSVKqdeQpnOb2LX7wBGbnPxMhsg6QUV/y/eHA==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-linux-arm64-gnu/-/rsapi-linux-arm64-gnu-0.0.73.tgz#1f423e296ca69e6e92a06afbb25f9f2f0e452b69"
+  integrity sha512-t5W9SBraKYU2iGPzBgUhSzVPtH98RwuE1tLW2MpAJh0QNb2DI4/axbc9nrC1IVc1dqAmTkuL8GGGN80VONisUA==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-linux-x64-gnu/-/rsapi-linux-x64-gnu-0.0.73.tgz#d33e621d7ea24089a9b863809e6dfbe4362443d5"
+  integrity sha512-WidDZv4mnG6Ys90KYLzJl0fwMWj/kEItGHjfR3FoDAVKkvu3mezNDWCsd+I2kMrh1dlIpPqcP+BJ9evI4Gbwkw==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi-win32-x64-msvc/-/rsapi-win32-x64-msvc-0.0.73.tgz#7734187c1b56da6373c947680a65b21551134bec"
+  integrity sha512-YQTE4ZMtlrbuDVfD/6DWtnsiC2uUZpNDqcy6LIN6Ui98yeZ88ktOsEV/lQJF4+4cAflrzW7Xr8ejl2SWSdX6EQ==
+
+"@logseq/[email protected]3":
+  version "0.0.73"
+  resolved "https://registry.yarnpkg.com/@logseq/rsapi/-/rsapi-0.0.73.tgz#b02fc017bb12df3afa0fce60ba13e1b01bbf4342"
+  integrity sha512-ppQCCzc1pAgqlxkYg2CprcU5RTz01fWqba9lhEVg2FpeF9l8IVVCXhbBIUnWPEFTqkoySiwXpMU8kbkTgvEPIg==
   optionalDependencies:
-    "@logseq/rsapi-darwin-arm64" "0.0.70"
-    "@logseq/rsapi-darwin-x64" "0.0.70"
-    "@logseq/rsapi-freebsd-x64" "0.0.70"
-    "@logseq/rsapi-linux-arm64-gnu" "0.0.70"
-    "@logseq/rsapi-linux-x64-gnu" "0.0.70"
-    "@logseq/rsapi-win32-x64-msvc" "0.0.70"
+    "@logseq/rsapi-darwin-arm64" "0.0.73"
+    "@logseq/rsapi-darwin-x64" "0.0.73"
+    "@logseq/rsapi-freebsd-x64" "0.0.73"
+    "@logseq/rsapi-linux-arm64-gnu" "0.0.73"
+    "@logseq/rsapi-linux-x64-gnu" "0.0.73"
+    "@logseq/rsapi-win32-x64-msvc" "0.0.73"
 
 "@malept/cross-spawn-promise@^1.0.0", "@malept/cross-spawn-promise@^1.1.0":
   version "1.1.1"

+ 12 - 12
yarn.lock

@@ -487,15 +487,15 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
-"@logseq/[email protected].24":
-  version "0.0.24"
-  resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.24.tgz#be7b69492b92df9c4e899502c632deebe179746b"
-  integrity sha512-CBIXEPYVp1ddjyYN+Z2dTQ9gwF0KYwZwlvwnl+zmR0Q3ODXxg75BExh5vAU8khXkSNZjZXgZT/J61/kn9xN11w==
+"@logseq/[email protected].30":
+  version "0.0.30"
+  resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.30.tgz#9441ad5689f6139acbc7444530b11e0648a586b3"
+  integrity sha512-rrk4CdSyS8y1M3WgqkFtdtoP3YWRhuaaQOPtO18roOTztbwdu/w7/+uEt7RDVcV92rwCjhCeg4yaTxbmgWwFYw==
 
-"@logseq/diff-merge@^0.0.2":
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/@logseq/diff-merge/-/diff-merge-0.0.2.tgz#0bf29a550921311f63754db65ea83b82c54b16c8"
-  integrity sha512-c6V7rTg/pEoqhByJxRv7yyZ7q3LylKmkiKxoU99R7Fr7Cf5j9dn7GUQW/RlOs68IqYv76BkTHR6EhrLf6cKtZg==
+"@logseq/diff-merge@0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@logseq/diff-merge/-/diff-merge-0.1.0.tgz#fca282e3ff7c256a1f447d0463d78fb23ebee1d9"
+  integrity sha512-VNAJI7Mo/xHEw2LN6rpoWf/BbVfsC1wRpyyLbvm1jQbRxcwRgqYwWkSIVS0t1wswquDS64ZolJkCIFXeNXQbTA==
 
 "@logseq/[email protected]":
   version "1.3.1-1"
@@ -4667,10 +4667,10 @@ mkdirp@^1.0.3:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
-mldoc@^1.5.5:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.5.5.tgz#2f18439740cbcf474bf7d59266d4d98bbbc89412"
-  integrity sha512-hBVLgzlh/648l0b0Y2rkQ9KK65EBf4T+IINNFGERpn2MccXwIAiXTCZXuMIe4xfhWQ2C8KHb+k4Wb2Nw5O1TKQ==
+mldoc@1.5.7:
+  version "1.5.7"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.5.7.tgz#9b316f633a3032ce217a339621b98e70bb229235"
+  integrity sha512-Ph9y8t195UWtkn0hK/VVNvi/wjZUJCLXAaUGVE80KmaE9Rwp9wWBMSEg4wPgjCac3edMjaONucD52fHOFTU9UA==
   dependencies:
     yargs "^12.0.2"