فهرست منبع

Merge branch 'master' into feat/db

Tienson Qin 5 ماه پیش
والد
کامیت
5e1a968530
39فایلهای تغییر یافته به همراه4910 افزوده شده و 1122 حذف شده
  1. 1 1
      .github/workflows/build-android.yml
  2. 1 1
      .github/workflows/build-demo.yml
  3. 8 3
      .github/workflows/build-desktop-release.yml
  4. 1 1
      .github/workflows/build-ios-release.yml
  5. 1 1
      .github/workflows/build-ios.yml
  6. 1 1
      .github/workflows/build-stage.yml
  7. 1 1
      .github/workflows/build.yml
  8. 1 1
      .github/workflows/db.yml
  9. 1 1
      .github/workflows/e2e.yml
  10. 1 1
      .github/workflows/graph-parser.yml
  11. 1 1
      .github/workflows/logseq-common.yml
  12. 1 1
      .github/workflows/publishing.yml
  13. 7 7
      deps/common/src/logseq/common/path.cljs
  14. 0 2
      deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn
  15. 1 1
      package.json
  16. 1 1
      resources/forge.config.js
  17. 0 45
      resources/js/preload.js
  18. 15 15
      resources/package.json
  19. 1 1
      src/main/frontend/components/block.cljs
  20. 1 1
      src/main/frontend/components/imports.cljs
  21. 2 2
      src/main/frontend/extensions/pdf/assets.cljs
  22. 10 12
      src/main/frontend/fs.cljs
  23. 3 15
      src/main/frontend/fs/capacitor_fs.cljs
  24. 72 105
      src/main/frontend/fs/sync.cljs
  25. 1 1
      src/main/frontend/handler/assets.cljs
  26. 12 10
      src/main/frontend/handler/draw.cljs
  27. 16 35
      src/main/frontend/handler/editor.cljs
  28. 3945 0
      src/main/frontend/handler/editor.cljs.~9b1a565dd097a74872fec18d659f52d3296a7680~
  29. 22 48
      src/main/frontend/handler/file_based/editor.cljs
  30. 5 5
      src/main/frontend/handler/file_based/events.cljs
  31. 43 43
      src/main/frontend/handler/file_based/file.cljs
  32. 13 13
      src/main/frontend/handler/global_config.cljs
  33. 28 14
      src/main/frontend/handler/plugin.cljs
  34. 2 2
      src/main/frontend/handler/plugin_config.cljs
  35. 6 10
      src/main/frontend/util/persist_var.cljs
  36. 4 5
      src/main/logseq/api.cljs
  37. 6 6
      src/main/logseq/sdk/git.cljs
  38. 366 328
      static/yarn.lock
  39. 309 381
      yarn.lock

+ 1 - 1
.github/workflows/build-android.yml

@@ -42,7 +42,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '17'
 
 jobs:

+ 1 - 1
.github/workflows/build-demo.yml

@@ -19,7 +19,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '17'
 
 jobs:

+ 8 - 3
.github/workflows/build-desktop-release.yml

@@ -48,7 +48,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '11'
 
 jobs:
@@ -307,6 +307,8 @@ jobs:
       - name: Build/Release Electron app
         run: yarn electron:make
         working-directory: ./static
+        env:
+          DEBUG: electron-packager
         #env:
         #  CODE_SIGN_CERTIFICATE_FILE: ../codesign.pfx
         #  CODE_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGN_CERTIFICATE_PASSWORD }}
@@ -393,7 +395,7 @@ jobs:
       #     key: ${{ runner.os }}-node-modules
 
       - name: Build/Release Electron App for x64
-        run: yarn install && yarn electron:make
+        run: yarn install && yarn rebuild:all && yarn electron:make
         working-directory: ./static
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
@@ -466,7 +468,7 @@ jobs:
       #     key: ${{ runner.os }}-node-modules
 
       - name: Fetch deps and fix dugit arch for arm64
-        run: yarn install --ignore-platform && cd node_modules/dugite && npm_config_arch=arm64 node script/download-git.js
+        run: yarn install --ignore-platform && yarn rebuild:all && cd node_modules/dugite && npm_config_arch=arm64 node script/download-git.js
         working-directory: ./static
 
       - name: Build/Release Electron App for arm64
@@ -507,6 +509,9 @@ jobs:
     needs: [ build-windows ]
     runs-on: [self-hosted, macos, token]
     steps:
+      - name: Remove old builds
+        run: rm -rf ./builds && mkdir ./builds
+
       - name: Download Windows Artifact
         uses: actions/download-artifact@v4
         with:

+ 1 - 1
.github/workflows/build-ios-release.yml

@@ -12,7 +12,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '11'
 
 jobs:

+ 1 - 1
.github/workflows/build-ios.yml

@@ -17,7 +17,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '11'
 
 jobs:

+ 1 - 1
.github/workflows/build-stage.yml

@@ -19,7 +19,7 @@ on:
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   JAVA_VERSION: '17'
 
 jobs:

+ 1 - 1
.github/workflows/build.yml

@@ -14,7 +14,7 @@ env:
   CLOJURE_VERSION: '1.11.1.1413'
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 1 - 1
.github/workflows/db.yml

@@ -23,7 +23,7 @@ env:
   CLOJURE_VERSION: '1.11.1.1413'
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 1 - 1
.github/workflows/e2e.yml

@@ -19,7 +19,7 @@ env:
   CLOJURE_VERSION: '1.11.1.1413'
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 1 - 1
.github/workflows/graph-parser.yml

@@ -28,7 +28,7 @@ env:
   # This is the same as 1.8.
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 1 - 1
.github/workflows/logseq-common.yml

@@ -23,7 +23,7 @@ env:
   CLOJURE_VERSION: '1.11.1.1413'
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 1 - 1
.github/workflows/publishing.yml

@@ -28,7 +28,7 @@ env:
   # This is the same as 1.8.
   JAVA_VERSION: '11'
   # This is the latest node version we can run.
-  NODE_VERSION: '20'
+  NODE_VERSION: '22'
   BABASHKA_VERSION: '1.0.168'
 
 jobs:

+ 7 - 7
deps/common/src/logseq/common/path.cljs

@@ -63,7 +63,6 @@
                 (re-find #"(?i)^(COM[0-9]|CON|LPT[0-9]|NUL|PRN|AUX|com[0-9]|con|lpt[0-9]|nul|prn|aux)\..+" fname)
                 (re-find #"[\u0000-\u001f\u0080-\u009f]" fname)))))
 
-
 (defn- path-join-internal
   "Joins the given path segments into a single path, handling relative paths,
   '..' and '.' normalization."
@@ -153,7 +152,6 @@
         encoded-new-path (apply uri-path-join-internal path segments)]
     (str scheme "//" domain encoded-new-path)))
 
-
 (defn path-join
   "Join path segments, or URL base and path segments"
   [base & segments]
@@ -191,13 +189,11 @@
     :else
     (path-join (str protocol "//") path)))
 
-
 (defn- path-normalize-internal
   "Normalize path using path-join, break into segment and re-join"
   [path]
   (path-join path))
 
-
 (defn url-normalize
   [origin-url]
   (let [^js url (js/URL. origin-url)
@@ -223,7 +219,13 @@
   (if (is-file-url? original-url)
     ;; NOTE: URL type is not consistent across all protocols
     ;; Check file:// and assets://, pathname behavior is different
-    (let [^js url (js/URL. (string/replace original-url "assets://" "file://"))
+    (let [^js url (try
+                    (js/URL. (string/replace (safe-decode-uri-component original-url) "assets://" "file://"))
+                    (catch :default e
+                      (js/console.error "invalid URL:"
+                                        (str "original-url: " original-url
+                                             " url: " (string/replace (safe-decode-uri-component original-url) "assets://" "file://")))
+                      (throw e)))
           path (safe-decode-uri-component (.-pathname url))
           host (.-host url)
           path (if (string/starts-with? path "///")
@@ -276,7 +278,6 @@
           (safe-decode-uri-component (str base-prefix (string/join "/" remain-segs)))
           (str base-prefix (string/join "/" remain-segs)))))))
 
-
 (defn parent
   "Parent, containing directory"
   [path]
@@ -285,7 +286,6 @@
     (path-normalize (str path "/.."))
     nil))
 
-
 (defn resolve-relative-path
   "Assume current-path is a file"
   [current-path rel-path]

+ 0 - 2
deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn

@@ -208,7 +208,6 @@
             [?h :block/marker ?marker]
             [(contains? #{"NOW" "DOING"} ?marker)]
             [?h :block/page ?p]
-            [?p :block/journal? true]
             [?p :block/journal-day ?d]
             [(>= ?d ?start)]
             [(<= ?d ?today)]]
@@ -225,7 +224,6 @@
             [?h :block/marker ?marker]
             [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
             [?h :block/page ?p]
-            [?p :block/journal? true]
             [?p :block/journal-day ?d]
             [(> ?d ?start)]
             [(< ?d ?next)]]

+ 1 - 1
package.json

@@ -131,7 +131,7 @@
         "d3-force": "3.0.0",
         "diff": "5.0.0",
         "dompurify": "2.4.0",
-        "electron": "35.0.1",
+        "electron": "36.2.0",
         "electron-dl": "^4.0.0",
         "emoji-mart": "^5.5.2",
         "fs": "0.0.1-security",

+ 1 - 1
resources/forge.config.js

@@ -5,7 +5,7 @@ module.exports = {
   packagerConfig: {
     name: 'Logseq',
     icon: './icons/logseq_big_sur.icns',
-    buildVersion: 84,
+    buildVersion: "85",
     protocols: [
       {
         "protocol": "logseq",

+ 0 - 45
resources/js/preload.js

@@ -107,51 +107,6 @@ contextBridge.exposeInMainWorld('apis', {
     )
   },
 
-  /**
-   * When from is empty. The resource maybe from
-   * client paste or screenshoot.
-   * @param repoPathRoot
-   * @param to
-   * @param from?
-   * @returns {Promise<void>}
-   */
-  async copyFileToAssets (repoPathRoot, to, from) {
-    if (from && fs.statSync(from).isDirectory()) {
-      throw new Error('not support copy directory')
-    }
-
-    const dest = path.join(repoPathRoot, to)
-    const assetsRoot = path.dirname(dest)
-
-    await fs.promises.mkdir(assetsRoot, { recursive: true })
-
-    from = from || getFilePathFromClipboard()
-
-    if (from) {
-      try {
-        // console.debug('copy file: ', from, dest)
-        await fs.promises.copyFile(from, dest)
-        return path.basename(from)
-      } catch (e) {
-        from = decodeURIComponent(from)
-        await fs.promises.copyFile(from, dest)
-        return path.basename(from)
-      }
-    }
-
-    // support image
-    // console.debug('read image: ', from, dest)
-    const nImg = clipboard.readImage()
-
-    if (nImg && !nImg.isEmpty()) {
-      const rawExt = path.extname(dest)
-      return await fs.promises.writeFile(
-        dest.replace(rawExt, '.png'),
-        nImg.toPNG()
-      )
-    }
-  },
-
   toggleMaxOrMinActiveWindow (isToggleMin = false) {
     ipcRenderer.invoke('toggle-max-or-min-active-win', isToggleMin)
   },

+ 15 - 15
resources/package.json

@@ -14,14 +14,14 @@
     "electron:make-linux-arm64": "electron-forge make --platform=linux --arch=arm64",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
-    "rebuild:all": "electron-rebuild -v 35.0.1 -f",
+    "rebuild:all": "electron-rebuild -v 36.2.0 -f",
     "postinstall": "install-app-deps"
   },
   "config": {
     "forge": "./forge.config.js"
   },
   "dependencies": {
-    "@fastify/cors": "10.0.1",
+    "@fastify/cors": "11.0.1",
     "@logseq/rsapi": "0.0.91",
     "@sentry/electron": "2.5.1",
     "abort-controller": "3.0.0",
@@ -35,7 +35,7 @@
     "electron-squirrel-startup": "1.0.0",
     "electron-window-state": "5.0.3",
     "extract-zip": "2.0.1",
-    "fastify": "5.1.0",
+    "fastify": "5.3.2",
     "fs-extra": "9.1.0",
     "https-proxy-agent": "7.0.2",
     "node-fetch": "2.6.7",
@@ -46,22 +46,22 @@
     "update-electron-app": "2.0.1"
   },
   "devDependencies": {
-    "@electron-forge/cli": "^7.3.1",
-    "@electron-forge/maker-deb": "^7.3.1",
-    "@electron-forge/maker-dmg": "^7.3.1",
-    "@electron-forge/maker-wix": "^7.3.1",
-    "@electron-forge/maker-rpm": "^7.3.1",
-    "@electron-forge/maker-squirrel": "^7.3.1",
-    "@electron-forge/maker-zip": "^7.3.1",
-    "@electron/rebuild": "3.2.10",
-    "electron": "35.0.1",
-    "electron-builder": "25.1.8",
+    "@electron-forge/cli": "^7.8.0",
+    "@electron-forge/maker-deb": "^7.8.0",
+    "@electron-forge/maker-dmg": "^7.8.0",
+    "@electron-forge/maker-wix": "^7.8.0",
+    "@electron-forge/maker-rpm": "^7.8.0",
+    "@electron-forge/maker-squirrel": "^7.8.0",
+    "@electron-forge/maker-zip": "^7.8.0",
+    "@electron/rebuild": "4.0.1",
+    "electron": "36.2.0",
+    "electron-builder": "26.0.12",
     "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git",
     "electron-devtools-installer": "^3.2.0"
   },
   "resolutions": {
-    "**/electron": "35.0.1",
-    "**/node-abi": "3.74.0",
+    "**/electron": "36.2.0",
+    "**/node-abi": "3.75.0",
     "**/node-gyp": "11.1.0",
     "string-width": "4.2.0",
     "wrap-ansi": "^7.0.0",

+ 1 - 1
src/main/frontend/components/block.cljs

@@ -458,7 +458,7 @@
                  (let [repo (state/get-current-repo)
                        file-rpath (string/replace s #"^[.\/\\]*assets[\/\\]+" "assets/")
                        dir (config/get-repo-dir repo)]
-                   (-> (fs/write-file! repo dir file-rpath content nil)
+                   (-> (fs/write-plain-text-file! repo dir file-rpath content nil)
                        (p/then load$)))))
              (js/console.error _e)))))))
 

+ 1 - 1
src/main/frontend/components/imports.cljs

@@ -345,7 +345,7 @@
                       parent-dir (path/path-join repo-dir (path/dirname (:path file)))]
                   (p/do!
                    (fs/mkdir-if-not-exists parent-dir)
-                   (fs/write-file! repo repo-dir (:path file) content {:skip-transact? true})))))))
+                   (fs/write-plain-text-file! repo repo-dir (:path file) content {:skip-transact? true})))))))
 
 (defn- import-file-graph
   [*files

+ 2 - 2
src/main/frontend/extensions/pdf/assets.cljs

@@ -216,7 +216,7 @@
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)
           data     (with-out-str (pprint {:highlights highlights :extra extra}))]
-      (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
+      (fs/write-plain-text-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
 
 (defn file-based-resolve-hls-data-by-key$
   [target-key]
@@ -242,7 +242,7 @@
           new-fpath  (str fdir "/" fname "_" fstamp ".png")
           old-fpath  (and old-fstamp (str fdir "/" fname "_" old-fstamp ".png"))
           _          (and old-fpath (fs/rename! repo-url old-fpath new-fpath))
-          _          (fs/write-file! repo-url repo-dir new-fpath png {:skip-compare? true})]
+          _          (fs/write-plain-text-file! repo-url repo-dir new-fpath png {:skip-compare? true})]
 
     (js/console.timeEnd :write-area-image)))
 

+ 10 - 12
src/main/frontend/fs.cljs

@@ -2,21 +2,21 @@
   "System-component-like ns that provides common file operations for all
   platforms by delegating to implementations of the fs protocol"
   (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
+            [electron.ipc :as ipc]
             [frontend.config :as config]
-            [frontend.fs.nfs :as nfs]
-            [frontend.fs.node :as node]
             [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.fs.memory-fs :as memory-fs]
-            [frontend.mobile.util :as mobile-util]
+            [frontend.fs.nfs :as nfs]
+            [frontend.fs.node :as node]
             [frontend.fs.protocol :as protocol]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
-            [promesa.core :as p]
             [logseq.common.path :as path]
-            [clojure.string :as string]
-            [frontend.state :as state]
             [logseq.common.util :as common-util]
-            [electron.ipc :as ipc]))
+            [promesa.core :as p]))
 
 (defonce nfs-backend (nfs/->Nfs))
 (defonce memory-backend (memory-fs/->MemoryFs))
@@ -99,8 +99,8 @@
     (when (= fs memory-backend)
       (protocol/rmdir! fs dir))))
 
-;; TODO(andelf): distinguish from graph file writing and global file write
-(defn write-file!
+(defn write-plain-text-file!
+  "Use it only for plain-text files, not binary"
   [repo dir rpath content opts]
   (when content
     (let [path (common-util/path-normalize rpath)
@@ -182,8 +182,6 @@
       (p/let [_ (mkdir-if-not-exists new-dir)]
         (protocol/copy! (get-fs old-path) repo old-path new-path)))))
 
-
-
 (defn open-dir
   [dir]
   (let [record (get-native-backend)]
@@ -227,7 +225,7 @@
          true)
        (p/catch
         (fn [_error]
-          (p/let [_ (write-file! repo dir path initial-content nil)]
+          (p/let [_ (write-plain-text-file! repo dir path initial-content nil)]
             false))))))
 
 (defn file-exists?

+ 3 - 15
src/main/frontend/fs/capacitor_fs.cljs

@@ -3,17 +3,17 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
-            [goog.string :as gstring]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.fs.protocol :as protocol]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.util :as util]
+            [goog.string :as gstring]
             [lambdaisland.glogi :as log]
+            [logseq.common.path :as path]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [logseq.common.path :as path]))
+            [rum.core :as rum]))
 
 (when (mobile-util/native-ios?)
   (defn ios-ensure-documents!
@@ -41,18 +41,6 @@
            (fn [_error]
              false)))
 
-(defn <write-file-with-base64
-  "Write a binary file, requires base64 encoding"
-  [path content]
-  (when-not (string/blank? path)
-    (-> (p/chain (.writeFile Filesystem (clj->js {:path path
-                                                  :data content
-                                                  :recursive true}))
-                 #(js->clj % :keywordize-keys true))
-        (p/catch (fn [error]
-                   (js/console.error "writeFile Error: " path ": " error)
-                   nil)))))
-
 (defn- <write-file-with-utf8
   [path content]
   (when-not (string/blank? path)

+ 72 - 105
src/main/frontend/fs/sync.cljs

@@ -164,8 +164,7 @@
                  :remote->local-full-sync-failed
                  :local->remote-full-sync-failed
                  :get-remote-graph-failed
-                 :get-deletion-logs-failed
-                 })
+                 :get-deletion-logs-failed})
 
 (s/def ::sync-event (s/keys :req-un [::event ::data]))
 
@@ -198,7 +197,6 @@
   [x]
   (when (instance? ExceptionInfo x) x))
 
-
 (def ws-addr config/WS-URL)
 
 ;; Warning: make sure to `persist-var/-load` graphs-txid before using it.
@@ -326,9 +324,9 @@
 (defn <request [api-name & args]
   (let [name (str api-name (.now js/Date))]
     (go (swap! *on-flying-request conj name)
-      (let [r (<! (apply <request* api-name args))]
-        (swap! *on-flying-request disj name)
-        r))))
+        (let [r (<! (apply <request* api-name args))]
+          (swap! *on-flying-request disj name)
+          r))))
 
 (defn- remove-dir-prefix [dir path]
   (let [r (string/replace path (js/RegExp. (str "^" (gstring/regExpEscape dir))) "")]
@@ -417,19 +415,19 @@
   (let [update? (= "update_files" TXType)
         delete? (= "delete_files" TXType)
         update-xf
-                (comp
-                 (remove #(or (empty? (first %))
-                              (empty? (last %))))
-                 (map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
+        (comp
+         (remove #(or (empty? (first %))
+                      (empty? (last %))))
+         (map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
         delete-xf
-                (comp
-                 (remove #(empty? (first %)))
-                 (map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
+        (comp
+         (remove #(empty? (first %)))
+         (map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
         rename-xf
-                (comp
-                 (remove #(or (empty? (first %))
-                              (empty? (second %))))
-                 (map #(->FileTxn (second %) (first %) false false TXId nil)))
+        (comp
+         (remove #(or (empty? (first %))
+                      (empty? (second %))))
+         (map #(->FileTxn (second %) (first %) false false TXId nil)))
         xf (case TXType
              "delete_files" delete-xf
              "update_files" update-xf
@@ -560,7 +558,6 @@
    (map-indexed filepath+checksum->diff)
    (diffs->partitioned-filetxns n)))
 
-
 (deftype FileMetadata [size etag path encrypted-path last-modified remote? txid ^:mutable normalized-path]
   Object
   (get-normalized-path [_]
@@ -596,13 +593,10 @@
       :txid txid
       not-found))
 
-
   IPrintWithWriter
   (-pr-writer [_ w _opts]
     (write-all w (str {:size size :etag etag :path path :remote? remote? :txid txid :last-modified last-modified}))))
 
-
-
 (def ^:private higher-priority-remote-files
   "when diff all remote files and local files, following remote files always need to download(when checksum not matched),
   even local-file's last-modified > remote-file's last-modified.
@@ -659,21 +653,21 @@
    #{} s1))
 
 (comment
- (defn map->FileMetadata [m]
-   (apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
-
- (assert
-  (=
-   #{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
-   (diff-file-metadata-sets
-    (into #{}
-          (map map->FileMetadata)
-          [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
-           {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
-    (into #{}
-          (map map->FileMetadata)
-          [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
-           {:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
+  (defn map->FileMetadata [m]
+    (apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
+
+  (assert
+   (=
+    #{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
+    (diff-file-metadata-sets
+     (into #{}
+           (map map->FileMetadata)
+           [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
+            {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
+     (into #{}
+           (map map->FileMetadata)
+           [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
+            {:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
 
 (extend-protocol IChecksum
   FileMetadata
@@ -726,7 +720,6 @@
 
   common-util/path-normalize)
 
-
 ;;; ### APIs
 ;; `RSAPI` call apis through rsapi package, supports operations on files
 
@@ -765,16 +758,13 @@
   (<get-graph-encrypt-keys [this graph-uuid])
   (<upload-graph-encrypt-keys [this graph-uuid public-key encrypted-private-key]))
 
-
 (defprotocol IRemoteControlAPI
   "api functions provided for outside the sync process"
-  (<delete-remote-files-control [this graph-uuid filepaths])
-  )
+  (<delete-remote-files-control [this graph-uuid filepaths]))
 
 (defprotocol IToken
   (<get-token [this]))
 
-
 (defn <case-different-local-file-exist?
   "e.g. filepath=\"pages/Foo.md\"
   found-filepath=\"pages/foo.md\"
@@ -1128,8 +1118,8 @@
      (go-loop []
        (let [{:keys [val stop]}
              (async/alt!
-              debug-print-sync-events-loop-stop-chan {:stop true}
-              out-ch ([v] {:val v}))]
+               debug-print-sync-events-loop-stop-chan {:stop true}
+               out-ch ([v] {:val v}))]
          (cond
            stop (do (async/unmix-all out-mix)
                     (doseq [[topic ch] topic&chs]
@@ -1138,12 +1128,10 @@
            val (do (pp/pprint [:debug :sync-event val])
                    (recur))))))))
 
-
 (defn stop-debug-print-sync-events-loop
   []
   (offer! debug-print-sync-events-loop-stop-chan true))
 
-
 ;;; sync events ends
 
 (defn- fire-file-sync-storage-exceed-limit-event!
@@ -1232,7 +1220,6 @@
                 (recur result-file-meta-list others))))
         (vals (persistent! result-file-meta-list))))))
 
-
 (extend-type RemoteAPI
   IRemoteAPI
   (<user-info [this]
@@ -1438,13 +1425,10 @@
 
 (comment
   (declare remoteapi)
-  (<delete-remote-files-control remoteapi (second @graphs-txid) ["pages/aa.md"])
-
-  )
+  (<delete-remote-files-control remoteapi (second @graphs-txid) ["pages/aa.md"]))
 
 (def remoteapi (->RemoteAPI nil))
 
-
 (def ^:private *get-graph-salt-memoize-cache (atom {}))
 (defn update-graph-salt-cache [graph-uuid v]
   {:pre [(map? v)
@@ -1521,9 +1505,6 @@
           val val
           timeout' (recur))))))
 
-
-
-
 (defn- assert-local-txid<=remote-txid
   []
   (when-let [local-txid (last @graphs-txid)]
@@ -1548,7 +1529,6 @@
          sync-state--remove-recent-remote->local-files
          sync-state--stopped?)
 
-
 (defn- filetxns=>recent-remote->local-files
   [filetxns]
   (let [{:keys [update-filetxns delete-filetxns rename-filetxns]}
@@ -1610,7 +1590,7 @@
                                       (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})
+                                       (fs/write-plain-text-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}))))
@@ -1660,7 +1640,7 @@
                                                     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})
+                                                 (fs/write-plain-text-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})))))))
@@ -1687,7 +1667,7 @@
 
                                           ;; else
                                             (p/do!
-                                             (fs/write-file! repo repo-dir current-change-file merged-content {:skip-compare? true})
+                                             (fs/write-plain-text-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})))))))))))))))))
@@ -1724,8 +1704,8 @@
                      [recent-remote->local-file-item])
               (<! (<delete-local-files rsapi graph-uuid base-path [relative-p*]))
               (go (<! (timeout 5000))
-                (swap! *sync-state sync-state--remove-recent-remote->local-files
-                       [recent-remote->local-file-item])))))
+                  (swap! *sync-state sync-state--remove-recent-remote->local-files
+                         [recent-remote->local-file-item])))))
 
         (let [update-local-files-ch (if (state/enable-sync-diff-merge?)
                                       (<fetch-remote-and-update-local-files graph-uuid base-path (map relative-path filetxns))
@@ -1781,8 +1761,8 @@
                                                      (not (instance? ExceptionInfo r)))]
           ;; remove these recent-remote->local-file-items 5s later
           (go (<! (timeout 5000))
-            (swap! *sync-state sync-state--remove-recent-remote->local-files
-                   recent-remote->local-file-items))
+              (swap! *sync-state sync-state--remove-recent-remote->local-files
+                     recent-remote->local-file-items))
           (cond
             (instance? ExceptionInfo r) r
             @*paused                    {:pause true}
@@ -1841,7 +1821,6 @@
   [r]
   (some->> (ex-cause r) str (re-find #"Request is not yet valid")))
 
-
 ;; type = "change" | "add" | "unlink"
 (deftype FileChangeEvent [type dir path stat checksum]
   IRelativePath
@@ -1876,7 +1855,6 @@
   (-pr-writer [_ w _opts]
     (write-all w (str {:type type :base-path dir :path path :size (:size stat) :checksum checksum}))))
 
-
 (defn- <file-change-event=>recent-remote->local-file-item
   "return nil when related local files not found"
   [graph-uuid ^FileChangeEvent e]
@@ -1949,8 +1927,8 @@
     (go-loop []
       (let [{:keys [rename-event local-change]}
             (async/alt!
-             rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
-             local-changes-chan' ([v] {:local-change v}))]
+              rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
+              local-changes-chan' ([v] {:local-change v}))]
         (cond
           rename-event
           (let [repo-dir (config/get-repo-dir (:repo rename-event))
@@ -1963,13 +1941,13 @@
             (swap! *rename-events conj k1 k2)
             ;; remove rename-events after 2s
             (go (<! (timeout 3000))
-              (swap! *rename-events disj k1 k2))
+                (swap! *rename-events disj k1 k2))
             ;; add 2 simulated file-watcher events
             (>! ch (->FileChangeEvent "unlink" repo-dir (:old-path rename-event*) nil nil))
             (>! ch (->FileChangeEvent "add" repo-dir (:new-path rename-event*)
                                       {:mtime (tc/to-long (t/now))
                                        :size 1 ; add a fake size
-                                       } "fake-checksum"))
+                                       }"fake-checksum"))
             (recur))
           local-change
           (cond
@@ -2046,7 +2024,6 @@
     (js/localStorage.removeItem k))
   (reset! pwd-map {}))
 
-
 (defn encrypt+persist-pwd!
   "- persist encrypted pwd at local-storage"
   [pwd graph-uuid]
@@ -2094,7 +2071,6 @@
       (when (and private-key (string/starts-with? private-key "AGE-SECRET-KEY"))
         (set-keys&notify graph-uuid public-key private-key)))))
 
-
 (def <restored-pwd (chan (async/sliding-buffer 1)))
 (def <restored-pwd-pub (async/pub <restored-pwd :graph-uuid))
 
@@ -2124,7 +2100,6 @@
   (swap! pwd-map dissoc graph-uuid)
   (remove-pwd! graph-uuid))
 
-
 (defn- <loop-ensure-pwd&keys
   [graph-uuid repo *stopped?]
   (let [<restored-pwd-sub-chan (chan 1)]
@@ -2186,7 +2161,6 @@
                       (clear-pwd! graph-uuid)
                       (recur))))))))))
 
-
 (defn- <set-env&keys
   [prod? graph-uuid]
   (let [{:keys [private-key public-key]} (get @pwd-map graph-uuid)]
@@ -2241,7 +2215,6 @@
            (fn [_ _ _ _]
              (offer! recent-edited-chan true)))
 
-
 ;;; ### sync state
 
 (def *resume-state
@@ -2385,7 +2358,6 @@
   (contains? #{::idle ::local->remote ::remote->local ::local->remote-full-sync ::remote->local-full-sync}
              (:state sync-state)))
 
-
 ;;; ### remote->local syncer & local->remote syncer
 
 (defprotocol IRemote->LocalSync
@@ -2641,11 +2613,11 @@
          local-files-meta-map))))
 
 (defrecord ^:large-vars/cleanup-todo
-  Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi'
-                       ^:mutable rate *txid *txid-for-get-deletion-log
-                       ^:mutable remote->local-syncer stop-chan *stopped *paused
+ Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi'
+                      ^:mutable rate *txid *txid-for-get-deletion-log
+                      ^:mutable remote->local-syncer stop-chan *stopped *paused
                        ;; control chans
-                       private-immediately-local->remote-chan private-recent-edited-chan]
+                      private-immediately-local->remote-chan private-recent-edited-chan]
   Object
   (filter-file-change-events-fn [_]
     (fn [^FileChangeEvent e]
@@ -2812,21 +2784,21 @@
                                             <!
                                             (sort-by (sort-file-metadata-fn :recent-days-range recent-10-days-range) >))
                 change-events
-                                       (sequence
-                                        (comp
+                (sequence
+                 (comp
                                          ;; convert to FileChangeEvent
-                                         (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
-                                                                  {:size (:size %)} (:etag %)))
-                                         (remove ignored?))
-                                        diff-local-files)
+                  (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
+                                           {:size (:size %)} (:etag %)))
+                  (remove ignored?))
+                 diff-local-files)
                 distinct-change-events (-> (distinct-file-change-events change-events)
                                            filter-upload-files-with-reserved-chars)
                 _                      (swap! *sync-state #(sync-state-reset-full-local->remote-files % distinct-change-events))
                 change-events-partitions
-                                       (sequence
+                (sequence
                                         ;; partition FileChangeEvents
-                                        (partition-file-change-events upload-batch-size)
-                                        distinct-change-events)]
+                 (partition-file-change-events upload-batch-size)
+                 distinct-change-events)]
             (println "[full-sync(local->remote)]"
                      (count (flatten change-events-partitions)) "files need to sync and"
                      (count delete-local-files) "local files need to delete")
@@ -2847,8 +2819,8 @@
                              [fake-recent-remote->local-file-item])
                       (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
                       (go (<! (timeout 5000))
-                        (swap! *sync-state sync-state--remove-recent-remote->local-files
-                               [fake-recent-remote->local-file-item])))))
+                          (swap! *sync-state sync-state--remove-recent-remote->local-files
+                                 [fake-recent-remote->local-file-item])))))
                 (recur fs)))
 
             ;; 2. upload local files
@@ -2866,13 +2838,12 @@
                               (or need-sync-remote graph-has-been-deleted unknown stop) r)))))]
               ;; update *txid-for-get-deletion-log
               (reset! *txid-for-get-deletion-log @*txid)
-              r
-              )))))))
+              r)))))))
 
 ;;; ### put all stuff together
 
 (defrecord ^:large-vars/cleanup-todo
-    SyncManager [user-uuid graph-uuid base-path *sync-state
+ 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
@@ -3276,7 +3247,6 @@
     (reset! current-sm-graph-uuid graph-uuid)
     (sync-manager user-uuid graph-uuid base-path repo txid *sync-state)))
 
-
 (defn <sync-stop []
   (go
     (when-let [sm ^SyncManager (state/get-file-sync-manager (state/get-current-file-sync-graph-uuid))]
@@ -3319,15 +3289,15 @@
   (go
     (let [r (<! (<list-remote-graphs remoteapi))
           result
-            (or
+          (or
              ;; if api call failed, assume this remote graph still exists
-             (instance? ExceptionInfo r)
-             (and
-              (contains? r :Graphs)
-              (->> (:Graphs r)
-                   (mapv :GraphUUID)
-                   set
-                   (#(contains? % local-graph-uuid)))))]
+           (instance? ExceptionInfo r)
+           (and
+            (contains? r :Graphs)
+            (->> (:Graphs r)
+                 (mapv :GraphUUID)
+                 set
+                 (#(contains? % local-graph-uuid)))))]
 
       (when-not result
         (notification/show! (t :file-sync/graph-deleted) :warning false))
@@ -3347,7 +3317,6 @@
   (when-let [graph-uuid (second @graphs-txid)]
     (get-pwd graph-uuid)))
 
-
 (defn- <connectivity-testing
   []
   (go
@@ -3517,7 +3486,6 @@
     (<sync-start))
   (recur))
 
-
 ;;; ### some sync events handler
 
 ;; re-exec remote->local-full-sync when it failed before
@@ -3543,6 +3511,5 @@
 
 ;;; add-tap
 (comment
- (def *x (atom nil))
- (add-tap (fn [v] (reset! *x v)))
- )
+  (def *x (atom nil))
+  (add-tap (fn [v] (reset! *x v))))

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

@@ -266,7 +266,7 @@
         repo-dir (config/get-repo-dir repo)
         file-path (path/path-join common-config/local-assets-dir
                                   (str asset-block-id-str "." asset-type))]
-    (fs/write-file! repo repo-dir file-path data {})))
+    (fs/write-plain-text-file! repo repo-dir file-path data {})))
 
 (defn <unlink-asset
   [repo asset-block-id asset-type]

+ 12 - 10
src/main/frontend/handler/draw.cljs

@@ -25,16 +25,18 @@
   (let [path file
         repo (state/get-current-repo)]
     (when repo
-      (->
-       (p/do!
-        (create-draws-directory! repo)
-        (db/transact! repo
-                      [{:file/path path
-                        :block/name (util/page-name-sanity-lc file)
-                        :block/file {:file/path path}}]))
-       (p/catch (fn [error]
-                  (prn "Write file failed, path: " path ", data: " data)
-                  (js/console.dir error)))))))
+      (let [repo-dir (config/get-repo-dir repo)]
+        (->
+         (p/do!
+          (create-draws-directory! repo)
+          (fs/write-plain-text-file! repo repo-dir path data nil)
+          (db/transact! repo
+                        [{:file/path path
+                          :block/name (util/page-name-sanity-lc file)
+                          :block/file {:file/path path}}]))
+         (p/catch (fn [error]
+                    (prn "Write file failed, path: " path ", data: " data)
+                    (js/console.dir error))))))))
 
 (defn load-excalidraw-file
   [file ok-handler]

+ 16 - 35
src/main/frontend/handler/editor.cljs

@@ -3,6 +3,7 @@
             [clojure.string :as string]
             [clojure.walk :as w]
             [dommy.core :as dom]
+            [electron.ipc :as ipc]
             [frontend.commands :as commands]
             [frontend.config :as config]
             [frontend.date :as date]
@@ -16,7 +17,6 @@
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.fs :as fs]
-            [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
@@ -46,7 +46,6 @@
             [frontend.util.list :as list]
             [frontend.util.text :as text-util]
             [frontend.util.thingatpt :as thingatpt]
-            [goog.crypt.base64 :as base64]
             [goog.dom :as gdom]
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
@@ -1054,6 +1053,8 @@
   (state/set-block-op-type! :cut)
   (when-let [blocks (->> (get-selected-blocks)
                          (remove #(dom/has-class? % "property-value-container"))
+                         (remove (fn [block] (or (= "true" (dom/attr block "data-query"))
+                                                 (= "true" (dom/attr block "data-transclude")))))
                          seq)]
     ;; remove queries
     (let [dom-blocks (remove (fn [block] (= "true" (dom/attr block "data-query"))) blocks)]
@@ -1382,8 +1383,6 @@
              (property-file/remove-properties-when-file-based repo format)
              string/trim)))
 
-(def insert-command! editor-common-handler/insert-command!)
-
 (defn delete-asset-of-block!
   [{:keys [repo asset-block href full-text block-id local? delete-local?] :as _opts}]
   (let [block (db-model/query-block-by-uuid block-id)
@@ -1407,36 +1406,16 @@
                                (path/resolve-relative-path block-file-rpath href)))]
             (fs/unlink! repo asset-fpath nil)))))))
 
-(defn- write-file!
-  [repo dir file file-rpath file-name]
-  (if (util/electron?)
-    (if-let [from (not-empty (.-path file))]
-      (-> (js/window.apis.copyFileToAssets dir file-rpath from)
-          (p/catch #(js/console.error "Debug: Copy Asset Error#" %)))
-      (-> (p/let [buffer (.arrayBuffer file)]
-            (fs/write-file! repo dir file-rpath buffer {:skip-compare? false}))
-          (p/catch #(js/console.error "Debug: Writing Asset #" %))))
-    (->
-     (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
-            (cond
-              (mobile-util/native-platform?)
-                          ;; capacitor fs accepts Blob, File implements Blob
-              (p/let [buffer (.arrayBuffer file)
-                      content (base64/encodeByteArray (js/Uint8Array. buffer))
-                      fpath (path/path-join dir file-rpath)]
-                (capacitor-fs/<write-file-with-base64 fpath content))
-
-              (config/db-based-graph? repo) ;; memory-fs
-              (p/let [buffer (.arrayBuffer file)
-                      content (js/Uint8Array. buffer)]
-                (fs/write-file! repo dir file-rpath content nil))
-
-              :else
-              (throw (ex-info "Paste failed"
-                              {:file-name file-name}))))
-     (p/catch (fn [error]
-                (prn :paste-file-error)
-                (js/console.error error))))))
+(defn db-based-save-asset!
+  [repo dir file file-rpath]
+  (p/let [buffer (.arrayBuffer file)]
+    (if (util/electron?)
+      (ipc/ipc "writeFile" repo (path/path-join dir file-rpath) buffer)
+      ;; web
+      (p/let [buffer (.arrayBuffer file)
+              content (js/Uint8Array. buffer)]
+        ;; actually, writing binary using memory fs
+        (fs/write-plain-text-file! repo dir file-rpath content nil)))))
 
 (defn db-based-save-assets!
   "Save incoming(pasted) assets to assets directory.
@@ -1470,7 +1449,7 @@
                    insert-opts {:custom-uuid block-id
                                 :edit-block? false
                                 :properties properties}
-                   _ (write-file! repo dir file file-rpath file-name)
+                   _ (db-based-save-asset! repo dir file file-rpath)
                    edit-block (state/get-edit-block)
                    insert-to-current-block-page? (and (:block/uuid edit-block) (string/blank? (state/get-edit-content)) (not pdf-area?))
                    insert-opts' (if insert-to-current-block-page?
@@ -1486,6 +1465,8 @@
              (or new-entity
                  (throw (ex-info "Can't save asset" {:files files}))))))))))
 
+(def insert-command! editor-common-handler/insert-command!)
+
 (defn db-upload-assets!
   "Paste asset for db graph and insert link to current editing block"
   [repo id ^js files format uploading? drop-or-paste?]

+ 3945 - 0
src/main/frontend/handler/editor.cljs.~9b1a565dd097a74872fec18d659f52d3296a7680~

@@ -0,0 +1,3945 @@
+(ns ^:no-doc frontend.handler.editor
+  (:require [clojure.set :as set]
+            [clojure.string :as string]
+            [clojure.walk :as w]
+            [dommy.core :as dom]
+            [frontend.commands :as commands]
+            [frontend.config :as config]
+            [frontend.date :as date]
+            [frontend.db :as db]
+            [frontend.db.async :as db-async]
+            [frontend.db.file-based.model :as file-model]
+            [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
+            [frontend.diff :as diff]
+            [frontend.extensions.pdf.utils :as pdf-utils]
+            [frontend.format.block :as block]
+            [frontend.format.mldoc :as mldoc]
+            [frontend.fs :as fs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
+            [frontend.handler.assets :as assets-handler]
+            [frontend.handler.block :as block-handler]
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.common.editor :as editor-common-handler]
+            [frontend.handler.db-based.editor :as db-editor-handler]
+            [frontend.handler.export.html :as export-html]
+            [frontend.handler.export.text :as export-text]
+            [frontend.handler.file-based.editor :as file-editor-handler]
+            [frontend.handler.file-based.status :as status]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.property :as property-handler]
+            [frontend.handler.property.file :as property-file]
+            [frontend.handler.property.util :as pu]
+            [frontend.handler.route :as route-handler]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.modules.outliner.op :as outliner-op]
+            [frontend.modules.outliner.tree :as tree]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]
+            [frontend.ref :as ref]
+            [frontend.search :as search]
+            [frontend.state :as state]
+            [frontend.template :as template]
+            [frontend.util :as util]
+            [frontend.util.cursor :as cursor]
+            [frontend.util.file-based.drawer :as drawer]
+            [frontend.util.keycode :as keycode]
+            [frontend.util.list :as list]
+            [frontend.util.text :as text-util]
+            [frontend.util.thingatpt :as thingatpt]
+            [goog.crypt.base64 :as base64]
+            [goog.dom :as gdom]
+            [goog.dom.classes :as gdom-classes]
+            [goog.object :as gobj]
+            [goog.string :as gstring]
+            [lambdaisland.glogi :as log]
+            [logseq.common.path :as path]
+            [logseq.common.util :as common-util]
+            [logseq.common.util.block-ref :as block-ref]
+            [logseq.common.util.page-ref :as page-ref]
+            [logseq.db :as ldb]
+            [logseq.db.common.entity-plus :as entity-plus]
+            [logseq.db.file-based.schema :as file-schema]
+            [logseq.db.frontend.property :as db-property]
+            [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.property :as gp-property]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.utf8 :as utf8]
+            [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.property :as outliner-property]
+            [logseq.shui.popup.core :as shui-popup]
+            [promesa.core :as p]
+            [rum.core :as rum]))
+
+;; FIXME: should support multiple images concurrently uploading
+
+(defonce *asset-uploading? (atom false))
+(defonce *asset-uploading-process (atom 0))
+
+(def clear-selection! state/clear-selection!)
+(def edit-block! block-handler/edit-block!)
+
+(defn- outliner-save-block!
+  [block & {:as opts}]
+  (outliner-op/save-block! block opts))
+
+(defn get-block-own-order-list-type
+  [block]
+  (pu/lookup block :logseq.property/order-list-type))
+
+(defn set-block-own-order-list-type!
+  [block type]
+  (when-let [uuid (:block/uuid block)]
+    (property-handler/set-block-property! (state/get-current-repo) uuid (pu/get-pid :logseq.property/order-list-type) (name type))))
+
+(defn remove-block-own-order-list-type!
+  [block]
+  (when-let [uuid (:block/uuid block)]
+    (property-handler/remove-block-property! (state/get-current-repo) uuid (pu/get-pid :logseq.property/order-list-type))))
+
+(defn own-order-number-list?
+  [block]
+  (when-let [block (db/entity (:db/id block))]
+    (= (get-block-own-order-list-type block) "number")))
+
+(defn make-block-as-own-order-list!
+  [block]
+  (some-> block (set-block-own-order-list-type! "number")))
+
+(defn toggle-blocks-as-own-order-list!
+  [blocks]
+  (when (seq blocks)
+    (let [has-ordered?    (some own-order-number-list? blocks)
+          blocks-uuids    (some->> blocks (map :block/uuid) (remove nil?))
+          order-list-prop (pu/get-pid :logseq.property/order-list-type)
+          repo (state/get-current-repo)]
+      (if has-ordered?
+        (property-handler/batch-remove-block-property! repo blocks-uuids order-list-prop)
+        (property-handler/batch-set-block-property! repo blocks-uuids order-list-prop "number")))))
+
+(defn get-selection-and-format
+  []
+  (when-let [block (state/get-edit-block)]
+    (when (:block/uuid block)
+      (when-let [edit-id (state/get-edit-input-id)]
+        (when-let [input (gdom/getElement edit-id)]
+          (let [selection-start (util/get-selection-start input)
+                selection-end (util/get-selection-end input)
+                value (gobj/get input "value")
+                selection (when (not= selection-start selection-end)
+                            (subs value selection-start selection-end))
+                selection-start (+ selection-start
+                                   (count (take-while #(= " " %) selection)))
+                selection-end (- selection-end
+                                 (count (take-while #(= " " %) (reverse selection))))]
+            {:selection-start selection-start
+             :selection-end selection-end
+             :selection (some-> selection
+                                string/trim)
+             :format (get block :block/format :markdown)
+             :value value
+             :block block
+             :edit-id edit-id
+             :input input}))))))
+
+(defn- format-text!
+  [pattern-fn]
+  (when-let [m (get-selection-and-format)]
+    (let [{:keys [selection-start selection-end format selection value edit-id input]} m
+          pattern (pattern-fn format)
+          pattern-count (count pattern)
+          pattern-prefix (subs value (max 0 (- selection-start pattern-count)) selection-start)
+          pattern-suffix (subs value selection-end (min (count value) (+ selection-end pattern-count)))
+          already-wrapped? (= pattern pattern-prefix pattern-suffix)
+          prefix (if already-wrapped?
+                   (subs value 0 (- selection-start pattern-count))
+                   (subs value 0 selection-start))
+          postfix (if already-wrapped?
+                    (subs value (+ selection-end pattern-count))
+                    (subs value selection-end))
+          inner-value (cond-> selection
+                        (not already-wrapped?)
+                        (#(str pattern % pattern)))
+          new-value (str prefix inner-value postfix)]
+      (state/set-edit-content! edit-id new-value)
+      (cond
+        already-wrapped? (cursor/set-selection-to input (- selection-start pattern-count) (- selection-end pattern-count))
+        selection (cursor/move-cursor-to input (+ selection-end pattern-count))
+        :else (cursor/set-selection-to input (+ selection-start pattern-count) (+ selection-end pattern-count))))))
+
+(defn bold-format! []
+  (format-text! config/get-bold))
+
+(defn italics-format! []
+  (format-text! config/get-italic))
+
+(defn highlight-format! []
+  (when-let [block (state/get-edit-block)]
+    (let [format (get block :block/format :markdown)]
+      (format-text! #(config/get-highlight format)))))
+
+(defn strike-through-format! []
+  (format-text! config/get-strike-through))
+
+(defn html-link-format!
+  ([]
+   (html-link-format! nil))
+  ([text]
+   (when-let [m (get-selection-and-format)]
+     (let [{:keys [selection-start selection-end format selection value edit-id input]} m
+           empty-selection? (= selection-start selection-end)
+           selection-link? (and selection (gp-mldoc/mldoc-link? format selection))
+           [content forward-pos] (cond
+                                   empty-selection?
+                                   (config/get-empty-link-and-forward-pos format)
+
+                                   (and text selection-link?)
+                                   (config/with-label-link format text selection)
+
+                                   text
+                                   (config/with-label-link format selection text)
+
+                                   selection-link?
+                                   (config/with-default-link format selection)
+
+                                   :else
+                                   (config/with-default-label format selection))
+           new-value (str
+                      (subs value 0 selection-start)
+                      content
+                      (subs value selection-end))
+           cur-pos (or selection-start (cursor/pos input))]
+       (state/set-edit-content! edit-id new-value)
+       (cursor/move-cursor-to input (+ cur-pos forward-pos))))))
+
+(defn open-block-in-sidebar!
+  [block-id]
+  ; (assert (uuid? block-id) "frontend.handler.editor/open-block-in-sidebar! expects block-id to be of type uuid")
+  (when block-id
+    (when-let [block (db/entity (if (number? block-id) block-id [:block/uuid block-id]))]
+      (let [page? (nil? (:block/page block))]
+        (state/sidebar-add-block!
+         (state/get-current-repo)
+         (:db/id block)
+         (if page? :page :block))))))
+
+(defn reset-cursor-range!
+  [node]
+  (when node
+    (state/set-cursor-range! (util/caret-range node))))
+
+(defn restore-cursor-pos!
+  [id markup]
+  (when-let [node (gdom/getElement (str id))]
+    (let [cursor-range (state/get-cursor-range)
+          pos (or (state/get-editor-last-pos)
+                  (and cursor-range
+                       (diff/find-position markup cursor-range)))]
+      (cursor/move-cursor-to node pos))))
+
+(defn highlight-block!
+  [block-uuid]
+  (let [blocks (util/get-blocks-by-id block-uuid)]
+    (doseq [block blocks]
+      (dom/add-class! block "block-highlight"))))
+
+(defn unhighlight-blocks!
+  []
+  (let [blocks (some->> (array-seq (js/document.getElementsByClassName "block-highlight"))
+                        (repeat 2)
+                        (apply concat))]
+    (doseq [block blocks]
+      (gdom-classes/remove block "block-highlight"))))
+
+(defn wrap-parse-block
+  [block]
+  (if (config/db-based-graph? (state/get-current-repo))
+    (db-editor-handler/wrap-parse-block block)
+    (file-editor-handler/wrap-parse-block block)))
+
+(defn- save-block-inner!
+  [block value opts]
+  (let [block {:db/id (:db/id block)
+               :block/uuid (:block/uuid block)
+               :block/title value}
+        block' (-> (wrap-parse-block block)
+                   ;; :block/uuid might be changed when backspace/delete
+                   ;; a block that has been refed
+                   (assoc :block/uuid (:block/uuid block)))
+        opts' (assoc opts :outliner-op :save-block)]
+    (ui-outliner-tx/transact!
+     opts'
+     (outliner-save-block! block'))))
+
+;; id: block dom id, "ls-block-counter-uuid"
+(defn- another-block-with-same-id-exists?
+  [current-id block-id]
+  (when-let [id (and (string? block-id) (parse-uuid block-id))]
+    (and (not= current-id id)
+         (db/entity [:block/uuid id]))))
+
+(defn save-block-if-changed!
+  ([block value]
+   (save-block-if-changed! block value nil))
+  ([block value
+    {:keys [force?]
+     :as opts}]
+   (let [{:block/keys [uuid format repo title]} block
+         format (or format :markdown)
+         repo (or repo (state/get-current-repo))
+         format (or format (state/get-preferred-format))
+         block-id (let [properties (:block/properties block)]
+                    (when (and (not (config/db-based-graph? repo)) (map? properties))
+                      (get properties :id)))
+         content (if (config/db-based-graph? repo)
+                   (:block/title (db/entity (:db/id block)))
+                   (-> (property-file/remove-built-in-properties-when-file-based repo format title)
+                       (drawer/remove-logbook)))]
+     (cond
+       (another-block-with-same-id-exists? uuid block-id)
+       (notification/show!
+        [:p.content
+         (util/format "Block with the id %s already exists!" block-id)]
+        :error)
+
+       force?
+       (save-block-inner! block value opts)
+
+       :else
+       (when content
+         (let [content-changed? (not= (string/trim content) (string/trim value))]
+           (when content-changed?
+             (save-block-inner! block value opts))))))))
+
+(defn- compute-fst-snd-block-text
+  [value selection-start selection-end]
+  (when (string? value)
+    (let [fst-block-text (subs value 0 selection-start)
+          snd-block-text (string/triml (subs value selection-end))]
+      [fst-block-text snd-block-text])))
+
+(declare save-current-block!)
+(defn outliner-insert-block!
+  [config current-block new-block {:keys [sibling? keep-uuid? ordered-list?
+                                          replace-empty-target?]}]
+  (let [ref-query-top-block? (and (or (:ref? config)
+                                      (:custom-query? config))
+                                  (not (:ref-query-child? config)))
+        has-children? (db/has-children? (:block/uuid current-block))
+        sibling? (cond
+                   ref-query-top-block?
+                   false
+
+                   (boolean? sibling?)
+                   sibling?
+
+                   (util/collapsed? current-block)
+                   true
+
+                   :else
+                   (not has-children?))]
+    (ui-outliner-tx/transact!
+     {:outliner-op :insert-blocks}
+     (save-current-block! {:current-block current-block})
+     (outliner-op/insert-blocks! [new-block] current-block {:sibling? sibling?
+                                                            :keep-uuid? keep-uuid?
+                                                            :ordered-list? ordered-list?
+                                                            :replace-empty-target? replace-empty-target?}))))
+
+(defn- block-self-alone-when-insert?
+  [config uuid]
+  (let [current-page (state/get-current-page)
+        block-id (or (some-> (:id config) parse-uuid)
+                     (some-> current-page parse-uuid))]
+    (= uuid block-id)))
+
+(defn insert-new-block-before-block-aux!
+  [config block value]
+  (let [edit-input-id (state/get-edit-input-id)
+        input (gdom/getElement edit-input-id)
+        input-text-selected? (util/input-text-selected? input)
+        new-m {:block/uuid (db/new-block-id)
+               :block/title ""}
+        prev-block (-> (merge (select-keys block [:block/parent :block/format :block/page])
+                              new-m)
+                       (wrap-parse-block))
+        block' (db/entity (:db/id block))
+        left-or-parent (or (ldb/get-left-sibling block') (:block/parent block'))]
+    (when input-text-selected?
+      (let [selection-start (util/get-selection-start input)
+            selection-end (util/get-selection-end input)
+            [_ new-content] (compute-fst-snd-block-text value selection-start selection-end)]
+        (state/set-edit-content! edit-input-id new-content)))
+    (let [sibling? (not= (:db/id left-or-parent) (:db/id (:block/parent block)))
+          result (outliner-insert-block! config left-or-parent prev-block {:sibling? sibling?
+                                                                           :keep-uuid? true})]
+      [result sibling? prev-block])))
+
+(defn insert-new-block-aux!
+  [config
+   {:block/keys [uuid]
+    :as block}
+   value]
+  (let [block-self? (block-self-alone-when-insert? config uuid)
+        input (gdom/getElement (state/get-edit-input-id))
+        selection-start (util/get-selection-start input)
+        selection-end (util/get-selection-end input)
+        [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
+        current-block (assoc block :block/title fst-block-text)
+        current-block (apply dissoc current-block file-schema/retract-attributes)
+        new-m {:block/uuid (db/new-block-id)
+               :block/title snd-block-text}
+        next-block (-> (merge (select-keys block [:block/parent :block/format :block/page])
+                              new-m)
+                       (wrap-parse-block))
+        sibling? (or (:block/collapsed? (:block/link block)) (when block-self? false))
+        result (outliner-insert-block! config current-block next-block {:sibling? sibling?
+                                                                        :keep-uuid? true})]
+    [result sibling? next-block]))
+
+(defn clear-when-saved!
+  []
+  (commands/restore-state))
+
+(defn get-state
+  []
+  (let [[{:keys [on-hide block block-id block-parent-id format sidebar?]} id config] (state/get-editor-args)
+        node (gdom/getElement id)]
+    (when node
+      (let [value (gobj/get node "value")
+            pos (util/get-selection-start node)]
+        {:config config
+         :on-hide on-hide
+         :sidebar? sidebar?
+         :format format
+         :id id
+         :block (or (db/entity [:block/uuid (:block/uuid block)]) block)
+         :block-id block-id
+         :block-parent-id block-parent-id
+         :node node
+         :value value
+         :pos pos
+         :block-container (util/rec-get-node node "ls-block")}))))
+
+(defn- get-node-container-id
+  [node]
+  (some-> (dom/attr node "containerid")
+          util/safe-parse-int))
+
+(defn- get-node-parent
+  [node]
+  (some-> (gobj/get node "parentNode")
+          (util/rec-get-node "ls-block")))
+
+(defn- get-new-container-id
+  [op data]
+  (let [{:keys [block block-container]} (get-state)]
+    (when block
+      (let [node block-container
+            linked? (some? (dom/attr node "originalblockid"))]
+        (case op
+          :insert
+          (when (and linked? (not (false? (:sibling? data))))
+            (some-> (util/rec-get-node node "blocks-container")
+                    get-node-container-id))
+
+          :indent
+          ;; Get prev sibling's container id
+          (when-let [prev (.-previousSibling node)]
+            (when (dom/attr prev "originalblockid")
+              (get-node-container-id prev)))
+
+          :move-up
+          (let [parent (get-node-parent node)
+                prev (when parent (.-previousSibling parent))]
+            (when (and prev (dom/attr prev "originalblockid"))
+              (get-node-container-id prev)))
+
+          :move-down
+          (let [parent (get-node-parent node)
+                next (when parent (.-nextSibling parent))]
+            (when (and next (dom/attr next "originalblockid"))
+              (get-node-container-id next)))
+
+          :outdent
+          ;; Get embed block's root container id
+          (when-let [parent (some-> (gobj/get node "parentNode")
+                                    (util/rec-get-node "ls-block"))]
+            (when (dom/attr parent "originalblockid")
+              (some-> (util/rec-get-node parent "blocks-container")
+                      get-node-container-id))))))))
+
+(defn insert-new-block!
+  "Won't save previous block content - remember to save!"
+  ([state]
+   (insert-new-block! state nil))
+  ([_state block-value]
+   (->
+    (when (not config/publishing?)
+      (when-let [state (get-state)]
+        (state/set-state! :editor/async-unsaved-chars "")
+        (let [{:keys [block value config]} state
+              value (if (string? block-value) block-value value)
+              block-id (:block/uuid block)
+              block-self? (block-self-alone-when-insert? config block-id)
+              input-id (state/get-edit-input-id)
+              input (gdom/getElement input-id)
+              selection-start (util/get-selection-start input)
+              selection-end (util/get-selection-end input)
+              [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
+              insert-above? (and (string/blank? fst-block-text) (not (string/blank? snd-block-text)))
+              block' (or (db/entity [:block/uuid block-id]) block)
+              original-block (:original-block config)
+              block'' (or
+                       (when original-block
+                         (let [e (db/entity (:db/id block'))]
+                           (if (and (some? (first (:block/_parent e)))
+                                    (not (:block/collapsed? e)))
+                          ;; object has children and not collapsed
+                             block'
+                             original-block)))
+                       block')
+              insert-fn (cond
+                          block-self?
+                          insert-new-block-aux!
+
+                          insert-above?
+                          insert-new-block-before-block-aux!
+
+                          :else
+                          insert-new-block-aux!)
+              [result-promise sibling? next-block] (insert-fn config block'' value)]
+          (p/do!
+           result-promise
+           (clear-when-saved!)
+           (let [next-block' (db/entity [:block/uuid (:block/uuid next-block)])
+                 pos 0
+                 unsaved-chars @(:editor/async-unsaved-chars @state/state)
+                 container-id (get-new-container-id :insert {:sibling? sibling?})]
+             (edit-block! next-block' (+ pos (count unsaved-chars))
+                          {:container-id container-id
+                           :custom-content (str unsaved-chars (:block/title next-block'))}))))))
+    (p/finally (fn []
+                 (state/set-state! :editor/async-unsaved-chars nil))))))
+
+(defn api-insert-new-block!
+  [content {:keys [page block-uuid sibling? before? properties
+                   custom-uuid replace-empty-target? edit-block? ordered-list? other-attrs]
+            :or {sibling? false
+                 before? false
+                 edit-block? true}}]
+  (when (or page block-uuid)
+    (let [repo (state/get-current-repo)
+          db-based? (config/db-based-graph? repo)
+          before? (if page false before?)
+          sibling? (boolean sibling?)
+          sibling? (if before? true (if page false sibling?))
+          block (if page
+                  (db/get-page page)
+                  (db/entity [:block/uuid block-uuid]))]
+      (when block
+        (let [last-block (when (not sibling?)
+                           (let [children (:block/_parent block)
+                                 blocks (db/sort-by-order children)
+                                 last-block-id (:db/id (last blocks))]
+                             (when last-block-id
+                               (db/entity last-block-id))))
+              format (or
+                      (get block :block/format :markdown)
+                      (db/get-page-format (:block/name block))
+                      (state/get-preferred-format))
+              content (if (and (not db-based?) (seq properties))
+                        (property-file/insert-properties-when-file-based repo format content properties)
+                        content)
+              new-block (cond->
+                         (-> (select-keys block [:block/page])
+                             (assoc :block/title content))
+                          (not db-based?)
+                          (assoc :block/format format))
+              new-block (assoc new-block :block/page
+                               (if page
+                                 (:db/id block)
+                                 (:db/id (:block/page new-block))))
+              new-block (-> new-block
+                            (wrap-parse-block)
+                            (assoc :block/uuid (or custom-uuid (db/new-block-id))))
+              new-block (merge new-block other-attrs)
+              [block-m sibling?] (cond
+                                   before?
+                                   (let [left-or-parent (or (ldb/get-left-sibling block)
+                                                            (:block/parent block))
+                                         sibling? (if (= (:db/id (:block/parent block)) (:db/id left-or-parent))
+                                                    false sibling?)]
+                                     [left-or-parent sibling?])
+
+                                   sibling?
+                                   [(db/entity (:db/id block)) sibling?]
+
+                                   last-block
+                                   [last-block true]
+
+                                   block
+                                   [(db/entity (:db/id block)) sibling?]
+
+                                   ;; FIXME: assert
+                                   :else
+                                   nil)]
+          (when block-m
+            (p/do!
+             (ui-outliner-tx/transact!
+              {:outliner-op :insert-blocks}
+              (outliner-insert-block! {} block-m new-block {:sibling? sibling?
+                                                            :keep-uuid? true
+                                                            :ordered-list? ordered-list?
+                                                            :replace-empty-target? replace-empty-target?})
+              (when (and db-based? (seq properties))
+                (property-handler/set-block-properties! repo (:block/uuid new-block) properties)))
+             (when edit-block?
+               (if (and replace-empty-target?
+                        (string/blank? (:block/title last-block)))
+                 (edit-block! last-block :max)
+                 (edit-block! new-block :max)))
+             (when-let [id (:block/uuid new-block)]
+               (db/entity [:block/uuid id])))))))))
+
+(defn insert-first-page-block-if-not-exists!
+  [page-uuid-or-title]
+  (let [page-title (str page-uuid-or-title)]
+    (when-not (string/blank? page-title)
+      (when-let [page (db/get-page page-title)]
+        (let [class-or-property? (or (ldb/class? page) (ldb/property? page))]
+          (when (or class-or-property? (db/page-empty? (state/get-current-repo) (:db/id page)))
+            (let [new-block (cond-> {:block/title ""}
+                              (not (config/db-based-graph? (state/get-current-repo)))
+                              (assoc :block/format (get page :block/format :markdown)))]
+              (ui-outliner-tx/transact!
+               {:outliner-op :insert-blocks}
+               (outliner-op/insert-blocks! [new-block] page {:sibling? false})))))))))
+
+(defn check
+  [{:block/keys [marker title repeated? uuid] :as block}]
+  (let [new-content (string/replace-first title marker "DONE")
+        new-content (if repeated?
+                      (file-editor-handler/update-timestamps-content! block title)
+                      new-content)
+        input-id (state/get-edit-input-id)]
+    (if (and input-id
+             (string/ends-with? input-id (str uuid)))
+      (state/set-edit-content! input-id new-content)
+      (save-block-if-changed! block new-content))))
+
+(defn uncheck
+  [{:block/keys [title uuid] :as block}]
+  (let [marker (if (= :now (state/get-preferred-workflow))
+                 "LATER"
+                 "TODO")
+        new-content (string/replace-first title "DONE" marker)
+        input-id (state/get-edit-input-id)]
+    (if (and input-id
+             (string/ends-with? input-id (str uuid)))
+      (state/set-edit-content! input-id new-content)
+      (save-block-if-changed! block new-content))))
+
+(defn get-selected-blocks
+  []
+  (distinct (seq (state/get-selection-blocks))))
+
+(defn set-marker
+  "The set-marker will set a new marker on the selected block.
+  if the `new-marker` is nil, it will generate it automatically."
+  ([block]
+   (set-marker block nil))
+  ([{:block/keys [marker title format] :as block} new-marker]
+   (let [[new-content _] (status/cycle-marker title marker new-marker format (state/get-preferred-workflow))]
+     (save-block-if-changed! block new-content))))
+
+(defn file-based-cycle-todo!
+  [block]
+  (when (not-empty (:block/title block))
+    (set-marker block)))
+
+(defn db-based-cycle-todo!
+  [block]
+  (let [status-value (if (ldb/class-instance? (db/entity :logseq.class/Task) block)
+                       (:logseq.property/status block)
+                       (get block :logseq.property/status {}))
+        next-status (case (:db/ident status-value)
+                      :logseq.property/status.todo
+                      :logseq.property/status.doing
+                      :logseq.property/status.doing
+                      :logseq.property/status.done
+                      :logseq.property/status.done
+                      nil
+                      :logseq.property/status.todo)
+        repo (state/get-current-repo)]
+    (property-handler/set-block-property! repo (:block/uuid block)
+                                          :logseq.property/status
+                                          (:db/id (db/entity next-status)))))
+
+(defn cycle-todos!
+  []
+  (when-let [blocks (seq (get-selected-blocks))]
+    (let [db-based? (config/db-based-graph? (state/get-current-repo))
+          ids (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
+                                     (uuid id)) blocks))
+                   (remove nil?))]
+      (ui-outliner-tx/transact!
+       {:outliner-op :cycle-todos}
+       (doseq [id ids]
+         (when-let [block (db/entity [:block/uuid id])]
+           (if db-based?
+             (db-based-cycle-todo! block)
+             (file-based-cycle-todo! block))))))))
+
+(defn cycle-todo!
+  []
+  #_:clj-kondo/ignore
+  (when-not (state/get-editor-action)
+    (if-let [blocks (seq (get-selected-blocks))]
+      (cycle-todos!)
+      (when-let [edit-block (state/get-edit-block)]
+        (let [edit-input-id (state/get-edit-input-id)
+              current-input (gdom/getElement edit-input-id)]
+          (if (config/db-based-graph? (state/get-current-repo))
+            (when-let [block (db/entity (:db/id edit-block))]
+              (let [pos (state/get-edit-pos)]
+                (p/do!
+                 (ui-outliner-tx/transact!
+                  {:outliner-op :cycle-todos}
+                  (db-based-cycle-todo! block))
+
+                 ;; FIXME: don't change current editor's position
+                 )))
+            (let [content (state/get-edit-content)
+                  format (or (db/get-page-format (state/get-current-page))
+                             (state/get-preferred-format))
+                  [new-content marker] (status/cycle-marker content nil nil format (state/get-preferred-workflow))
+                  new-pos (commands/compute-pos-delta-when-change-marker
+                           content marker (cursor/pos current-input))]
+              (state/set-edit-content! edit-input-id new-content)
+              (cursor/move-cursor-to current-input new-pos))))))))
+
+(defn set-priority
+  [{:block/keys [priority title] :as block} new-priority]
+  (when-not (config/db-based-graph? (state/get-current-repo))
+    (let [new-content (string/replace-first title
+                                            (util/format "[#%s]" priority)
+                                            (util/format "[#%s]" new-priority))]
+      (save-block-if-changed! block new-content))))
+
+(defn delete-block-aux!
+  [{:block/keys [uuid] :as _block}]
+  (let [block (db/entity [:block/uuid uuid])]
+    (when block
+      (let [blocks (block-handler/get-top-level-blocks [block])]
+        (ui-outliner-tx/transact!
+         {:outliner-op :delete-blocks}
+         (outliner-op/delete-blocks! blocks {}))))))
+
+(defn- move-to-prev-block
+  [repo sibling-block format value]
+  (when (and repo sibling-block)
+    (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
+      (when-let [sibling-entity (db/entity [:block/uuid (uuid sibling-block-id)])]
+        (if (:block/name sibling-entity)
+          {:prev-block sibling-entity
+           :new-value (:block/title sibling-entity)
+           :edit-block-f #(edit-block! sibling-entity :max)}
+          (let [db? (config/db-based-graph? repo)
+                original-content (if (= (:db/id sibling-entity) (:db/id (state/get-edit-block)))
+                                   (state/get-edit-content)
+                                   (:block/title sibling-entity))
+                value' (if db?
+                         original-content
+                         (-> (property-file/remove-built-in-properties-when-file-based repo format original-content)
+                             (drawer/remove-logbook)))
+                value (if db?
+                        value
+                        (->> value
+                             (property-file/remove-properties-when-file-based repo format)
+                             (drawer/remove-logbook)))
+                new-value (str value' value)
+                tail-len (count value)
+                pos (max
+                     (if original-content
+                       (gobj/get (utf8/encode original-content) "length")
+                       0)
+                     0)
+                [edit-target container-id] [(db/entity (:db/id sibling-entity)) (some-> (dom/attr sibling-block "containerid")
+                                                                                        util/safe-parse-int)]]
+            {:prev-block sibling-entity
+             :new-content new-value
+             :pos pos
+             :edit-block-f #(edit-block! edit-target
+                                         pos
+                                         {:custom-content new-value
+                                          :tail-len tail-len
+                                          :container-id container-id})}))))))
+
+(declare save-block!)
+
+(declare expand-block!)
+
+(defn delete-block-inner!
+  [repo {:keys [block-id value format config]}]
+  (when block-id
+    (when-let [block-e (db/entity [:block/uuid block-id])]
+      (let [prev-block (db-model/get-prev (db/get-db) (:db/id block-e))
+            block-parent-id (str "ls-block-" block-id)]
+        (cond
+          (and (nil? prev-block)
+               (nil? (:block/parent block-e)))
+          nil
+
+          :else
+          (let [has-children? (seq (:block/_parent block-e))
+                block (db/entity (:db/id block-e))
+                left (or (ldb/get-left-sibling block) (:block/parent block))
+                left-has-children? (and left
+                                        (when-let [block-id (:block/uuid left)]
+                                          (let [block (db/entity [:block/uuid block-id])]
+                                            (seq (:block/_parent block)))))]
+            (when-not (and has-children? left-has-children?)
+              (when block-parent-id
+                (let [block-parent (gdom/getElement block-parent-id)
+                      sibling-or-parent-block
+                      (if (:embed? config)
+                        (util/get-prev-block-non-collapsed
+                         block-parent
+                         {:container (util/rec-get-blocks-container block-parent)})
+                        (util/get-prev-block-non-collapsed-non-embed block-parent))
+                      {:keys [prev-block new-content edit-block-f]}
+                      (move-to-prev-block repo sibling-or-parent-block format value)
+                      concat-prev-block? (boolean (and prev-block new-content))
+                      transact-opts {:outliner-op :delete-blocks}]
+                  (cond
+                    (and prev-block (:block/name prev-block)
+                         (not= (:db/id prev-block) (:db/id (:block/parent block)))
+                         (db-model/hidden-page? (:block/page block))) ; embed page
+                    nil
+
+                    concat-prev-block?
+                    (let [children (:block/_parent (db/entity (:db/id block)))
+                          db-based? (config/db-based-graph? repo)
+                          prev-block-is-not-parent? (empty? (:block/_parent prev-block))
+                          delete-prev-block? (and db-based?
+                                                  prev-block-is-not-parent?
+                                                  (empty? (:block/tags block))
+                                                  (not (:logseq.property.node/display-type block))
+                                                  (seq (:block/properties block))
+                                                  (empty? (:block/properties prev-block))
+                                                  (not (:logseq.property/created-from-property block)))]
+                      (if delete-prev-block?
+                        (p/do!
+                         (ui-outliner-tx/transact!
+                          transact-opts
+                          (delete-block-aux! prev-block)
+                          (save-block! repo block new-content {}))
+                         (edit-block! (assoc block :block/title new-content) (count (:block/title prev-block))))
+                        (p/do!
+                         (ui-outliner-tx/transact!
+                          transact-opts
+                          (when (seq children)
+                            (outliner-op/move-blocks! children prev-block false))
+                          (delete-block-aux! block)
+                          (save-block! repo prev-block new-content {}))
+                         (when edit-block-f (edit-block-f)))))
+
+                    :else
+                    (p/do!
+                     (delete-block-aux! block)
+                     (when edit-block-f (edit-block-f)))))))))))))
+
+(defn delete-block!
+  [repo]
+  (delete-block-inner! repo (get-state)))
+
+(defn delete-blocks!
+  [repo block-uuids blocks dom-blocks]
+  (when (seq block-uuids)
+    (let [uuid->dom-block (zipmap block-uuids dom-blocks)
+          block (first blocks)
+          block-parent (get uuid->dom-block (:block/uuid block))
+          sibling-block (when block-parent (util/get-prev-block-non-collapsed-non-embed block-parent))
+          blocks' (block-handler/get-top-level-blocks blocks)]
+      (p/do!
+       (ui-outliner-tx/transact!
+        {:outliner-op :delete-blocks}
+        (outliner-op/delete-blocks! blocks' nil))
+       (when sibling-block
+         (let [{:keys [edit-block-f]} (move-to-prev-block repo sibling-block
+                                                          (get block :block/format :markdown)
+                                                          "")]
+           (when edit-block-f (edit-block-f))))))))
+
+(defn set-block-timestamp!
+  [block-id key value]
+  (let [key (string/lower-case (str key))
+        block-id (if (string? block-id) (uuid block-id) block-id)
+        value (str value)]
+    (when-let [block (db/entity [:block/uuid block-id])]
+      (let [{:block/keys [title]} block
+            content (or title (state/get-edit-content))
+            new-content (-> (text-util/remove-timestamp content key)
+                            (text-util/add-timestamp key value))]
+        (when (not= content new-content)
+          (let [input-id (state/get-edit-input-id)]
+            (if (and input-id
+                     (string/ends-with? input-id (str block-id)))
+              (state/set-edit-content! input-id new-content)
+              (save-block-if-changed! block new-content))))))))
+
+(defn set-editing-block-timestamp!
+  "Almost the same as set-block-timestamp! except for:
+   - it doesn't save the block
+   - it extracts current content from current input"
+  [key value]
+  (let [key (string/lower-case (str key))
+        value (str value)
+        content (state/get-edit-content)
+        new-content (-> (text-util/remove-timestamp content key)
+                        (text-util/add-timestamp key value))]
+    (when (not= content new-content)
+      (let [input-id (state/get-edit-input-id)]
+        (state/set-edit-content! input-id new-content)))))
+
+(defn set-blocks-id!
+  "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
+   Accepts a list of uuids."
+  [block-ids]
+  (let [repo (state/get-current-repo)]
+    (when-not (config/db-based-graph? repo)
+      (file-editor-handler/set-blocks-id! block-ids))))
+
+(defn copy-block-ref!
+  ([block-id]
+   (copy-block-ref! block-id #(str %)))
+  ([block-id tap-clipboard]
+   (p/do!
+    (save-current-block!)
+    (set-blocks-id! [block-id])
+    (util/copy-to-clipboard! (tap-clipboard block-id)))))
+
+(defn select-block!
+  [block-uuid]
+  (block-handler/select-block! block-uuid))
+
+(defn- compose-copied-blocks-contents
+  [repo block-ids & {:as opts}]
+  (let [blocks (map (fn [id] (db/entity [:block/uuid id])) block-ids)
+        top-level-block-uuids (->> (block-handler/get-top-level-blocks blocks)
+                                   (map :block/uuid))
+        content (export-text/export-blocks-as-markdown
+                 repo top-level-block-uuids
+                 (merge
+                  opts
+                  {:indent-style (state/get-export-block-text-indent-style)
+                   :remove-options (set (state/get-export-block-text-remove-options))}))]
+    [top-level-block-uuids content]))
+
+(defn- get-all-blocks-by-ids
+  [repo ids]
+  (loop [ids ids
+         result []]
+    (if (seq ids)
+      (let [db-id (:db/id (db/entity [:block/uuid (first ids)]))
+            blocks (tree/get-sorted-block-and-children repo db-id
+                                                       {:include-property-block? true})
+            result (vec (concat result blocks))]
+        (recur (remove (set (map :block/uuid result)) (rest ids)) result))
+      result)))
+
+(defn copy-selection-blocks
+  [html? & {:keys [selected-blocks] :as opts}]
+  (let [repo (state/get-current-repo)
+        selected-ids (state/get-selection-block-ids)
+        ids (or (seq selected-ids) (map :block/uuid selected-blocks))
+        [top-level-block-uuids content] (compose-copied-blocks-contents repo ids opts)
+        block (db/entity [:block/uuid (first ids)])
+        db-based? (config/db-based-graph? repo)]
+    (when block
+      (let [html (export-html/export-blocks-as-html repo top-level-block-uuids nil)
+            copied-blocks (cond->> (get-all-blocks-by-ids repo top-level-block-uuids)
+                            db-based?
+                            (map (fn [block]
+                                   (let [b (db/entity (:db/id block))]
+                                     (->
+                                      (->> (map (fn [[k v]]
+                                                  (let [v' (cond
+                                                             (and (map? v) (:db/id v))
+                                                             [:block/uuid (:block/uuid (db/entity (:db/id v)))]
+                                                             (and (coll? v) (every? #(and (map? %) (:db/id %)) v))
+                                                             (set (map (fn [i] [:block/uuid (:block/uuid (db/entity (:db/id i)))]) v))
+                                                             :else
+                                                             v)]
+                                                    [k v'])) b)
+                                           (into {}))
+                                      (assoc :db/id (:db/id b)))))))]
+        (common-handler/copy-to-clipboard-without-id-property! repo (get block :block/format :markdown) content (when html? html) copied-blocks))
+      (state/set-block-op-type! :copy)
+      (notification/show! "Copied!" :success))))
+
+(defn copy-block-refs
+  []
+  (when-let [selected-blocks (seq (get-selected-blocks))]
+    (let [blocks (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
+                                        (let [level (dom/attr % "level")]
+                                          {:id (uuid id)
+                                           :level (int level)}))
+                                     selected-blocks))
+                      (remove nil?))
+          first-block (first blocks)
+          first-root-level-index (ffirst
+                                  (filter (fn [[_ block]] (= (:level block) 1))
+                                          (map-indexed vector blocks)))
+          root-level (atom (:level first-block))
+          adjusted-blocks (map-indexed
+                           (fn [index {:keys [id level]}]
+                             {:id id
+                              :level (if (< index first-root-level-index)
+                                       (if (< level @root-level)
+                                         (do
+                                           (reset! root-level level)
+                                           1)
+                                         (inc (- level @root-level)))
+                                       level)})
+                           blocks)
+          block (db/entity [:block/uuid (:id first-block)])
+          copy-str (some->> adjusted-blocks
+                            (map (fn [{:keys [id level]}]
+                                   (if (config/db-based-graph? (state/get-current-repo))
+                                     (str (string/join (repeat (dec level) "\t")) "- " (ref/->page-ref id))
+                                     (condp = (get block :block/format :markdown)
+                                       :org
+                                       (str (string/join (repeat level "*")) " " (ref/->block-ref id))
+                                       :markdown
+                                       (str (string/join (repeat (dec level) "\t")) "- " (ref/->block-ref id))))))
+                            (string/join "\n\n"))]
+      (set-blocks-id! (map :id blocks))
+      (util/copy-to-clipboard! copy-str))))
+
+(defn copy-block-embeds
+  []
+  (when-let [blocks (seq (get-selected-blocks))]
+    (let [ids (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
+                                     (uuid id)) blocks))
+                   (remove nil?))
+          ids-str (if (config/db-based-graph? (state/get-current-repo))
+                    (some->> ids
+                             (map (fn [id] (ref/->block-ref id)))
+                             (string/join "\n\n"))
+                    (some->> ids
+                             (map (fn [id] (util/format "{{embed ((%s))}}" id)))
+                             (string/join "\n\n")))]
+      (set-blocks-id! ids)
+      (util/copy-to-clipboard! ids-str))))
+
+(defn get-selected-toplevel-block-uuids
+  []
+  (when-let [blocks (seq (get-selected-blocks))]
+    (let [repo (state/get-current-repo)
+          block-ids (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
+                                           (uuid id)) blocks))
+                         (remove nil?))
+          blocks (db-utils/pull-many repo '[*] (mapv (fn [id] [:block/uuid id]) block-ids))
+          page-id (:db/id (:block/page (first blocks)))
+          ;; filter out blocks not belong to page with 'page-id'
+          blocks* (remove (fn [block] (some-> (:db/id (:block/page block)) (not= page-id))) blocks)]
+      (->> (block-handler/get-top-level-blocks blocks*)
+           (map :block/uuid)))))
+
+(defn cut-selection-blocks
+  [copy?]
+  (when copy? (copy-selection-blocks true))
+  (state/set-block-op-type! :cut)
+  (when-let [blocks (->> (get-selected-blocks)
+                         (remove #(dom/has-class? % "property-value-container"))
+                         seq)]
+    ;; remove queries
+    (let [dom-blocks (remove (fn [block] (= "true" (dom/attr block "data-query"))) blocks)]
+      (when (seq dom-blocks)
+        (let [repo (state/get-current-repo)
+              block-uuids (distinct (map #(uuid (dom/attr % "blockid")) dom-blocks))
+              lookup-refs (map (fn [id] [:block/uuid id]) block-uuids)
+              blocks (->> (map db/entity lookup-refs)
+                          (remove ldb/page?))
+              top-level-blocks (when (seq blocks) (block-handler/get-top-level-blocks blocks))
+              sorted-blocks (mapcat (fn [block]
+                                      (tree/get-sorted-block-and-children repo (:db/id block)))
+                                    top-level-blocks)]
+          (when (seq sorted-blocks)
+            (delete-blocks! repo (map :block/uuid sorted-blocks) sorted-blocks dom-blocks)))))))
+
+(def url-regex
+  "Didn't use link/plain-link as it is incorrectly detects words as urls."
+  #"[^\s\(\[]+://[^\s\)\]]+")
+
+(defn extract-nearest-link-from-text
+  [text pos & additional-patterns]
+  (let [;; didn't use page-ref regexs b/c it handles page-ref and org link cases
+        page-pattern #"\[\[([^\]]+)]]"
+        tag-pattern #"#\S+"
+        page-matches (util/re-pos page-pattern text)
+        block-matches (util/re-pos block-ref/block-ref-re text)
+        tag-matches (util/re-pos tag-pattern text)
+        additional-matches (mapcat #(util/re-pos % text) additional-patterns)
+        matches (->> (concat page-matches block-matches tag-matches additional-matches)
+                     (remove nil?))
+        [_ match] (first (sort-by
+                          (fn [[start-pos content]]
+                            (let [end-pos (+ start-pos (count content))]
+                              (cond
+                                (< pos start-pos)
+                                (- pos start-pos)
+
+                                (> pos end-pos)
+                                (- end-pos pos)
+
+                                :else
+                                0)))
+                          >
+                          matches))]
+    (when match
+      (cond
+        (some #(re-find % match) additional-patterns)
+        match
+        (string/starts-with? match "#")
+        (subs match 1 (count match))
+        :else
+        (subs match 2 (- (count match) 2))))))
+
+(defn- get-nearest-page-or-url
+  "Return the nearest page-name (not dereferenced, may be an alias), block, tag or url"
+  []
+  (when-let [block (state/get-edit-block)]
+    (when (:block/uuid block)
+      (when-let [edit-id (state/get-edit-input-id)]
+        (when-let [input (gdom/getElement edit-id)]
+          (when-let [pos (cursor/pos input)]
+            (let [value (gobj/get input "value")]
+              (extract-nearest-link-from-text value pos url-regex))))))))
+
+(defn get-nearest-page
+  "Return the nearest page-name (not dereferenced, may be an alias), block or tag"
+  []
+  (when-let [block (state/get-edit-block)]
+    (when (:block/uuid block)
+      (when-let [edit-id (state/get-edit-input-id)]
+        (when-let [input (gdom/getElement edit-id)]
+          (when-let [pos (cursor/pos input)]
+            (let [value (gobj/get input "value")]
+              (extract-nearest-link-from-text value pos))))))))
+
+(defn follow-link-under-cursor!
+  []
+  (when-let [page (get-nearest-page-or-url)]
+    (when-not (string/blank? page)
+      (p/do!
+       (state/clear-editor-action!)
+       (save-current-block!)
+       (if (re-find url-regex page)
+         (js/window.open page)
+         (let [page-name (db-model/get-redirect-page-name page)]
+           (state/clear-edit!)
+           (insert-first-page-block-if-not-exists! page-name)))))))
+
+(defn open-link-in-sidebar!
+  []
+  (when-let [page (get-nearest-page)]
+    (let [page-name (string/lower-case page)
+          block? (util/uuid-string? page-name)]
+      (when-let [page (db/get-page page-name)]
+        (if block?
+          (state/sidebar-add-block!
+           (state/get-current-repo)
+           (:db/id page)
+           :block)
+          (state/sidebar-add-block!
+           (state/get-current-repo)
+           (:db/id page)
+           :page))))))
+
+(declare save-current-block!)
+
+;; FIXME: shortcut `mod+.` doesn't work on Web (Chrome)
+(defn zoom-in! []
+  (if (state/editing?)
+    (when-let [id (some-> (state/get-edit-block)
+                          :block/uuid
+                          ((fn [id] [:block/uuid id]))
+                          db/entity
+                          :block/uuid)]
+      (state/clear-editor-action!)
+      (state/set-editing-block-id! [:unknown-container id])
+      (p/do!
+       (save-current-block!)
+       (route-handler/redirect-to-page! id)))
+    (js/window.history.forward)))
+
+(defn zoom-out!
+  []
+  (if (state/editing?)
+    (let [page (state/get-current-page)
+          block-id (and (string? page) (parse-uuid page))]
+      (p/do!
+       (state/clear-editor-action!)
+       (save-current-block!)
+       (when block-id
+         (state/set-editing-block-id! [:unknown-container (:block/uuid (state/get-edit-block))])
+         (let [block-parent (db/get-block-parent block-id)]
+           (if-let [id (and
+                        (nil? (:block/name block-parent))
+                        (:block/uuid block-parent))]
+             (route-handler/redirect-to-page! id)
+             (let [page-id (some-> (db/entity [:block/uuid block-id])
+                                   :block/page
+                                   :db/id)]
+               (when-let [page (db/entity page-id)]
+                 (route-handler/redirect-to-page! (:block/uuid page)))))))))
+    (js/window.history.back)))
+
+(defn cut-block!
+  [block-id]
+  (when-let [block (db/entity [:block/uuid block-id])]
+    (let [repo (state/get-current-repo)
+          ;; TODO: support org mode
+          [_top-level-block-uuids md-content] (compose-copied-blocks-contents repo [block-id])
+          html (export-html/export-blocks-as-html repo [block-id] nil)
+          sorted-blocks (tree/get-sorted-block-and-children repo (:db/id block))]
+      (common-handler/copy-to-clipboard-without-id-property! repo (get block :block/format :markdown) md-content html sorted-blocks)
+      (state/set-block-op-type! :cut)
+      (delete-block-aux! block))))
+
+(defn highlight-selection-area!
+  [end-block-id & {:keys [append?]}]
+  (when-let [start-block (state/get-selection-start-block-or-first)]
+    (let [end-block-node (gdom/getElement end-block-id)
+          start-node (gdom/getElement start-block)
+          select-direction (state/get-selection-direction)
+          selected-blocks (state/get-unsorted-selection-blocks)
+          last-node (when-let [node (last selected-blocks)]
+                      (gdom/getElement (.-id ^js node)))
+          latest-visible-block (or last-node start-node)
+          latest-block-id (when latest-visible-block (.-id latest-visible-block))]
+      (if (and start-node end-block-node)
+        (let [blocks (util/get-nodes-between-two-nodes start-block end-block-id "ls-block")
+              direction (util/get-direction-between-two-nodes start-block end-block-id "ls-block")
+              blocks (if (= direction :up) (reverse blocks) blocks)]
+          (state/exit-editing-and-set-selected-blocks! blocks direction))
+        (when latest-visible-block
+          (let [blocks (util/get-nodes-between-two-nodes latest-block-id end-block-id "ls-block")
+                direction (if (= latest-block-id end-block-id)
+                            select-direction
+                            (util/get-direction-between-two-nodes latest-block-id end-block-id "ls-block"))
+                blocks (if (= direction :up) (reverse (util/sort-by-height blocks)) (util/sort-by-height blocks))]
+            (if append?
+              (do (state/clear-edit!)
+                  (if (and select-direction (not= direction select-direction))
+                    (state/drop-selection-blocks-starts-with! end-block-node)
+                    (state/conj-selection-block! blocks direction)))
+              (state/exit-editing-and-set-selected-blocks! blocks direction))))))))
+
+(defonce *action-bar-timeout (atom nil))
+
+(defn show-action-bar!
+  [& {:keys [delay]
+      :or {delay 200}}]
+  (when (config/db-based-graph?)
+    (when-let [timeout @*action-bar-timeout]
+      (js/clearTimeout timeout))
+    (state/pub-event! [:editor/hide-action-bar])
+    (let [timeout (js/setTimeout #(state/pub-event! [:editor/show-action-bar]) delay)]
+      (reset! *action-bar-timeout timeout))))
+
+(defn- select-block-up-down
+  [direction]
+  (cond
+    ;; when editing, quit editing and select current block
+    (state/editing?)
+    (let [element (gdom/getElement (state/get-editing-block-dom-id))]
+      (when element
+        (p/do!
+         (save-current-block!)
+         (util/scroll-to-block element)
+         (state/exit-editing-and-set-selected-blocks! [element]))))
+
+    ;; when selection and one block selected, select next block
+    (and (state/selection?) (== 1 (count (state/get-selection-blocks))))
+    (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
+          element (f (first (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
+      (when element
+        (util/scroll-to-block element)
+        (state/conj-selection-block! element direction)))
+
+    ;; if same direction, keep conj on same direction
+    (and (state/selection?) (= direction (state/get-selection-direction)))
+    (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
+          first-last (if (= :up direction) first last)
+          element (f (first-last (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
+      (when element
+        (util/scroll-to-block element)
+        (state/conj-selection-block! element direction)))
+
+    ;; if different direction, keep clear until one left
+    (state/selection?)
+    (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
+          last-first (if (= :up direction) last first)
+          element (f (last-first (state/get-selection-blocks))
+                     {:up-down? true
+                      :exclude-property? true})]
+      (when element
+        (util/scroll-to-block element)
+        (state/drop-last-selection-block!))))
+  (show-action-bar! {:delay 500})
+  nil)
+
+(defn on-select-block
+  [direction]
+  (fn [_event]
+    (select-block-up-down direction)))
+
+(defn save-block-aux!
+  [block value opts]
+  (let [entity (db/entity [:block/uuid (:block/uuid block)])]
+    (when (and (:db/id entity) (not (ldb/built-in? entity)))
+      (let [value (string/trim value)]
+        ;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever
+        ;; maybe we shouldn't save the block/file in "will-unmount" event?
+        (save-block-if-changed! block value opts)))))
+
+(defn save-block!
+  ([repo block-or-uuid content]
+   (save-block! repo block-or-uuid content {}))
+  ([repo block-or-uuid content {:keys [properties] :as opts}]
+   (let [block (if (or (uuid? block-or-uuid)
+                       (string? block-or-uuid))
+                 (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)]
+     (save-block!
+      {:block block :repo repo :opts (dissoc opts :properties)}
+      (if (seq properties)
+        (property-file/insert-properties-when-file-based repo (get block :block/format :markdown) content properties)
+        content))))
+  ([{:keys [block repo opts] :as _state} value]
+   (let [repo (or repo (state/get-current-repo))]
+     (when (db/entity repo [:block/uuid (:block/uuid block)])
+       (save-block-aux! block value opts)))))
+
+(defn save-blocks!
+  [blocks]
+  (ui-outliner-tx/transact!
+   {:outliner-op :save-block}
+   (doseq [[block value] blocks]
+     (save-block-if-changed! block value))))
+
+(defn save-current-block!
+  "skip-properties? if set true, when editing block is likely be properties, skip saving"
+  ([]
+   (save-current-block! {}))
+  ([{:keys [force? skip-properties? current-block] :as opts}]
+   ;; non English input method
+   (when-not (or (state/editor-in-composition?)
+                 (state/get-editor-action))
+     (when (state/get-current-repo)
+       (try
+         (let [input-id (state/get-edit-input-id)
+               block (state/get-edit-block)
+               db-block (when-let [block-id (:block/uuid block)]
+                          (db/entity [:block/uuid block-id]))
+               elem (and input-id (gdom/getElement input-id))
+               db-content (:block/title db-block)
+               db-content-without-heading (and db-content
+                                               (common-util/safe-subs db-content (:block/level db-block)))
+               value (if (= (:block/uuid current-block) (:block/uuid block))
+                       (:block/title current-block)
+                       (and elem (gobj/get elem "value")))]
+           (when value
+             (cond
+               force?
+               (save-block-aux! db-block value opts)
+
+               (and skip-properties?
+                    (db-model/top-block? block)
+                    (when elem (thingatpt/properties-at-point elem)))
+               nil
+
+               (and block value db-content-without-heading
+                    (not= (string/trim db-content-without-heading)
+                          (string/trim value)))
+               (save-block-aux! db-block value opts))))
+         (catch :default error
+           (log/error :save-block-failed error)))))))
+
+(defn- clean-content!
+  [repo format content]
+  (if (config/db-based-graph? repo)
+    content
+    (some->> (text/remove-level-spaces content format (config/get-block-pattern format))
+             (drawer/remove-logbook)
+             (property-file/remove-properties-when-file-based repo format)
+             string/trim)))
+
+(def insert-command! editor-common-handler/insert-command!)
+
+(defn delete-asset-of-block!
+  [{:keys [repo asset-block href full-text block-id local? delete-local?] :as _opts}]
+  (let [block (db-model/query-block-by-uuid block-id)
+        _ (or block (throw (ex-info (str block-id " not exists")
+                                    {:block-id block-id})))
+        text (:block/title block)
+        content (if asset-block
+                  (string/replace text (ref/->page-ref (:block/uuid asset-block)) "")
+                  (string/replace text full-text ""))]
+    (save-block! repo block content)
+    (when (and local? delete-local?)
+      (if asset-block
+        (delete-block-aux! asset-block)
+        (when-let [href (if (util/electron?) href
+                            (second (re-find #"\((.+)\)$" full-text)))]
+          (let [block-file-rpath (file-model/get-block-file-path block)
+                asset-fpath (if (string/starts-with? href "assets://")
+                              (path/url-to-path href)
+                              (config/get-repo-fpath
+                               repo
+                               (path/resolve-relative-path block-file-rpath href)))]
+            (fs/unlink! repo asset-fpath nil)))))))
+
+(defn- write-file!
+  [repo dir file file-rpath file-name]
+  (if (util/electron?)
+    (if-let [from (not-empty (.-path file))]
+      (-> (js/window.apis.copyFileToAssets dir file-rpath from)
+          (p/catch #(js/console.error "Debug: Copy Asset Error#" %)))
+      (-> (p/let [buffer (.arrayBuffer file)]
+            (fs/write-file! repo dir file-rpath buffer {:skip-compare? false}))
+          (p/catch #(js/console.error "Debug: Writing Asset #" %))))
+    (->
+     (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
+            (cond
+              (mobile-util/native-platform?)
+                          ;; capacitor fs accepts Blob, File implements Blob
+              (p/let [buffer (.arrayBuffer file)
+                      content (base64/encodeByteArray (js/Uint8Array. buffer))
+                      fpath (path/path-join dir file-rpath)]
+                (capacitor-fs/<write-file-with-base64 fpath content))
+
+              (config/db-based-graph? repo) ;; memory-fs
+              (p/let [buffer (.arrayBuffer file)
+                      content (js/Uint8Array. buffer)]
+                (fs/write-file! repo dir file-rpath content nil))
+
+              :else
+              (throw (ex-info "Paste failed"
+                              {:file-name file-name}))))
+     (p/catch (fn [error]
+                (prn :paste-file-error)
+                (js/console.error error))))))
+
+(defn db-based-save-assets!
+  "Save incoming(pasted) assets to assets directory.
+
+   Returns: asset entity"
+  [repo files & {:keys [pdf-area?]}]
+  (p/let [[repo-dir asset-dir-rpath] (assets-handler/ensure-assets-dir! repo)]
+    (p/all
+     (for [[_index ^js file] (map-indexed vector files)]
+      ;; WARN file name maybe fully qualified path when paste file
+       (p/let [file-name (util/node-path.basename (.-name file))
+               file-name-without-ext (.-name (util/node-path.parse file-name))
+               checksum (assets-handler/get-file-checksum file)
+               existing-asset (db-async/<get-asset-with-checksum repo checksum)]
+         (if existing-asset
+           existing-asset
+           (p/let [block-id (ldb/new-block-id)
+                   ext (when file-name
+                         (string/lower-case (.substr (util/node-path.extname file-name) 1)))
+                   _ (when (string/blank? ext)
+                       (throw (ex-info "File doesn't have a valid ext."
+                                       {:file-name file-name})))
+                   file-path   (str block-id "." ext)
+                   file-rpath  (str asset-dir-rpath "/" file-path)
+                   dir repo-dir
+                   asset (db/entity :logseq.class/Asset)
+                   properties {:logseq.property.asset/type ext
+                               :logseq.property.asset/size (.-size file)
+                               :logseq.property.asset/checksum checksum
+                               :block/tags (:db/id asset)}
+                   insert-opts {:custom-uuid block-id
+                                :edit-block? false
+                                :properties properties}
+                   _ (write-file! repo dir file file-rpath file-name)
+                   edit-block (state/get-edit-block)
+                   insert-to-current-block-page? (and (:block/uuid edit-block) (string/blank? (state/get-edit-content)) (not pdf-area?))
+                   insert-opts' (if insert-to-current-block-page?
+                                  (assoc insert-opts
+                                         :block-uuid (:block/uuid edit-block)
+                                         :replace-empty-target? true
+                                         :sibling? true)
+                                  (assoc insert-opts :page (:block/uuid asset)))
+                   result (api-insert-new-block! file-name-without-ext insert-opts')
+                   new-entity (db/entity [:block/uuid (:block/uuid result)])]
+             (when insert-to-current-block-page?
+               (state/clear-edit!))
+             (or new-entity
+                 (throw (ex-info "Can't save asset" {:files files}))))))))))
+
+(defn db-upload-assets!
+  "Paste asset for db graph and insert link to current editing block"
+  [repo id ^js files format uploading? drop-or-paste?]
+  (when (or (config/local-file-based-graph? repo)
+            (config/db-based-graph? repo))
+    (-> (db-based-save-assets! repo (js->clj files))
+          ;; FIXME: only the first asset is handled
+        (p/then
+         (fn [entities]
+           (let [entity (first entities)]
+             (insert-command!
+              id
+              (ref/->page-ref (:block/uuid entity))
+              format
+              {:last-pattern (if drop-or-paste? "" commands/command-trigger)
+               :restore?     true
+               :command      :insert-asset})
+             entities)))
+        (p/catch (fn [e]
+                   (js/console.error e)))
+        (p/finally
+          (fn []
+            (reset! uploading? false)
+            (reset! *asset-uploading? false)
+            (reset! *asset-uploading-process 0))))))
+
+(defn upload-asset!
+  "Paste asset and insert link to current editing block"
+  [id ^js files format uploading? drop-or-paste?]
+  (let [repo (state/get-current-repo)]
+    (if (config/db-based-graph? repo)
+      (db-upload-assets! repo id ^js files format uploading? drop-or-paste?)
+      (file-editor-handler/file-upload-assets! repo id ^js files format uploading? *asset-uploading? *asset-uploading-process drop-or-paste?))))
+
+;; Editor should track some useful information, like editor modes.
+;; For example:
+;; 1. Which file format is it, markdown or org mode?
+;; 2. Is it in the properties area? Then we can enable the ":" autopair
+(def autopair-map
+  {"[" "]"
+   "{" "}"
+   "(" ")"
+   "`" "`"
+   "~" "~"
+   "*" "*"
+   "_" "_"
+   "^" "^"
+   "=" "="
+   "/" "/"
+   "+" "+"})
+;; ":" ":"                              ; TODO: only properties editing and org mode tag
+
+(def reversed-autopair-map
+  (zipmap (vals autopair-map)
+          (keys autopair-map)))
+
+(def autopair-when-selected
+  #{"*" "^" "_" "=" "+" "/"})
+
+(def delete-map
+  (assoc autopair-map
+         "$" "$"
+         ":" ":"))
+
+(defn- autopair
+  [input-id prefix _format _option]
+  (let [value (get autopair-map prefix)
+        selected (util/get-selected-text)
+        postfix (str selected value)
+        value (str prefix postfix)
+        input (gdom/getElement input-id)]
+    (when value
+      (let [[prefix _pos] (commands/simple-replace! input-id value selected
+                                                    {:backward-pos (count postfix)
+                                                     :check-fn (fn [new-value prefix-pos]
+                                                                 (when (>= prefix-pos 0)
+                                                                   [(subs new-value prefix-pos (+ prefix-pos 2))
+                                                                    (+ prefix-pos 2)]))})]
+        (cond
+          (= prefix page-ref/left-brackets)
+          (do
+            (commands/handle-step [:editor/search-page])
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)
+                                            :selected selected}))
+
+          (and (= prefix block-ref/left-parens)
+               (config/db-based-graph? (state/get-current-repo)))
+          (notification/show!
+           "To reference a node, please use `[[]]`."
+           :warning)
+
+          (= prefix block-ref/left-parens)
+          (do
+            (commands/handle-step [:editor/search-block :reference])
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)
+                                            :selected selected})))))))
+
+(defn surround-by?
+  [input before end]
+  (when input
+    (let [value (gobj/get input "value")
+          pos (cursor/pos input)]
+      (text-util/surround-by? value pos before end))))
+
+(defn- autopair-left-paren?
+  [input key]
+  (and (= key "(")
+       (or (surround-by? input :start "")
+           (surround-by? input "\n" "")
+           (surround-by? input " " "")
+           (surround-by? input "]" "")
+           (surround-by? input "(" ""))))
+
+(defn wrapped-by?
+  [input before end]
+  (when input
+    (let [value (gobj/get input "value")
+          pos (cursor/pos input)]
+      (when (>= pos 0)
+        (text-util/wrapped-by? value pos before end)))))
+
+(defn get-matched-classes
+  "Return matched classes except the root tag"
+  [q]
+  (let [classes (->> (db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
+                     (mapcat (fn [class]
+                               (conj (:block/alias class) class)))
+                     (common-util/distinct-by :db/id)
+                     (map (fn [e] (select-keys e [:block/uuid :block/title]))))]
+    (search/fuzzy-search classes q {:extract-fn :block/title})))
+
+(defn <get-matched-blocks
+  "Return matched blocks that are not built-in"
+  [q & [{:keys [nlp-pages? page-only?]}]]
+  (p/let [block (state/get-edit-block)
+          result (search/block-search (state/get-current-repo) q {:built-in? false
+                                                                  :enable-snippet? false
+                                                                  :page-only? page-only?})
+          matched (remove (fn [b] (= (:block/uuid b) (:block/uuid block))) result)]
+    (-> (concat matched
+                (when nlp-pages?
+                  (map (fn [title] {:block/title title :nlp-date? true})
+                       date/nlp-pages)))
+        (search/fuzzy-search q {:extract-fn :block/title :limit 50}))))
+
+(defn <get-matched-templates
+  [q]
+  (search/template-search q))
+
+(defn <get-matched-properties
+  [q]
+  (search/property-search q))
+
+(defn get-matched-property-values
+  [property q]
+  (search/property-value-search property q))
+
+(defn get-last-command
+  [input]
+  (try
+    (let [edit-content (or (gobj/get input "value") "")
+          pos (cursor/pos input)
+          last-slash-caret-pos (:pos (:pos (state/get-editor-action-data)))
+          last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
+      (when (> pos 0) last-command))
+    (catch :default e
+      (js/console.error e)
+      nil)))
+
+(defn get-matched-commands
+  [command]
+  (condp = command
+    nil nil
+    "" @commands/*initial-commands
+    (commands/get-matched-commands command)))
+
+(defn auto-complete?
+  []
+  (or @*asset-uploading?
+      (state/get-editor-action)))
+
+(defn in-shui-popup?
+  []
+  (or (some-> js/document.activeElement
+              (.closest "[data-radix-menu-content]")
+              (nil?)
+              (not))
+      (.querySelector js/document.body
+                      "div[data-radix-popper-content-wrapper]")))
+
+(defn get-current-input-char
+  [input]
+  (when-let [pos (cursor/pos input)]
+    (let [value (gobj/get input "value")]
+      (when (and (>= (count value) (inc pos))
+                 (>= pos 1))
+        (util/nth-safe value pos)))))
+
+(defn move-up-down
+  [up?]
+  (fn [event]
+    (util/stop event)
+    (state/pub-event! [:editor/hide-action-bar])
+    (let [edit-block-id (:block/uuid (state/get-edit-block))
+          move-nodes (fn [blocks]
+                       (let [blocks' (block-handler/get-top-level-blocks blocks)
+                             result (ui-outliner-tx/transact!
+                                     {:outliner-op :move-blocks}
+                                     (outliner-op/move-blocks-up-down! blocks' up?))]
+                         (when-let [block-node (util/get-first-block-by-id (:block/uuid (first blocks)))]
+                           (.scrollIntoView block-node #js {:behavior "smooth" :block "nearest"}))
+                         result))]
+      (if edit-block-id
+        (when-let [block (db/entity [:block/uuid edit-block-id])]
+          (let [blocks [(assoc block :block/title (state/get-edit-content))]
+                container-id (get-new-container-id (if up? :move-up :move-down) {})]
+            (p/do!
+             (save-current-block!)
+             (move-nodes blocks)
+             (if container-id
+               (state/set-editing-block-id! [container-id edit-block-id])
+               (when-let [input (some-> (state/get-edit-input-id) gdom/getElement)]
+                 (.focus input)
+                 (util/scroll-editor-cursor input))))))
+        (let [ids (state/get-selection-block-ids)]
+          (when (seq ids)
+            (let [lookup-refs (map (fn [id] [:block/uuid id]) ids)
+                  blocks (map db/entity lookup-refs)]
+              (move-nodes blocks))))))))
+
+(defn get-selected-ordered-blocks
+  []
+  (let [repo (state/get-current-repo)
+        ids (state/get-selection-block-ids)
+        lookup-refs (->> (map (fn [id] [:block/uuid id]) ids)
+                         (remove nil?))]
+    (db/pull-many repo '[*] lookup-refs)))
+
+(defn on-tab
+  "`direction` = :left | :right."
+  [direction]
+  (let [blocks (get-selected-ordered-blocks)]
+    (block-handler/indent-outdent-blocks! blocks (= direction :right) nil)))
+
+(defn- get-link [format link label]
+  (let [link (or link "")
+        label (or label "")]
+    (case (keyword format)
+      :markdown (util/format "[%s](%s)" label link)
+      :org (util/format "[[%s][%s]]" link label)
+      nil)))
+
+(defn- get-image-link
+  [format link label]
+  (let [link (or link "")
+        label (or label "")]
+    (case (keyword format)
+      :markdown (util/format "![%s](%s)" label link)
+      :org (util/format "[[%s]]"))))
+
+(defn handle-command-input-close [id]
+  (state/set-editor-show-input! nil)
+  (when-let [saved-cursor (state/get-editor-last-pos)]
+    (when-let [input (gdom/getElement id)]
+      (cursor/move-cursor-to input saved-cursor true))))
+
+(defn handle-command-input [command id format m]
+  ;; TODO: Add error handling for when user doesn't provide a required field.
+  ;; (The current behavior is to just revert back to the editor.)
+  (case command
+
+    :link (let [{:keys [link label]} m]
+            (when-not (or (string/blank? link) (string/blank? label))
+              (insert-command!
+               id
+               (get-link format link label)
+               format
+               {:last-pattern (str commands/command-trigger "link")
+                :command :link})))
+
+    :image-link (let [{:keys [link label]} m]
+                  (when (not (string/blank? link))
+                    (insert-command!
+                     id
+                     (get-image-link format link label)
+                     format
+                     {:last-pattern (str commands/command-trigger "link")
+                      :command :image-link})))
+
+    nil)
+
+  (handle-command-input-close id))
+
+(defn restore-last-saved-cursor!
+  ([] (restore-last-saved-cursor! (state/get-input)))
+  ([input]
+   (when-let [saved-cursor (and input (state/get-editor-last-pos))]
+     (cursor/move-cursor-to input saved-cursor true))))
+
+(defn- close-autocomplete-if-outside
+  [input]
+  (when (and input
+             (contains? #{:page-search :page-search-hashtag :block-search} (state/get-editor-action))
+             (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
+             (not (wrapped-by? input block-ref/left-parens block-ref/right-parens))
+             ;; wrapped-by? doesn't detect multiple beginnings when ending with "" so
+             ;; use subs to correctly detect current hashtag
+             (not (text-util/wrapped-by? (subs (.-value input) 0 (cursor/pos input)) (cursor/pos input) commands/hashtag ""))
+             (not (and (config/db-based-graph? (state/get-current-repo))
+                       (= :block-search (state/get-editor-action)))))
+    (state/clear-editor-action!)))
+
+(defn resize-image!
+  [config block-id metadata full_text size]
+  (let [asset (:asset-block config)]
+    (if (config/db-based-graph?)
+      (property-handler/set-block-property! (state/get-current-repo)
+                                            (if asset (:db/id asset) block-id)
+                                            :logseq.property.asset/resize-metadata
+                                            size)
+      (let [new-meta (merge metadata size)
+            image-part (first (string/split full_text #"\{"))
+            md-link? (string/starts-with? image-part "![")
+            new-full-text (str (if md-link? image-part (str "![image](" image-part ")")) (pr-str new-meta))
+            block (db/entity [:block/uuid block-id])
+            value (:block/title block)
+            new-value (string/replace value full_text new-full-text)]
+        (save-block-aux! block new-value {})))))
+
+(defonce *auto-save-timeout (atom nil))
+(defn edit-box-on-change!
+  [e block id]
+  (when (= (:db/id block) (:db/id (state/get-edit-block)))
+    (let [value (util/evalue e)
+          repo (state/get-current-repo)]
+      (state/set-edit-content! id value false)
+      (when @*auto-save-timeout
+        (js/clearTimeout @*auto-save-timeout))
+      (block-handler/mark-last-input-time! repo)
+      (reset! *auto-save-timeout
+              (js/setTimeout
+               (fn []
+                 (when (and (state/input-idle? repo :diff 450)
+                          ;; don't auto-save block if it has tags
+                            (not (and
+                                  (config/db-based-graph? repo)
+                                  (re-find #"#\S+" value))))
+                 ; don't auto-save for page's properties block
+                   (save-current-block! {:skip-properties? true})))
+               450)))))
+
+(defn- start-of-new-word?
+  [input pos]
+  (contains? #{" " "\t"} (get (.-value input) (- pos 2))))
+
+(defn handle-last-input []
+  (let [input           (state/get-input)
+        input-id        (state/get-edit-input-id)
+        edit-block      (state/get-edit-block)
+        pos             (cursor/pos input)
+        content         (.-value input)
+        last-input-char (util/nth-safe content (dec pos))
+        last-prev-input-char (util/nth-safe content (dec (dec pos)))
+        prev-prev-input-char (util/nth-safe content (- pos 3))
+        repo (state/get-current-repo)
+        db-based? (config/db-based-graph? repo)]
+
+    ;; TODO: is it cross-browser compatible?
+    ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
+    (cond
+      (and (= content "1. ") (= last-input-char " ") input-id edit-block
+           (not (own-order-number-list? edit-block)))
+      (p/let [_ (state/pub-event! [:editor/toggle-own-number-list edit-block])]
+        (state/set-edit-content! input-id ""))
+
+      (and (= last-input-char commands/command-trigger)
+           (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (commands/reinit-matched-commands!)
+        (state/set-editor-show-commands!))
+
+      (and (= last-input-char last-prev-input-char commands/colon)
+           (or (nil? prev-prev-input-char)
+               (= prev-prev-input-char "\n"))
+           (not db-based?))
+      (do
+        (cursor/move-cursor-backward input 2)
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-action! :property-search))
+
+      (and
+       (not= :property-search (state/get-editor-action))
+       (let [{:keys [line start-pos]} (text-util/get-current-line-by-pos (.-value input) (dec pos))]
+         (text-util/wrapped-by? line (- pos start-pos) "" gp-property/colons))
+       (not db-based?))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-action! :property-search))
+
+      (and (= last-input-char commands/colon)
+           (= :property-search (state/get-editor-action))
+           (not db-based?))
+      (state/clear-editor-action!)
+
+      (or (= last-input-char last-prev-input-char commands/hashtag)
+          (and (= last-prev-input-char commands/hashtag)
+               (= last-input-char " ")))
+      (state/clear-editor-action!)
+
+      ;; Open "Search page or New page" auto-complete
+      (and (= last-input-char commands/hashtag)
+             ;; Only trigger at beginning of a line, before whitespace or after a reference
+           (or (re-find #"(?m)^#" (str (.-value input)))
+               (start-of-new-word? input pos)
+               (and db-based? (= page-ref/right-brackets (common-util/safe-subs (str (.-value input)) (- pos 3) (dec pos))))))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-last-pos! pos)
+        (state/set-editor-action! :page-search-hashtag))
+
+      :else
+      nil)))
+
+(defn get-selected-text
+  []
+  (let [text (:selected (state/get-editor-action-data))]
+    (when-not (string/blank? text)
+      text)))
+
+(defn block-on-chosen-handler
+  [id q format selected-text]
+  (fn [chosen _click?]
+    (state/clear-editor-action!)
+    (let [uuid-string (str (:block/uuid chosen))]
+
+      ;; block reference
+      (insert-command! id
+                       (ref/->block-ref uuid-string)
+                       format
+                       {:last-pattern (str block-ref/left-parens (if selected-text "" q))
+                        :end-pattern block-ref/right-parens
+                        :postfix-fn   (fn [s] (util/replace-first block-ref/right-parens s ""))
+                        :forward-pos 3
+                        :command :block-ref})
+
+      ;; Save it so it'll be parsed correctly in the future
+      (property-handler/file-persist-block-id! (state/get-current-repo) (:block/uuid chosen))
+
+      (when-let [input (gdom/getElement id)]
+        (.focus input)))))
+
+(defn block-non-exist-handler
+  [input]
+  (fn []
+    (state/clear-editor-action!)
+    (cursor/move-cursor-forward input 2)))
+
+(defn- paste-block-cleanup
+  [repo block page exclude-properties format content-update-fn keep-uuid?]
+  (let [db-based? (config/db-based-graph? (state/get-current-repo))
+        new-content
+        (if content-update-fn
+          (content-update-fn (:block/title block))
+          (:block/title block))
+        new-content
+        (cond->> new-content
+          (not keep-uuid?) (property-file/remove-property-when-file-based repo format "id")
+          true             (property-file/remove-property-when-file-based repo format "custom_id"))]
+    (merge (apply dissoc block (conj (if-not keep-uuid? [:block/_refs] []) :block/pre-block? :block/meta))
+           (cond->
+            {:block/page {:db/id (:db/id page)}
+             :block/title new-content}
+             (not db-based?)
+             (assoc :block/properties (apply dissoc (not-empty (:block/properties block))
+                                             (concat
+                                              (when-not keep-uuid? [:id])
+                                              [:custom_id :custom-id]
+                                              exclude-properties))
+                    :block/format format)
+             (not db-based?)
+             (assoc :block/properties-text-values (apply dissoc (:block/properties-text-values block)
+                                                         (concat
+                                                          (when-not keep-uuid? [:id])
+                                                          exclude-properties)))))))
+
+(defn- edit-last-block-after-inserted!
+  [result]
+  (util/schedule
+   (fn []
+     (when-let [last-block (last (:blocks result))]
+       (clear-when-saved!)
+       (let [last-block' (db/entity [:block/uuid (:block/uuid last-block)])]
+         (edit-block! last-block' :max))))))
+
+(defn- nested-blocks
+  [blocks]
+  (let [ids (set (map :db/id blocks))]
+    (some? (some #(ids (:db/id (:block/parent %))) blocks))))
+
+(defn paste-blocks
+  "Given a vec of blocks, insert them into the target page.
+   keep-uuid?: if true, keep the uuid provided in the block structure."
+  [blocks {:keys [content-update-fn
+                  exclude-properties
+                  target-block
+                  sibling?
+                  keep-uuid?
+                  revert-cut-txs
+                  skip-empty-target?
+                  ops-only?]
+           :or {exclude-properties []}}]
+  (let [editing-block (when-let [editing-block (state/get-edit-block)]
+                        (some-> (db/entity [:block/uuid (:block/uuid editing-block)])
+                                (assoc :block/title (state/get-edit-content))))
+        has-unsaved-edits (and editing-block
+                               (not= (:block/title (db/entity (:db/id editing-block)))
+                                     (state/get-edit-content)))
+        target-block (or target-block editing-block)
+        block (db/entity (:db/id target-block))
+        page (if (:block/name block) block
+                 (when target-block (:block/page (db/entity (:db/id target-block)))))
+        empty-target? (if (true? skip-empty-target?) false
+                          (string/blank? (:block/title target-block)))
+        paste-nested-blocks? (nested-blocks blocks)
+        target-block-has-children? (db/has-children? (:block/uuid target-block))
+        replace-empty-target? (and empty-target?
+                                   (or (not target-block-has-children?)
+                                       (and target-block-has-children? (= (count blocks) 1))))
+        target-block' (if (and empty-target? target-block-has-children? paste-nested-blocks?)
+                        (or (ldb/get-left-sibling target-block)
+                            (:block/parent (db/entity (:db/id target-block))))
+                        target-block)
+        sibling? (cond
+                   (and paste-nested-blocks? empty-target?)
+                   (= (:block/parent target-block') (:block/parent target-block))
+
+                   (some? sibling?)
+                   sibling?
+
+                   target-block-has-children?
+                   false
+
+                   :else
+                   true)
+        transact-blocks! #(ui-outliner-tx/transact!
+                           {:outliner-op :insert-blocks
+                            :additional-tx revert-cut-txs}
+                           (when target-block'
+                             (let [format (get target-block' :block/format :markdown)
+                                   repo (state/get-current-repo)
+                                   blocks' (map (fn [block]
+                                                  (paste-block-cleanup repo block page exclude-properties format content-update-fn keep-uuid?))
+                                                blocks)]
+                               (outliner-op/insert-blocks! blocks' target-block' {:sibling? sibling?
+                                                                                  :outliner-op :paste
+                                                                                  :replace-empty-target? replace-empty-target?
+                                                                                  :keep-uuid? keep-uuid?}))))]
+    (if ops-only?
+      (transact-blocks!)
+      (p/let [_ (when has-unsaved-edits
+                  (ui-outliner-tx/transact!
+                   {:outliner-op :save-block}
+                   (outliner-save-block! editing-block)))
+              result (transact-blocks!)]
+        (state/set-block-op-type! nil)
+        (when result
+          (edit-last-block-after-inserted! result)
+          result)))))
+
+(defn- block-tree->blocks
+  "keep-uuid? - maintain the existing :uuid in tree vec"
+  [repo tree-vec format keep-uuid? page-name]
+  (->> (outliner-core/tree-vec-flatten tree-vec)
+       (map (fn [block]
+              (let [content (:content block)
+                    props (into [] (:properties block))
+                    content* (str (if (= :markdown format) "- " "* ")
+                                  (property-file/insert-properties-when-file-based repo format content props))
+                    ast (mldoc/->edn content* format)
+                    blocks (->> (block/extract-blocks ast content* format {:page-name page-name})
+                                (map wrap-parse-block))
+                    fst-block (first blocks)
+                    fst-block (if (and keep-uuid? (uuid? (:uuid block)))
+                                (assoc fst-block :block/uuid (:uuid block))
+                                fst-block)]
+                (assert fst-block "fst-block shouldn't be nil")
+                (assoc fst-block :block/level (:block/level block)))))))
+
+(defn insert-block-tree
+  "`tree-vec`: a vector of blocks.
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
+  (let [repo (state/get-current-repo)
+        page-id (:db/id (:block/page target-block))
+        page-name (some-> page-id (db/entity) :block/name)
+        blocks (block-tree->blocks repo tree-vec format keep-uuid? page-name)
+        blocks (gp-block/with-parent-and-order page-id blocks)
+        block-refs (->> (mapcat :block/refs blocks)
+                        (set)
+                        (filter (fn [ref] (and (vector? ref) (= :block/uuid (first ref))))))]
+    (ui-outliner-tx/transact!
+     {:outliner-op :paste-blocks}
+     (when (seq block-refs)
+       (db/transact! (map (fn [[_ id]] {:block/uuid id}) block-refs)))
+     (paste-blocks blocks (merge opts {:ops-only? true})))))
+
+(defn insert-block-tree-after-target
+  "`tree-vec`: a vector of blocks.
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [target-block-id sibling? tree-vec format keep-uuid?]
+  (insert-block-tree tree-vec format
+                     {:target-block       (db/entity target-block-id)
+                      :keep-uuid?         keep-uuid?
+                      :skip-empty-target? true
+                      :sibling?           sibling?}))
+
+(defn insert-template!
+  ([element-id db-id]
+   (insert-template! element-id db-id {}))
+  ([element-id db-id {:keys [target] :as opts}]
+   (let [repo (state/get-current-repo)
+         db? (config/db-based-graph? repo)]
+     (p/let [block (if (integer? db-id)
+                     (db-async/<pull repo db-id)
+                     (db-async/<get-template-by-name (name db-id)))
+             block (when (:block/uuid block)
+                     (db-async/<get-block repo (:block/uuid block)
+                                          {:children? true
+                                           :nested-children? true}))]
+       (when (:db/id block)
+         (let [journal? (ldb/journal? target)
+               target (or target (state/get-edit-block))
+               format (get block :block/format :markdown)
+               block-uuid (:block/uuid block)
+               template-including-parent? (not (false? (:template-including-parent (:block/properties block))))
+               blocks (db/get-block-and-children repo block-uuid {:include-property-block? true})
+               sorted-blocks (if db?
+                               (let [blocks' (rest blocks)]
+                                 (cons
+                                  (-> (first blocks')
+                                      (assoc :logseq.property/used-template (:db/id block)))
+                                  (rest blocks')))
+                               (cons
+                                (-> (first blocks)
+                                    (update :block/properties-text-values dissoc :template)
+                                    (update :block/properties-order (fn [keys]
+                                                                      (vec (remove #{:template} keys)))))
+                                (rest blocks)))
+               blocks (cond
+                        db?
+                        sorted-blocks
+                        template-including-parent?
+                        sorted-blocks
+                        :else
+                        (drop 1 sorted-blocks))]
+           (when element-id
+             (insert-command! element-id "" format {:end-pattern commands/command-trigger}))
+           (let [exclude-properties [:id :template :template-including-parent]
+                 content-update-fn (fn [content]
+                                     (->> content
+                                          (property-file/remove-property-when-file-based repo format "template")
+                                          (property-file/remove-property-when-file-based repo format "template-including-parent")
+                                          template/resolve-dynamic-template!))
+                 page (if (:block/name block) block
+                          (when target (:block/page (db/entity (:db/id target)))))
+                 blocks' (if (config/db-based-graph?)
+                           blocks
+                           (map (fn [block]
+                                  (paste-block-cleanup repo block page exclude-properties format content-update-fn false))
+                                blocks))
+                 sibling? (:sibling? opts)
+                 sibling?' (cond
+                             (some? sibling?)
+                             sibling?
+
+                             (db/has-children? (:block/uuid target))
+                             false
+
+                             :else
+                             true)]
+             (when (seq blocks')
+               (try
+                 (p/let [result (ui-outliner-tx/transact!
+                                 {:outliner-op :insert-blocks
+                                  :created-from-journal-template? journal?}
+                                 (when-not (string/blank? (state/get-edit-content))
+                                   (save-current-block!))
+                                 (outliner-op/insert-blocks! blocks' target
+                                                             (assoc opts
+                                                                    :sibling? sibling?'
+                                                                    :insert-template? true)))]
+                   (when result (edit-last-block-after-inserted! result)))
+
+                 (catch :default ^js/Error e
+                   (notification/show!
+                    [:p.content
+                     (util/format "Template insert error: %s" (.-message e))]
+                    :error)))))))))))
+
+(defn template-on-chosen-handler
+  [element-id]
+  (fn [template-block]
+    (when-let [db-id (:db/id template-block)]
+      (insert-template! element-id db-id
+                        {:replace-empty-target? true}))))
+
+(defn get-searching-property
+  [input]
+  (let [value (.-value input)
+        pos (util/get-selection-start input)
+        postfix (subs value pos)
+        end-index (when-let [idx (string/index-of postfix gp-property/colons)]
+                    (+ (max 0 (count (subs value 0 pos))) idx))
+        start-index (or (when-let [p (string/last-index-of (subs value 0 pos) "\n")]
+                          (inc p))
+                        0)]
+    {:end-index end-index
+     :searching-property (when (and start-index end-index (>= end-index start-index))
+                           (subs value start-index end-index))}))
+
+(defn property-on-chosen-handler
+  [element-id q]
+  (fn [property]
+    (when-let [input (gdom/getElement element-id)]
+      (let [{:keys [end-index searching-property]} (get-searching-property input)]
+        (cursor/move-cursor-to input (+ end-index 2))
+        (commands/insert! element-id (str (or property q) gp-property/colons " ")
+                          {:last-pattern (str searching-property gp-property/colons)})
+        (state/clear-editor-action!)
+        (js/setTimeout (fn []
+                         (let [pos (let [input (gdom/getElement element-id)]
+                                     (cursor/get-caret-pos input))]
+                           (state/set-editor-action-data! {:property (or property q)
+                                                           :pos pos})
+                           (state/set-editor-action! :property-value-search)))
+                       50)))))
+
+(defn property-value-on-chosen-handler
+  [element-id q]
+  (fn [property-value]
+    (commands/insert! element-id (str gp-property/colons " " (or property-value q))
+                      {:last-pattern (str gp-property/colons " " q)})
+    (state/clear-editor-action!)))
+
+(declare indent-outdent)
+
+(defn- last-top-level-child?
+  [{:keys [id]} block]
+  (when id
+    (when-let [entity (if-let [id' (parse-uuid (str id))]
+                        (db/entity [:block/uuid id'])
+                        (db/get-page id))]
+      (= (:block/uuid entity) (:block/uuid (:block/parent block))))))
+
+(defn insert
+  ([insertion]
+   (insert insertion false))
+  ([insertion auto-complete-enabled?]
+   (when (or auto-complete-enabled?
+             (not (auto-complete?)))
+     (let [^js input (state/get-input)
+           selected-start (util/get-selection-start input)
+           selected-end (util/get-selection-end input)
+           value (.-value input)
+           s1 (subs value 0 selected-start)
+           s2 (subs value selected-end)]
+       (state/set-edit-content! (state/get-edit-input-id)
+                                (str s1 insertion))
+       ;; HACK: save scroll-pos of current pos, then add trailing content
+       ;; This logic is also in commands/simple-insert!
+       (let [scroll-container (util/nearest-scrollable-container input)
+             scroll-pos (.-scrollTop scroll-container)]
+         (state/set-edit-content! (state/get-edit-input-id)
+                                  (str s1 insertion s2))
+         (cursor/move-cursor-to input (+ selected-start (count insertion)))
+         (set! (.-scrollTop scroll-container) scroll-pos))))))
+
+(defn- keydown-new-line
+  "Insert newline to current cursor position"
+  []
+  (insert "\n"))
+
+(declare delete-and-update)
+
+(defn- dwim-in-properties
+  [state]
+  (when-not (auto-complete?)
+    (let [{:keys [block]} (get-state)]
+      (when block
+        (let [input (state/get-input)
+              content (gobj/get input "value")
+              format (get (:block (get-state)) :block/format :markdown)
+              property-key (:raw-content (thingatpt/property-key-at-point input))
+              org? (= format :org)
+              move-to-pos (if org? 2 3)]
+          (if org?
+            (cond
+              (and property-key (not= property-key ""))
+              (case property-key
+                ;; When cursor in "PROPERTIES", add :|: in a new line and move cursor to |
+                "PROPERTIES"
+                (do (cursor/move-cursor-to-line-end input)
+                    (insert "\n:: ")
+                    (cursor/move-cursor-backward input move-to-pos))
+                ;; When cursor in "END", new block (respect the previous enter behavior)
+                "END"
+                (do
+                  (cursor/move-cursor-to-end input)
+                  (save-current-block!)
+                  (insert-new-block! state))
+                ;; cursor in other positions of :ke|y: or ke|y::, move to line end for inserting value.
+                (if (property-file/property-key-exist?-when-file-based format content property-key)
+                  (notification/show!
+                   [:p.content
+                    (util/format "Property key \"%s\" already exists!" property-key)]
+                   :error)
+                  (cursor/move-cursor-to-line-end input)))
+
+              ;; when cursor in empty property key
+              (and property-key (= property-key ""))
+              (do (delete-and-update
+                   input
+                   (cursor/line-beginning-pos input)
+                   (inc (cursor/line-end-pos input)))
+                  (property-file/goto-properties-end-when-file-based format input)
+                  (cursor/move-cursor-to-line-end input))
+              :else
+              ;;When cursor in other place of PROPERTIES drawer, add :|: in a new line and move cursor to |
+              (do
+                (insert "\n:: ")
+                (cursor/move-cursor-backward input move-to-pos)))
+            (insert "\n")))))))
+
+(defn toggle-list-checkbox
+  [{:block/keys [title] :as block} item-content]
+  (let [toggle-fn (fn [m x-mark]
+                    (case (string/lower-case x-mark)
+                      "[ ]" (str "[x] " item-content)
+                      "[x]" (str "[ ] " item-content)
+                      m))
+        pattern (re-pattern (str "(\\[[xX ]\\])\\s+?" (gstring/regExpEscape item-content)))
+        new-content (string/replace-first title pattern toggle-fn)]
+    (save-block-if-changed! block new-content)))
+
+(defn- dwim-in-list
+  []
+  (when-not (auto-complete?)
+    (let [{:keys [block]} (get-state)]
+      (when block
+        (let [input (state/get-input)]
+          (when-let [item (thingatpt/list-item-at-point input)]
+            (let [{:keys [full-content indent bullet checkbox ordered _]} item
+                  next-bullet (if ordered (str (inc bullet) ".") bullet)
+                  checkbox (when checkbox "[ ] ")]
+              (if (and
+                   (= (count full-content)
+                      (+ (if ordered (+ (count (str bullet)) 2) 2) (when checkbox (count checkbox))))
+                   (string/includes? (.-value input) "\n"))
+                (delete-and-update input (cursor/line-beginning-pos input) (cursor/line-end-pos input))
+                (let [start-pos (util/get-selection-start input)
+                      value (.-value input)
+                      before (subs value 0 start-pos)
+                      after (subs value start-pos)
+                      cursor-in-item-content? (and (re-find #"^(\d+){1}\." (last (string/split-lines before)))
+                                                   (not (string/blank? (first (string/split-lines after)))))]
+                  (when-not cursor-in-item-content?
+                    (cursor/move-cursor-to-line-end input)
+                    (insert (str "\n" indent next-bullet " " checkbox)))
+                  (when ordered
+                    (let [value (.-value input)
+                          start-pos (util/get-selection-start input)
+                          after-lists-str (string/trim (subs value start-pos))
+                          after-lists-str (if cursor-in-item-content?
+                                            (str indent next-bullet " " after-lists-str)
+                                            after-lists-str)
+                          lines (string/split-lines after-lists-str)
+                          after-lists-str' (list/re-order-items lines (if cursor-in-item-content? bullet (inc bullet)))
+                          value' (str (subs value 0 start-pos) "\n" after-lists-str')
+                          cursor' (if cursor-in-item-content?
+                                    (inc (count (str (subs value 0 start-pos) indent next-bullet " ")))
+                                    (+ (:end item) (count next-bullet) 2))]
+                      (state/set-edit-content! (state/get-edit-input-id) value')
+                      (cursor/move-cursor-to input cursor'))))))))))))
+
+(defn toggle-page-reference-embed
+  [parent-id]
+  (let [{:keys [block]} (get-state)]
+    (when block
+      (let [input (state/get-input)
+            new-pos (cursor/get-caret-pos input)
+            page-ref-fn (fn [bounds backward-pos]
+                          (commands/simple-insert!
+                           parent-id bounds
+                           {:backward-pos backward-pos
+                            :check-fn (fn [_ _ _]
+                                        (state/set-editor-action-data! {:pos new-pos})
+                                        (commands/handle-step [:editor/search-page]))}))]
+        (state/clear-editor-action!)
+        (let [selection (get-selection-and-format)
+              {:keys [selection-start selection-end selection]} selection]
+          (if selection
+            (do (delete-and-update input selection-start selection-end)
+                (insert (ref/->page-ref selection)))
+            (if-let [embed-ref (thingatpt/embed-macro-at-point input)]
+              (let [{:keys [raw-content start end]} embed-ref]
+                (delete-and-update input start end)
+                (if (= 5 (count raw-content))
+                  (page-ref-fn page-ref/left-and-right-brackets 2)
+                  (insert raw-content)))
+              (if-let [page-ref (thingatpt/page-ref-at-point input)]
+                (let [{:keys [start end full-content raw-content]} page-ref]
+                  (delete-and-update input start end)
+                  (if (= raw-content "")
+                    (page-ref-fn "{{embed [[]]}}" 4)
+                    (insert (util/format "{{embed %s}}" full-content))))
+                (page-ref-fn page-ref/left-and-right-brackets 2)))))))))
+
+(defn toggle-block-reference-embed
+  [parent-id]
+  (let [{:keys [block]} (get-state)]
+    (when block
+      (let [input (state/get-input)
+            new-pos (cursor/get-caret-pos input)
+            block-ref-fn (fn [bounds backward-pos]
+                           (commands/simple-insert!
+                            parent-id bounds
+                            {:backward-pos backward-pos
+                             :check-fn     (fn [_ _ _]
+                                             (state/set-editor-action-data! {:pos new-pos})
+                                             (commands/handle-step [:editor/search-block]))}))]
+        (state/clear-editor-action!)
+        (if-let [embed-ref (thingatpt/embed-macro-at-point input)]
+          (let [{:keys [raw-content start end]} embed-ref]
+            (delete-and-update input start end)
+            (if (= 5 (count raw-content))
+              (block-ref-fn block-ref/left-and-right-parens 2)
+              (insert raw-content)))
+          (if-let [page-ref (thingatpt/block-ref-at-point input)]
+            (let [{:keys [start end full-content raw-content]} page-ref]
+              (delete-and-update input start end)
+              (if (= raw-content "")
+                (block-ref-fn "{{embed (())}}" 4)
+                (insert (util/format "{{embed %s}}" full-content))))
+            (block-ref-fn block-ref/left-and-right-parens 2)))))))
+
+(defn- keydown-new-block
+  [state]
+  (when-not (auto-complete?)
+    (let [{:keys [block config]} (get-state)]
+      (when block
+        (let [block (db/entity (:db/id block))
+              input (state/get-input)
+              config (assoc config :keydown-new-block true)
+              content (gobj/get input "value")
+              pos (cursor/pos input)
+              has-right? (ldb/get-right-sibling block)
+              db-based? (config/db-based-graph? (state/get-current-repo))
+              thing-at-point ;intern is not supported in cljs, need a more elegant solution
+              (or (when (thingatpt/get-setting :admonition&src?)
+                    (thingatpt/admonition&src-at-point input))
+                  (when (thingatpt/get-setting :markup?)
+                    (thingatpt/markup-at-point input))
+                  (when (thingatpt/get-setting :block-ref?)
+                    (thingatpt/block-ref-at-point input))
+                  (when (thingatpt/get-setting :page-ref?)
+                    (thingatpt/page-ref-at-point input))
+                  (when (and (not db-based?) (thingatpt/get-setting :properties?))
+                    (thingatpt/properties-at-point input))
+                  (when (thingatpt/get-setting :list?)
+                    (and (not (cursor/beginning-of-line? input))
+                         (thingatpt/list-item-at-point input))))]
+          (cond
+            thing-at-point
+            (case (:type thing-at-point)
+              "markup" (let [right-bound (:bounds thing-at-point)]
+                         (cursor/move-cursor-to
+                          input
+                          (+ (string/index-of content right-bound pos)
+                             (count right-bound))))
+              "admonition-block" (keydown-new-line)
+              "source-block" (do
+                               (keydown-new-line)
+                               (case (:action thing-at-point)
+                                 :into-code-editor
+                                 (state/into-code-editor-mode!)
+                                 nil))
+              "block-ref" (open-block-in-sidebar! (:link thing-at-point))
+              "page-ref" (when-not (string/blank? (:link thing-at-point))
+                           (let [page (:link thing-at-point)
+                                 page-name (db-model/get-redirect-page-name page)]
+                             (insert-first-page-block-if-not-exists! page-name)))
+              "list-item" (dwim-in-list)
+              "properties-drawer" (dwim-in-properties state))
+
+            (and (string/blank? content)
+                 (own-order-number-list? block)
+                 (not (some-> (db-model/get-block-parent (:block/uuid block))
+                              (own-order-number-list?))))
+            (remove-block-own-order-list-type! block)
+
+            (and
+             (string/blank? content)
+             (not has-right?)
+             (not (last-top-level-child? config block)))
+            (indent-outdent false)
+
+            :else
+            (insert-new-block! state)))))))
+
+(defn- inside-of-single-block
+  "When we are in a single block wrapper, we should always insert a new line instead of new block"
+  [el]
+  (some? (dom/closest el ".single-block")))
+
+(defn- inside-of-editor-block
+  [el]
+  (some? (dom/closest el ".block-editor")))
+
+(defn keydown-new-block-handler [^js e]
+  (let [state (get-state)]
+    (when (or (nil? (.-target e)) (inside-of-editor-block (.-target e)))
+      (if (or (state/doc-mode-enter-for-new-line?) (inside-of-single-block (rum/dom-node state)))
+        (keydown-new-line)
+        (do
+          (.preventDefault e)
+          (keydown-new-block state))))))
+
+(defn keydown-new-line-handler [e]
+  (let [state (get-state)]
+    (when (or (nil? (.-target e)) (inside-of-editor-block (.-target e)))
+      (if (and (state/doc-mode-enter-for-new-line?) (not (inside-of-single-block (rum/dom-node state))))
+        (keydown-new-block state)
+        (do
+          (.preventDefault e)
+          (keydown-new-line))))))
+
+(defn- select-first-last
+  "Select first or last block in viewport"
+  [direction]
+  (let [f (case direction :up last :down first)
+        container (if (some-> js/document.activeElement
+                              (.querySelector ".blocks-container"))
+                    js/document.activeElement js/document.body)
+        block (->> (util/get-blocks-noncollapse container)
+                   (f))]
+    (when block
+      (util/scroll-to-block block)
+      (state/exit-editing-and-set-selected-blocks! [block]))))
+
+(defn- select-up-down [direction]
+  (let [selected-blocks (state/get-selection-blocks)
+        selected (case direction
+                   :up (first selected-blocks)
+                   :down (last selected-blocks))
+        f (case direction
+            :up util/get-prev-block-non-collapsed
+            :down util/get-next-block-non-collapsed)
+        sibling-block (f selected {:up-down? true
+                                   :exclude-property? true})]
+    (when (and sibling-block (dom/attr sibling-block "blockid"))
+      (util/scroll-to-block sibling-block)
+      (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
+
+(defn- active-jtrigger?
+  []
+  (some-> js/document.activeElement (dom/has-class? "jtrigger")))
+
+(defn- property-value-node?
+  [node]
+  (some-> node (dom/has-class? "property-value-container")))
+
+(defn- focus-trigger
+  [_current-block sibling-block]
+  (when-let [trigger (first (dom/by-class sibling-block "jtrigger"))]
+    (state/clear-edit!)
+    (if (or (dom/has-class? trigger "ls-number")
+            (dom/has-class? trigger "ls-empty-text-property"))
+      (.click trigger)
+      (.focus trigger))))
+
+(defn move-cross-boundary-up-down
+  [direction move-opts]
+  (let [input (or (:input move-opts) (state/get-input))
+        active-element js/document.activeElement
+        input-or-active-element (or input active-element)]
+    (when input-or-active-element
+      (let [repo (state/get-current-repo)
+            f (case direction
+                :up util/get-prev-block-non-collapsed
+                :down util/get-next-block-non-collapsed)
+            current-block (util/rec-get-node input-or-active-element "ls-block")
+            sibling-block (f current-block {:up-down? true})
+            {:block/keys [uuid title format]} (state/get-edit-block)
+            format (or format :markdown)
+            sibling-block (or (when (property-value-node? sibling-block)
+                                (first (dom/by-class sibling-block "ls-block")))
+                              sibling-block)
+            property-value-container? (property-value-node? sibling-block)]
+        (if sibling-block
+          (let [sibling-block-id (dom/attr sibling-block "blockid")
+                container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
+                value (state/get-edit-content)]
+            (p/do!
+             (when (and
+                    uuid
+                    (not (state/block-component-editing?))
+                    (not= (clean-content! repo format title)
+                          (string/trim value)))
+               (save-block! repo uuid value))
+
+             (if property-value-container?
+               (focus-trigger current-block sibling-block)
+               (let [new-uuid (cljs.core/uuid sibling-block-id)
+                     block (db/entity [:block/uuid new-uuid])]
+                 (edit-block! block
+                              (or (:pos move-opts)
+                                  (when input [direction (util/get-line-pos (.-value input) (util/get-selection-start input))])
+                                  0)
+                              {:container-id container-id
+                               :direction direction})))))
+          (case direction
+            :up (cursor/move-cursor-to input 0)
+            :down (cursor/move-cursor-to-end input)))))))
+
+(defn keydown-up-down-handler
+  [direction {:keys [_pos] :as move-opts}]
+  (let [input (state/get-input)
+        selected-start (util/get-selection-start input)
+        selected-end (util/get-selection-end input)
+        up? (= direction :up)
+        down? (= direction :down)]
+    (cond
+      (active-jtrigger?)
+      (move-cross-boundary-up-down direction move-opts)
+
+      (not= selected-start selected-end)
+      (if up?
+        (cursor/move-cursor-to input selected-start)
+        (cursor/move-cursor-to input selected-end))
+
+      (and input
+           (or (and up? (cursor/textarea-cursor-first-row? input))
+               (and down? (cursor/textarea-cursor-last-row? input))))
+      (move-cross-boundary-up-down direction move-opts)
+
+      :else
+      (when input
+        (if up?
+          (cursor/move-cursor-up input)
+          (cursor/move-cursor-down input))))))
+
+(defn move-to-block-when-cross-boundary
+  [direction {:keys [block]}]
+  (let [up? (= :left direction)
+        pos (if up? :max 0)
+        {:block/keys [format uuid] :as block} (or block (state/get-edit-block))
+        format (or format :markdown)
+        repo (state/get-current-repo)
+        editing-block (gdom/getElement (state/get-editing-block-dom-id))
+        f (if up? util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
+        sibling-block (f editing-block)
+        sibling-block (or (when (and sibling-block (property-value-node? sibling-block))
+                            (if (and up? editing-block (gdom/contains sibling-block editing-block))
+                              (f sibling-block)
+                              (first (dom/by-class sibling-block "ls-block"))))
+                          sibling-block)]
+    (when sibling-block
+      (if-let [sibling-block-id (dom/attr sibling-block "blockid")]
+        (do
+          (let [content (:block/title block)
+                value (state/get-edit-content)]
+            (when (and value (not= (clean-content! repo format content) (string/trim value)))
+              (save-block! repo uuid value)))
+
+          (let [container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
+                block (db/entity repo [:block/uuid (cljs.core/uuid sibling-block-id)])]
+            (edit-block! block pos {:container-id container-id})))
+        (when (property-value-node? sibling-block)
+          (focus-trigger editing-block sibling-block))))))
+
+(defn keydown-arrow-handler
+  [direction]
+  (let [input (state/get-input)
+        element js/document.activeElement
+        selected-start (util/get-selection-start input)
+        selected-end (util/get-selection-end input)
+        left? (= direction :left)
+        right? (= direction :right)
+        block (some-> (state/get-edit-block) :db/id db/entity)
+        property? (ldb/property? block)]
+    (cond
+      (and input (not= input element))
+      (.focus input)
+
+      (= input element)
+      (cond
+        (and property? right? (not (cursor/end? input)))
+        (cursor/move-cursor-to-end input)
+
+        (and property? left? (not (cursor/start? input)))
+        (cursor/move-cursor-to-start input)
+
+        (and property? right? (cursor/end? input)
+             (or (not= (:logseq.property/type block) :default)
+                 (seq (:property/closed-values block))))
+        (let [pair (util/rec-get-node input "property-pair")
+              jtrigger (when pair (dom/sel1 pair ".property-value-container .jtrigger"))]
+          (when jtrigger
+            (.focus jtrigger)))
+
+        (not= selected-start selected-end)
+        (cond
+          left?
+          (cursor/move-cursor-to input selected-start)
+          :else
+          (cursor/move-cursor-to input selected-end))
+
+        (or (and left? (cursor/start? input))
+            (and right? (cursor/end? input)))
+        (move-to-block-when-cross-boundary direction {})
+
+        :else
+        (if left?
+          (cursor/move-cursor-backward input)
+          (cursor/move-cursor-forward input)))
+
+      :else
+      nil)))
+
+(defn- delete-and-update [^js input start end]
+  (util/safe-set-range-text! input "" start end)
+  (state/set-edit-content! (state/get-edit-input-id) (.-value input)))
+
+(defn- delete-concat [current-block]
+  (p/let [repo (state/get-current-repo)
+          collapsed? (util/collapsed? current-block)
+          next-block (when-not collapsed?
+                       (let [db (db/get-db repo)]
+                         (when-let [e (or
+                                       ;; first child or next sibling
+                                       (ldb/get-first-child db (:db/id current-block))
+                                       (db-model/get-next db (:db/id current-block)))]
+                           (db/entity (:db/id e)))))]
+    (cond
+      collapsed?
+      nil
+
+      (nil? next-block)
+      nil
+
+      :else
+      (let [repo (state/get-current-repo)
+            editor-state (assoc (get-state)
+                                :block-id (:block/uuid next-block)
+                                :value (:block/title next-block))]
+        (delete-block-inner! repo editor-state)))))
+
+(defn keydown-delete-handler
+  [_e]
+  (let [^js input (state/get-input)
+        current-pos (cursor/pos input)
+        value (gobj/get input "value")
+        end? (= current-pos (count value))
+        current-block (state/get-edit-block)
+        selected-start (util/get-selection-start input)
+        selected-end (util/get-selection-end input)]
+    (when current-block
+      (cond
+        (not= selected-start selected-end)
+        (delete-and-update input selected-start selected-end)
+
+        (and end? current-block)
+        (let [editor-state (get-state)
+              custom-query? (get-in editor-state [:config :custom-query?])]
+          (when-not custom-query?
+            (delete-concat current-block)))
+
+        :else
+        (delete-and-update
+         input current-pos (util/safe-inc-current-pos-from-start (.-value input) current-pos))))))
+
+(defn keydown-backspace-handler
+  [cut? e]
+  (let [^js input (state/get-input)
+        element js/document.activeElement]
+    (if (= input element)
+      (let [id (state/get-edit-input-id)
+            current-pos (cursor/pos input)
+            value (gobj/get input "value")
+            deleted (and (> current-pos 0)
+                         (util/nth-safe value (dec current-pos)))
+            selected-start (util/get-selection-start input)
+            selected-end (util/get-selection-end input)
+            block (state/get-edit-block)
+            block (db/entity (:db/id block))
+            repo (state/get-current-repo)
+            top-block? (= (:db/id (or (ldb/get-left-sibling block) (:block/parent block)))
+                          (:db/id (:block/page block)))
+            single-block? (inside-of-single-block (.-target e))
+            root-block? (= (:block.temp/container block) (str (:block/uuid block)))]
+        (block-handler/mark-last-input-time! repo)
+        (cond
+          (not= selected-start selected-end)
+          (do
+            (util/stop e)
+            (when cut?
+              (js/document.execCommand "copy"))
+            (delete-and-update input selected-start selected-end))
+
+          (zero? current-pos)
+          (let [editor-state (get-state)
+                custom-query? (get-in editor-state [:config :custom-query?])]
+            (util/stop e)
+            (when (and (not (and top-block? (not (string/blank? value))))
+                       (not root-block?)
+                       (not single-block?)
+                       (not custom-query?))
+              (if (own-order-number-list? block)
+                (p/do!
+                 (save-current-block!)
+                 (remove-block-own-order-list-type! block))
+                (delete-block! repo))))
+
+          (and (> current-pos 0)
+               (contains? #{commands/command-trigger commands/command-ask}
+                          (util/nth-safe value (dec current-pos))))
+          (do
+            (util/stop e)
+            (commands/restore-state)
+            (delete-and-update input (dec current-pos) current-pos))
+
+          ;; pair
+          (and
+           deleted
+           (contains?
+            (set (keys delete-map))
+            deleted)
+           (>= (count value) (inc current-pos))
+           (= (util/nth-safe value current-pos)
+              (get delete-map deleted)))
+
+          (do
+            (util/stop e)
+            (commands/delete-pair! id)
+            (cond
+              (and (= deleted "[") (state/get-editor-show-page-search?))
+              (state/clear-editor-action!)
+
+              (and (= deleted "(") (state/get-editor-show-block-search?))
+              (state/clear-editor-action!)
+
+              :else
+              nil))
+
+          ;; deleting hashtag
+          (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
+          (do
+            (state/clear-editor-action!)
+            (delete-and-update input (dec current-pos) current-pos))
+
+          ;; just delete
+          :else
+          (when (and input (not (mobile-util/native-ios?)))
+            (util/stop e)
+            (delete-and-update
+             input (util/safe-dec-current-pos-from-end (.-value input) current-pos) current-pos))))
+      false)))
+
+(defn indent-outdent
+  [indent?]
+  (let [{:keys [block block-container]} (get-state)]
+    (when block
+      (let [node block-container
+            prev-container-id (get-node-container-id node)
+            container-id (get-new-container-id (if indent? :indent :outdent) {})]
+        (p/do!
+         (block-handler/indent-outdent-blocks! [block] indent? save-current-block!)
+         (when (and (not= prev-container-id container-id) container-id)
+           (state/set-editing-block-id! [container-id (:block/uuid block)])))))))
+
+(defn keydown-tab-handler
+  [direction]
+  (fn [e]
+    (cond
+      (state/editing?)
+      (when-not (state/get-editor-action)
+        (util/stop e)
+        (indent-outdent (not (= :left direction))))
+
+      (state/selection?)
+      (do
+        (util/stop e)
+        (state/pub-event! [:editor/hide-action-bar])
+        (on-tab direction)))
+    nil))
+
+(defn- double-chars-typed?
+  [value pos key sym]
+  (and (= key sym)
+       (>= (count value) 1)
+       (> pos 0)
+       (= (nth value (dec pos)) sym)
+       (if (> (count value) pos)
+         (not= (nth value pos) sym)
+         true)))
+
+(defn ^:large-vars/cleanup-todo keydown-not-matched-handler
+  "NOTE: Keydown cannot be used on Android platform"
+  [format]
+  (fn [e _key-code]
+    (let [input-id (state/get-edit-input-id)
+          input (state/get-input)
+          key (gobj/get e "key")
+          value (gobj/get input "value")
+          ctrlKey (gobj/get e "ctrlKey")
+          metaKey (gobj/get e "metaKey")
+          pos (cursor/pos input)
+          hashtag? (or (surround-by? input "#" " ")
+                       (surround-by? input "#" :end)
+                       (= key "#"))
+          db-based? (config/db-based-graph? (state/get-current-repo))]
+      (when (or (not @(:editor/start-pos @state/state))
+                (and key (string/starts-with? key "Arrow")))
+        (state/set-state! :editor/start-pos pos))
+
+      (cond
+        (and (= :page-search (state/get-editor-action))
+             (= key commands/hashtag))
+        (do
+          (util/stop e)
+          (notification/show! "Page name can't include \"#\"." :warning))
+        ;; stop accepting edits if the new block is not created yet
+        (some? @(:editor/async-unsaved-chars @state/state))
+        (do
+          (when (= 1 (count (str key)))
+            (state/update-state! :editor/async-unsaved-chars
+                                 (fn [s]
+                                   (str s key))))
+          (util/stop e))
+
+        (and (contains? #{"ArrowLeft" "ArrowRight"} key)
+             (contains? #{:property-search :property-value-search} (state/get-editor-action)))
+        (state/clear-editor-action!)
+
+        (and (util/goog-event-is-composing? e true) ;; #3218
+             (not hashtag?) ;; #3283 @Rime
+             (not (state/get-editor-show-page-search-hashtag?))) ;; #3283 @MacOS pinyin
+        nil
+
+        (or ctrlKey metaKey)
+        nil
+
+        ;; FIXME: On mobile, a backspace click to call keydown-backspace-handler
+        ;; does not work if cursor is at the beginning of a block, hence the block
+        ;; can't be deleted. Need to figure out why and find a better solution.
+        (and (mobile-util/native-platform?)
+             (= key "Backspace")
+             (zero? pos)
+             (string/blank? (.toString (js/window.getSelection))))
+        (keydown-backspace-handler false e)
+
+        (and (= key "#")
+             (> pos 0)
+             (= "#" (util/nth-safe value (dec pos))))
+        (state/clear-editor-action!)
+
+        (and (contains? (set/difference (set (keys reversed-autopair-map))
+                                        #{"`"})
+                        key)
+             (= (get-current-input-char input) key))
+        (do
+          (util/stop e)
+          (cursor/move-cursor-forward input))
+
+        (and (autopair-when-selected key) (string/blank? (util/get-selected-text)))
+        nil
+
+        (some? @(:editor/action @state/state))
+        nil
+
+        (and (not (string/blank? (util/get-selected-text)))
+             (contains? keycode/left-square-brackets-keys key))
+        (do
+          (autopair input-id "[" format nil)
+          (util/stop e))
+
+        (and (not (string/blank? (util/get-selected-text)))
+             (contains? keycode/left-paren-keys key))
+        (do (util/stop e)
+            (autopair input-id "(" format nil))
+
+          ;; If you type `xyz`, the last backtick should close the first and not add another autopair
+          ;; If you type several backticks in a row, each one should autopair to accommodate multiline code (```)
+        (-> (keys autopair-map)
+            set
+            (disj "(")
+            (contains? key)
+            (or (autopair-left-paren? input key)))
+        (let [curr (get-current-input-char input)
+              prev (util/nth-safe value (dec pos))]
+          (util/stop e)
+          (if (and (= key "`") (= "`" curr) (not= "`" prev))
+            (cursor/move-cursor-forward input)
+            (autopair input-id key format nil)))
+
+        ; `;;` to add or change property for db graphs
+        (let [sym ";"]
+          (and db-based? (double-chars-typed? value pos key sym)))
+        (state/pub-event! [:editor/new-property])
+
+        (let [sym "$"]
+          (double-chars-typed? value pos key sym))
+        (commands/simple-insert! input-id "$$" {:backward-pos 2})
+
+        (let [sym "^"]
+          (double-chars-typed? value pos key sym))
+        (commands/simple-insert! input-id "^^" {:backward-pos 2})
+
+        :else
+        nil))))
+
+(defn- input-page-ref?
+  [k current-pos blank-selected? last-key-code]
+  (and blank-selected?
+       (contains? keycode/left-square-brackets-keys k)
+       (= (:key last-key-code) k)
+       (> current-pos 0)))
+
+(defn- default-case-for-keyup-handler
+  [input current-pos k code is-processed?]
+  (let [last-key-code (state/get-last-key-code)
+        blank-selected? (string/blank? (util/get-selected-text))
+        non-enter-processed? (and is-processed? ;; #3251
+                                  (not= code keycode/enter-code))  ;; #3459
+        editor-action (state/get-editor-action)]
+    (if (and (= editor-action :page-search-hashtag)
+             (input-page-ref? k current-pos blank-selected? last-key-code))
+      (do
+        (commands/handle-step [:editor/input page-ref/right-brackets {:last-pattern :skip-check
+                                                                      :backward-pos 2}])
+        (commands/handle-step [:editor/search-page])
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+      (when (and (not editor-action) (not non-enter-processed?))
+        (cond
+         ;; When you type text inside square brackets
+          (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp" "Escape"} k))
+               (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
+          (let [orig-pos (cursor/get-caret-pos input)
+                value (gobj/get input "value")
+                square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) page-ref/left-brackets)
+                pos (+ square-pos 2)
+                _ (state/set-editor-last-pos! pos)
+                pos (assoc orig-pos :pos pos)
+                command-step (if (= \# (util/nth-safe value (dec square-pos)))
+                               :editor/search-page-hashtag
+                               :editor/search-page)]
+            (commands/handle-step [command-step])
+            (state/set-editor-action-data! {:pos pos}))
+
+         ;; Handle non-ascii square brackets
+          (and (input-page-ref? k current-pos blank-selected? last-key-code)
+               (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets)))
+          (do
+            (commands/handle-step [:editor/input page-ref/left-and-right-brackets {:backward-truncate-number 2
+                                                                                   :backward-pos 2}])
+            (commands/handle-step [:editor/search-page])
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+         ;; Handle non-ascii parentheses
+          (and blank-selected?
+               (contains? keycode/left-paren-keys k)
+               (= (:key last-key-code) k)
+               (> current-pos 0)
+               (not (wrapped-by? input block-ref/left-parens block-ref/right-parens)))
+          (do
+            (commands/handle-step [:editor/input block-ref/left-and-right-parens {:backward-truncate-number 2
+                                                                                  :backward-pos 2}])
+            (commands/handle-step [:editor/search-block :reference])
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+          :else
+          nil)))))
+
+(defn keyup-handler
+  [_state input]
+  (fn [e key-code]
+    (when-not (util/goog-event-is-composing? e)
+      (let [db-based? (config/db-based-graph?)
+            current-pos (cursor/pos input)
+            value (gobj/get input "value")
+            c (util/nth-safe value (dec current-pos))
+            [key-code k code is-processed?]
+            (if (and c
+                     (mobile-util/native-android?)
+                     (or (= key-code 229)
+                         (= key-code 0)))
+              [(.charCodeAt value (dec current-pos))
+               c
+               (cond
+                 (= c " ")
+                 "Space"
+
+                 (parse-long c)
+                 (str "Digit" c)
+
+                 :else
+                 (str "Key" (string/upper-case c)))
+               false]
+              [key-code
+               (gobj/get e "key")
+               (if (mobile-util/native-android?)
+                 (gobj/get e "key")
+                 (gobj/getValueByKeys e "event_" "code"))
+                ;; #3440
+               (util/goog-event-is-composing? e true)])]
+        (cond
+          (and db-based? (= value "``````")) ; turn this block into a code block
+          (do
+            (state/set-edit-content! (.-id input) "")
+            (state/pub-event! [:editor/upsert-type-block {:block (assoc (state/get-edit-block) :block/title "")
+                                                          :type :code
+                                                          :update-current-block? true}]))
+
+          (and db-based? (= value ">")) ; turn this block into a quote block
+          (do
+            (state/set-edit-content! (.-id input) "")
+            (state/pub-event! [:editor/upsert-type-block {:block (assoc (state/get-edit-block) :block/title "")
+                                                          :type :quote
+                                                          :update-current-block? true}]))
+
+          ;; When you type something after /
+          (and (= :commands (state/get-editor-action)) (not= k commands/command-trigger))
+          (if (= commands/command-trigger (second (re-find #"(\S+)\s+$" value)))
+            (state/clear-editor-action!)
+            (let [command (get-last-command input)
+                  matched-commands (get-matched-commands command)]
+              (if (seq matched-commands)
+                (commands/set-matched-commands! command matched-commands)
+                (if (> (- (count command) (count @commands/*latest-matched-command)) 2)
+                  (state/clear-editor-action!)
+                  (reset! commands/*matched-commands nil)))))
+
+          :else
+          (default-case-for-keyup-handler input current-pos k code is-processed?))
+
+        (close-autocomplete-if-outside input)
+
+        (when-not (or (= k "Shift") is-processed?)
+          (state/set-last-key-code! {:key-code key-code
+                                     :code code
+                                     :key k
+                                     :shift? (.-shiftKey e)}))
+        (when-not (state/get-editor-action)
+          (state/set-editor-last-pos! current-pos))))))
+
+(defn editor-on-click!
+  [id]
+  (fn [_e]
+    (let [input (gdom/getElement id)]
+      (util/scroll-editor-cursor input)
+      (close-autocomplete-if-outside input))))
+
+(defn editor-on-change!
+  [block id search-timeout]
+  (fn [e]
+    (if (= :block-search (state/sub :editor/action))
+      (let [timeout 50]
+        (when @search-timeout
+          (js/clearTimeout @search-timeout))
+        (reset! search-timeout
+                (js/setTimeout
+                 #(edit-box-on-change! e block id)
+                 timeout)))
+      (let [input (gdom/getElement id)]
+        (edit-box-on-change! e block id)
+        (util/scroll-editor-cursor input)))))
+
+(defn- cut-blocks-and-clear-selections!
+  [copy?]
+  (when-not (get-in @state/state [:ui/find-in-page :active?])
+    (cut-selection-blocks copy?)
+    (clear-selection!)))
+
+(defn shortcut-copy-selection
+  [_e]
+  (copy-selection-blocks true))
+
+(defn shortcut-cut-selection
+  [e]
+  (when-not (util/input? (.-target e))
+    (util/stop e)
+    (cut-blocks-and-clear-selections! true)))
+
+(defn shortcut-delete-selection
+  [e]
+  (when-not (util/input? (.-target e))
+    (util/stop e)
+    (cut-blocks-and-clear-selections! false)))
+
+(defn- copy-current-block-ref
+  [format]
+  (when-let [current-block (state/get-edit-block)]
+    (when-let [block-id (:block/uuid current-block)]
+      (let [db? (config/db-based-graph? (state/get-current-repo))]
+        (if (= format "embed")
+          (if db?
+            (p/do!
+             (save-current-block!)
+             (util/copy-to-clipboard! (ref/->page-ref block-id)
+                                      {:graph (state/get-current-repo)
+                                       :blocks [{:block/uuid (:block/uuid current-block)}]
+                                       :embed-block? true}))
+            (copy-block-ref! block-id #(str "{{embed ((" % "))}}")))
+          (copy-block-ref! block-id
+                           (if db?
+                             ref/->page-ref
+                             ref/->block-ref)))))))
+
+(defn copy-current-block-embed []
+  (copy-current-block-ref "embed"))
+
+(defn shortcut-copy
+  "shortcut copy action:
+  * when in selection mode, copy selected blocks
+  * when in edit mode but no text selected, copy current block ref
+  * when in edit mode with text selected, copy selected text as normal
+  * when text is selected on a PDF, copy the highlighted text"
+  [e]
+  (when-not (auto-complete?)
+    (cond
+      (state/selection?)
+      (shortcut-copy-selection e)
+
+      (and (state/editing?) (nil? (:editor/code-block-context @state/state)))
+      (let [input (state/get-input)
+            selected-start (util/get-selection-start input)
+            selected-end (util/get-selection-end input)]
+        (save-current-block!)
+        (when (= selected-start selected-end)
+          (copy-current-block-ref "ref")))
+
+      (and (state/get-current-pdf)
+           (.closest (.. js/window getSelection -baseNode -parentElement)  ".pdfViewer"))
+      (util/copy-to-clipboard!
+       (pdf-utils/fix-selection-text-breakline (.. js/window getSelection toString))
+       nil))))
+
+(defn shortcut-copy-text
+  "shortcut copy action:
+  * when in selection mode, copy selected blocks
+  * when in edit mode with text selected, copy selected text as normal"
+  [_e]
+  (when-not (auto-complete?)
+    (cond
+      (state/selection?)
+      (copy-selection-blocks false)
+
+      :else
+      (js/document.execCommand "copy"))))
+
+(defn whiteboard?
+  []
+  (and (db-model/whiteboard-page? (state/get-current-page))
+       (.closest (.-activeElement js/document) ".logseq-tldraw")))
+
+(defn shortcut-cut
+  "shortcut cut action:
+  * when in selection mode, cut selected blocks
+  * when in edit mode with text selected, cut selected text
+  * otherwise nothing need to be handled."
+  [e]
+  (cond
+    (state/selection?)
+    (shortcut-cut-selection e)
+
+    (and (state/editing?) (util/input-text-selected?
+                           (gdom/getElement (state/get-edit-input-id))))
+    (keydown-backspace-handler true e)
+
+    (whiteboard?)
+    (.cut (state/active-tldraw-app))
+
+    :else
+    nil))
+
+(defn delete-selection
+  [e]
+  (cond
+    (state/selection?)
+    (shortcut-delete-selection e)
+
+    (and (whiteboard?) (not (state/editing?)))
+    (.deleteShapes (.-api ^js (state/active-tldraw-app)))
+
+    :else
+    nil))
+
+(defn editor-delete
+  [e]
+  (when (state/editing?)
+    (util/stop e)
+    (keydown-delete-handler e)))
+
+(defn editor-backspace
+  [e]
+  (when (state/editing?)
+    (keydown-backspace-handler false e)))
+
+(defn- in-page-preview?
+  []
+  (some-> js/document.activeElement
+          (.closest ".ls-preview-popup")
+          (nil?) (not)))
+
+(defn shortcut-up-down [direction]
+  (fn [e]
+    (state/pub-event! [:editor/hide-action-bar])
+    (when (and (not (auto-complete?))
+               (or (in-page-preview?)
+                   (not (in-shui-popup?)))
+               (not (state/get-timestamp-block)))
+      (util/stop e)
+      (cond
+        (or (state/editing?) (active-jtrigger?))
+        (keydown-up-down-handler direction {})
+
+        (state/selection?)
+        (select-up-down direction)
+
+        ;; if there is an edit-input-id set, we are probably still on editing mode,
+        ;; that is not fully initialized
+        (not (state/get-edit-input-id))
+        (select-first-last direction)))
+    nil))
+
+(defn shortcut-select-up-down [direction]
+  (fn [e]
+    (util/stop e)
+    (if (state/editing?)
+      (let [input (state/get-input)
+            selected-start (util/get-selection-start input)
+            selected-end (util/get-selection-end input)
+            [anchor cursor] (case (util/get-selection-direction input)
+                              "backward" [selected-end selected-start]
+                              [selected-start selected-end])
+            cursor-rect (cursor/get-caret-pos input cursor)]
+        (if
+          ;; if the move is to cross block boundary, select the whole block
+         (or (and (= direction :up) (cursor/textarea-cursor-rect-first-row? cursor-rect))
+             (and (= direction :down) (cursor/textarea-cursor-rect-last-row? cursor-rect)))
+          (select-block-up-down direction)
+          ;; simulate text selection
+          (cursor/select-up-down input direction anchor cursor-rect)))
+      (select-block-up-down direction))))
+
+(defn popup-exists?
+  [id]
+  (some->> (shui-popup/get-popups)
+           (some #(some-> % (:id) (str) (string/includes? (str id))))))
+
+(defn editor-commands-popup-exists?
+  []
+  (popup-exists? "editor.commands"))
+
+(defn open-selected-block!
+  [direction e]
+  (let [selected-blocks (state/get-selection-blocks)
+        f (case direction :left first :right last)
+        node (some-> selected-blocks f)]
+    (when-let [block-id (some-> node (dom/attr "blockid") uuid)]
+      (util/stop e)
+      (let [block {:block/uuid block-id}
+            left? (= direction :left)
+            opts {:container-id (some-> node (dom/attr "containerid") (parse-long))
+                  :event e}]
+        (edit-block! block (if left? 0 :max) opts)))))
+
+(defn shortcut-left-right [direction]
+  (fn [e]
+    (when (and (not (auto-complete?))
+               (not (state/get-timestamp-block)))
+      (cond
+        (state/editing?)
+        (do
+          (util/stop e)
+          (keydown-arrow-handler direction))
+
+        (state/selection?)
+        (do
+          (util/stop e)
+          (open-selected-block! direction e))
+
+        :else
+        nil))))
+
+(defn clear-block-content! []
+  (save-current-block! {:force? true})
+  (state/set-edit-content! (state/get-edit-input-id) ""))
+
+(defn kill-line-before! []
+  (save-current-block! {:force? true})
+  (util/kill-line-before! (state/get-input)))
+
+(defn kill-line-after! []
+  (save-current-block! {:force? true})
+  (util/kill-line-after! (state/get-input)))
+
+(defn beginning-of-block []
+  (cursor/move-cursor-to (state/get-input) 0))
+
+(defn end-of-block []
+  (cursor/move-cursor-to-end (state/get-input)))
+
+(defn cursor-forward-word []
+  (cursor/move-cursor-forward-by-word (state/get-input)))
+
+(defn cursor-backward-word []
+  (cursor/move-cursor-backward-by-word (state/get-input)))
+
+(defn backward-kill-word []
+  (let [input (state/get-input)]
+    (save-current-block! {:force? true})
+    (util/backward-kill-word input)
+    (state/set-edit-content! (state/get-edit-input-id) (.-value input))))
+
+(defn forward-kill-word []
+  (let [input (state/get-input)]
+    (save-current-block! {:force? true})
+    (util/forward-kill-word input)
+    (state/set-edit-content! (state/get-edit-input-id) (.-value input))))
+
+(defn block-with-title?
+  [format content semantic?]
+  (and (string/includes? content "\n")
+       (if semantic?
+         (let [ast (mldoc/->edn content format)
+               first-elem-type (first (ffirst ast))]
+           (mldoc/block-with-title? first-elem-type))
+         true)))
+
+(defn- db-collapsable?
+  [block]
+  (let [class-properties (:classes-properties (outliner-property/get-block-classes-properties (db/get-db) (:db/id block)))
+        db (db/get-db)
+        attributes (set (remove #{:block/alias} db-property/db-attribute-properties))
+        properties (->> (:block.temp/property-keys block)
+                        (map (partial entity-plus/entity-memoized db))
+                        (concat class-properties)
+                        (remove (fn [e] (attributes (:db/ident e))))
+                        (remove outliner-property/property-with-other-position?)
+                        (remove (fn [e] (:logseq.property/hide? e)))
+                        (remove nil?))]
+    (or (seq properties)
+        (ldb/class-instance? (entity-plus/entity-memoized db :logseq.class/Query) block))))
+
+(defn collapsable?
+  ([block-id]
+   (collapsable? block-id {}))
+  ([block-id {:keys [semantic? ignore-children?]
+              :or {semantic? false
+                   ignore-children? false}}]
+   (when block-id
+     (let [repo (state/get-current-repo)]
+       (if-let [block (db/entity [:block/uuid block-id])]
+         (let [db-based? (config/db-based-graph? repo)]
+           (or (if ignore-children? false (db-model/has-children? block-id))
+               (and db-based? (db-collapsable? block))
+               (and (not db-based?)
+                    (or (file-editor-handler/valid-dsl-query-block? block)
+                        (file-editor-handler/valid-custom-query-block? block)))
+               (and
+                (:outliner/block-title-collapse-enabled? (state/get-config))
+                (block-with-title? (get block :block/format :markdown)
+                                   (:block/title block)
+                                   semantic?))))
+         false)))))
+
+(defn <all-blocks-with-level
+  "Return all blocks associated with correct level
+   if :root-block is not nil, only return root block with its children
+   if :expanded? true, return expanded children
+   if :collapse? true, return without any collapsed children
+   if :incremental? true, collapse/expand will be step by step
+   for example:
+   - a
+    - b (collapsed)
+     - c
+     - d
+    - e
+   return:
+    blocks
+    [{:block a :level 1}
+     {:block b :level 2}
+     {:block e :level 2}]"
+  [{:keys [collapse? expanded? incremental? root-block page]
+    :or {collapse? false expanded? false incremental? true root-block nil}}]
+  (when-let [page (or page
+                      (state/get-current-page)
+                      (date/today))]
+    (p/let [block-id (or root-block (parse-uuid page))
+            page-id (when-not block-id
+                      (:db/id (db/get-page page)))
+            repo (state/get-current-repo)
+            result (db-async/<get-block repo (or block-id page-id)
+                                        {:children-only? true
+                                         :nested-children? true})
+            blocks (if page-id
+                     result
+                     (cons (db/entity [:block/uuid block-id]) result))
+            root-block (or block-id root-block)]
+      (if incremental?
+        (let [blocks (tree/blocks->vec-tree blocks (or block-id page-id))]
+          (->>
+           (cond->> blocks
+             root-block
+             (map (fn find [root]
+                    (if (= root-block (:block/uuid root))
+                      root
+                      (first (filter find (:block/children root []))))))
+
+             collapse?
+             (w/postwalk
+              (fn [b]
+                (if (and (map? b)
+                         (util/collapsed? b)
+                         (not= root-block (:block/uuid b)))
+                  (assoc b :block/children []) b)))
+
+             true
+             (mapcat (fn [x] (tree-seq map? :block/children x)))
+
+             expanded?
+             (filter (fn [b] (collapsable? (:block/uuid b))))
+
+             true
+             (map (fn [x] (dissoc x :block/children))))
+           (remove nil?)))
+
+        (cond->> blocks
+          collapse?
+          (filter util/collapsed?)
+
+          expanded?
+          (filter (fn [b] (collapsable? (:block/uuid b))))
+
+          true
+          (remove nil?))))))
+
+(defn- skip-collapsing-in-db?
+  []
+  (let [config (last (state/get-editor-args))]
+    (:ref? config)))
+
+(defn set-blocks-collapsed!
+  [block-ids value]
+  (let [block-ids (map (fn [block-id] (if (string? block-id) (uuid block-id) block-id)) block-ids)
+        repo (state/get-current-repo)
+        value (boolean value)]
+    (when repo
+      (save-current-block!) ;; Save the input contents before collapsing
+      (ui-outliner-tx/transact! ;; Save the new collapsed state as an undo transaction (if it changed)
+       {:outliner-op :collapse-expand-blocks}
+       (doseq [block-id block-ids]
+         (when-let [block (db/entity [:block/uuid block-id])]
+           (let [current-value (boolean (:block/collapsed? block))]
+             (when-not (= current-value value)
+               (let [block {:block/uuid block-id
+                            :block/collapsed? value}]
+                 (outliner-save-block! block {:outliner-op :collapse-expand-blocks})))))))
+      (doseq [block-id block-ids]
+        (state/set-collapsed-block! block-id value)))))
+
+(defn collapse-block! [block-id]
+  (when (collapsable? block-id)
+    (when-not (skip-collapsing-in-db?)
+      (set-blocks-collapsed! [block-id] true))
+    (state/set-collapsed-block! block-id true)))
+
+(defn expand-block! [block-id & {:keys [skip-db-collpsing?]}]
+  (let [repo (state/get-current-repo)]
+    (p/do!
+     (db-async/<get-block repo block-id {:children-only? true})
+     (when-not (or skip-db-collpsing? (skip-collapsing-in-db?))
+       (set-blocks-collapsed! [block-id] false))
+     (state/set-collapsed-block! block-id false))))
+
+(defn expand!
+  ([e] (expand! e false))
+  ([e clear-selection?]
+   (util/stop e)
+   (cond
+     (state/editing?)
+     (when-let [block-id (:block/uuid (state/get-edit-block))]
+       (expand-block! block-id))
+
+     (state/selection?)
+     (do
+       (->> (get-selected-blocks)
+            (map (fn [dom]
+                   (-> (dom/attr dom "blockid")
+                       uuid
+                       expand-block!)))
+            doall)
+       (and clear-selection? (clear-selection!)))
+
+     (whiteboard?)
+     (.setCollapsed (.-api ^js (state/active-tldraw-app)) false)
+
+     :else
+     ;; expand one level
+     (p/let [blocks-with-level (<all-blocks-with-level {})
+             max-level (or (apply max (map :block/level blocks-with-level)) 99)]
+       (loop [level 1]
+         (if (> level max-level)
+           nil
+           (let [blocks-to-expand (->> blocks-with-level
+                                       (filter (fn [b] (= (:block/level b) level)))
+                                       (filter util/collapsed?))]
+             (if (empty? blocks-to-expand)
+               (recur (inc level))
+               (doseq [{:block/keys [uuid]} blocks-to-expand]
+                 (expand-block! uuid))))))))))
+
+(defn collapse!
+  ([e] (collapse! e false))
+  ([e clear-selection?]
+   (when e (util/stop e))
+   (cond
+     (state/editing?)
+     (when-let [block-id (:block/uuid (state/get-edit-block))]
+       (collapse-block! block-id))
+
+     (state/selection?)
+     (do
+       (->> (get-selected-blocks)
+            (map (fn [dom]
+                   (-> (dom/attr dom "blockid")
+                       uuid
+                       collapse-block!)))
+            doall)
+       (and clear-selection? (clear-selection!)))
+
+     (whiteboard?)
+     (.setCollapsed (.-api ^js (state/active-tldraw-app)) true)
+
+     :else
+     ;; collapse by one level from outside
+     (p/let [blocks-with-level
+             (<all-blocks-with-level {:collapse? true})
+             max-level (or (apply max (map :block/level blocks-with-level)) 99)]
+       (loop [level max-level]
+         (if (zero? level)
+           nil
+           (let [blocks-to-collapse
+                 (->> blocks-with-level
+                      (filter (fn [b] (= (:block/level b) level)))
+                      (filter (fn [b] (collapsable? (:block/uuid b)))))]
+             (if (empty? blocks-to-collapse)
+               (recur (dec level))
+               (doseq [{:block/keys [uuid]} blocks-to-collapse]
+                 (collapse-block! uuid))))))))))
+
+(defn toggle-collapse!
+  ([e] (toggle-collapse! e false))
+  ([e clear-selection?]
+   (when e (util/stop e))
+   (cond
+     (state/editing?)
+     (when-let [block (state/get-edit-block)]
+        ;; get-edit-block doesn't track the latest collapsed state, so we need to reload from db.
+       (let [block-id (:block/uuid block)
+             block (db/entity [:block/uuid block-id])]
+         (if (:block/collapsed? block)
+           (expand! e clear-selection?)
+           (collapse! e clear-selection?))))
+
+     (state/selection?)
+     (do
+       (let [block-ids (map #(-> % (dom/attr "blockid") uuid) (get-selected-blocks))
+             first-block-id (first block-ids)]
+         (when first-block-id
+            ;; If multiple blocks are selected, they may not have all the same collapsed state.
+            ;; For simplicity, use the first block's state to decide whether to collapse/expand all.
+           (let [first-block (db/entity [:block/uuid first-block-id])]
+             (if (:block/collapsed? first-block)
+               (doseq [block-id block-ids] (expand-block! block-id))
+               (doseq [block-id block-ids] (collapse-block! block-id))))))
+       (and clear-selection? (clear-selection!)))
+
+     (whiteboard?)
+      ;; TODO: Looks like detecting the whiteboard selection's collapse state will take more work.
+      ;; Leaving unimplemented for now.
+     nil
+
+     :else
+      ;; If no block is being edited or selected, the "toggle" action doesn't make sense,
+      ;; so we no-op here, unlike in the expand! & collapse! functions.
+     nil)))
+
+(defn collapse-all!
+  ([]
+   (collapse-all! nil {}))
+  ([block-id {:keys [collapse-self?]
+              :or {collapse-self? true}}]
+   (p/let [blocks (<all-blocks-with-level {:incremental? false
+                                           :expanded? true
+                                           :root-block block-id})
+           block-ids (cond->> (mapv :block/uuid blocks)
+                       (not collapse-self?)
+                       (remove #{block-id}))]
+     (set-blocks-collapsed! block-ids true))))
+
+(defn expand-all!
+  ([]
+   (expand-all! nil))
+  ([block-id]
+   (p/let [blocks (<all-blocks-with-level {:incremental? false
+                                           :collapse? true
+                                           :root-block block-id})
+           block-ids (map :block/uuid blocks)]
+     (set-blocks-collapsed! block-ids false))))
+
+(defn collapse-all-selection!
+  []
+  (p/let [blocks (p/all
+                  (map #(<all-blocks-with-level {:incremental? false
+                                                 :expanded? true
+                                                 :root-block %})
+                       (get-selected-toplevel-block-uuids)))
+          block-ids (->> blocks
+                         (map :block/uuid)
+                         distinct)]
+    (set-blocks-collapsed! block-ids true)))
+
+(defn expand-all-selection!
+  []
+  (let [blocks (p/all
+                (map #(<all-blocks-with-level {:incremental? false
+                                               :expanded? true
+                                               :root-block %})
+                     (get-selected-toplevel-block-uuids)))
+        block-ids (->> blocks
+                       (map :block/uuid)
+                       distinct)]
+    (set-blocks-collapsed! block-ids false)))
+
+(defn toggle-open! []
+  (p/let [blocks (<all-blocks-with-level {:incremental? false
+                                          :collapse? true})
+          all-expanded? (empty? blocks)]
+    (if all-expanded?
+      (collapse-all!)
+      (expand-all!))))
+
+(defn toggle-open-block-children! [block-id]
+  (p/let [blocks (<all-blocks-with-level {:incremental? false
+                                          :collapse? true})
+          all-expanded? (empty? blocks)]
+    (if all-expanded?
+      (collapse-all! block-id {:collapse-self? false})
+      (expand-all! block-id))))
+
+(defn select-all-blocks!
+  [{:keys [page]}]
+  (p/do!
+   (if-let [current-input-id (state/get-edit-input-id)]
+     (let [input (gdom/getElement current-input-id)
+           blocks-container (util/rec-get-blocks-container input)
+           blocks (dom/by-class blocks-container "ls-block")]
+       (state/exit-editing-and-set-selected-blocks! blocks))
+     (p/let [blocks (<all-blocks-with-level {:page page
+                                             :collapse? true})]
+       (->> blocks
+            (map (fn [b] (or (some-> (:db/id (:block/link b)) db/entity) b)))
+            (mapcat (fn [b] (util/get-blocks-by-id (:block/uuid b))))
+            state/exit-editing-and-set-selected-blocks!)))
+   (state/set-state! :selection/selected-all? true)))
+
+(defn select-parent [e]
+  (let [edit-input (some-> (state/get-edit-input-id) gdom/getElement)
+        edit-block (state/get-edit-block)
+        target-element (.-nodeName (.-target e))]
+    (cond
+      ;; editing block fully selected
+      (and edit-block edit-input
+           (= (util/get-selected-text) (.-value edit-input)))
+      (do
+        (util/stop e)
+        (state/exit-editing-and-set-selected-blocks!
+         [(util/get-first-block-by-id (:block/uuid edit-block))]))
+
+      edit-block
+      nil
+
+      ;; Focusing other input element, e.g. when editing page title.
+      (contains? #{"INPUT" "TEXTAREA"} target-element)
+      nil
+
+      (whiteboard?)
+      (do
+        (util/stop e)
+        (.selectAll (.-api ^js (state/active-tldraw-app))))
+
+      :else
+      (do
+        (util/stop e)
+        (when-not @(:selection/selected-all? @state/state)
+          (if-let [block-id (some-> (first (state/get-selection-blocks))
+                                    (dom/attr "blockid")
+                                    uuid)]
+            (when-let [block (db/entity [:block/uuid block-id])]
+              (let [parent (:block/parent block)]
+                (cond
+                  (= (state/get-current-page) (str (:block/uuid block)))
+                  nil
+
+                  (and parent (:block/parent parent))
+                  (state/exit-editing-and-set-selected-blocks!
+                   [(util/get-first-block-by-id (:block/uuid parent))])
+
+                  (:block/name parent)
+                  ;; page block
+                  (select-all-blocks! {:page (:block/name parent)}))))
+            (select-all-blocks! {})))))))
+
+(defn escape-editing
+  [& {:keys [select? save-block?]
+      :or {save-block? true}}]
+  (let [edit-block (state/get-edit-block)]
+    (p/do!
+     (when save-block? (save-current-block!))
+     (if select?
+       (when-let [node (some-> (state/get-input) (util/rec-get-node "ls-block"))]
+         (state/exit-editing-and-set-selected-blocks! [node]))
+       (when (= (:db/id edit-block) (:db/id (state/get-edit-block)))
+         (state/clear-edit!))))))
+
+(defn replace-block-reference-with-content-at-point
+  []
+  (let [repo (state/get-current-repo)]
+    (when-let [{:keys [start end link]} (thingatpt/block-ref-at-point)]
+      (when-let [block (db/entity [:block/uuid link])]
+        (let [block-content (:block/title block)
+              format (get block :block/format :markdown)
+              block-content-without-prop (-> (property-file/remove-properties-when-file-based repo format block-content)
+                                             (drawer/remove-logbook))]
+          (when-let [input (state/get-input)]
+            (when-let [current-block-content (gobj/get input "value")]
+              (let [block-content* (str (subs current-block-content 0 start)
+                                        block-content-without-prop
+                                        (subs current-block-content end))]
+                (state/set-block-content-and-last-pos! input block-content* 1)))))))))
+
+(defn copy-current-ref
+  [block-id]
+  (when block-id
+    (util/copy-to-clipboard! (ref/->block-ref block-id))))
+
+(defn delete-current-ref!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [content (if (config/db-based-graph?)
+                    (string/replace (:block/title block) (ref/->page-ref ref-id) "")
+                    (let [match (re-pattern (str "\\s?"
+                                                 (string/replace (ref/->block-ref ref-id) #"([\(\)])" "\\$1")))]
+                      (string/replace (:block/title block) match "")))]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))
+
+(defn replace-ref-with-text!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [repo (state/get-current-repo)
+          match (ref/->block-ref ref-id)
+          ref-block (db/entity [:block/uuid ref-id])
+          block-ref-content (->> (or (:block/title ref-block) "")
+                                 (property-file/remove-built-in-properties-when-file-based repo
+                                                                                           (get ref-block :block/format :markdown))
+                                 (drawer/remove-logbook))
+          content (string/replace-first (:block/title block) match
+                                        block-ref-content)]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))
+
+(defn replace-ref-with-embed!
+  [block ref-id]
+  (when (and block ref-id)
+    (let [match (ref/->block-ref ref-id)
+          content (string/replace-first (:block/title block) match
+                                        (util/format "{{embed ((%s))}}"
+                                                     (str ref-id)))]
+      (save-block! (state/get-current-repo)
+                   (:block/uuid block)
+                   content))))
+
+(defn block-default-collapsed?
+  "Whether a block should be collapsed by default.
+  Currently, this handles all the kinds of views."
+  [block config]
+  (let [block (or (db/entity (:db/id block)) block)]
+    (or
+     (util/collapsed? block)
+     (and (or (:view? config) (:popup? config))
+          (or (ldb/page? block)
+              (some? (:block/_parent block))
+              (:table-block-title? config))))))
+
+(defn batch-set-heading!
+  [block-ids heading]
+  (let [repo (state/get-current-repo)]
+    (if (config/db-based-graph? repo)
+      (db-editor-handler/batch-set-heading! repo block-ids heading)
+      (file-editor-handler/batch-set-heading! block-ids heading))))
+
+(defn set-heading!
+  [block-id heading]
+  (batch-set-heading! [block-id] heading))
+
+(defn remove-heading!
+  [block-id]
+  (set-heading! block-id nil))
+
+(defn batch-remove-heading!
+  [block-ids]
+  (batch-set-heading! block-ids nil))
+
+(defn block->data-transfer!
+  "Set block or page name to the given event's dataTransfer. Used in dnd."
+  [block-or-page-name event page?]
+  (.setData (gobj/get event "dataTransfer")
+            (if page? "page-name" "block-uuid")
+            (str block-or-page-name)))
+
+(defn run-query-command!
+  []
+  (let [repo (state/get-current-repo)]
+    (when-let [block (some-> (state/get-edit-block)
+                             :db/id
+                             (db/entity))]
+      (p/do!
+       (save-current-block!)
+       (state/clear-edit!)
+       (p/let [query-block (or (:logseq.property/query block)
+                               (p/do!
+                                (property-handler/set-block-property! repo (:db/id block) :logseq.property/query "")
+                                (:logseq.property/query (db/entity (:db/id block)))))
+               current-query (:block/title (db/entity (:db/id block)))]
+         (p/do!
+          (ui-outliner-tx/transact!
+           {:outliner-op :save-block}
+           (property-handler/set-block-property! repo (:db/id block) :block/tags :logseq.class/Query)
+           (save-block-inner! block "" {})
+           (when query-block
+             (save-block-inner! query-block current-query {})))))))))

+ 22 - 48
src/main/frontend/handler/file_based/editor.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.file-based.editor
   "File-based graph implementation"
   (:require [clojure.string :as string]
+            [electron.ipc :as ipc]
             [frontend.commands :as commands]
             [frontend.config :as config]
             [frontend.date :as date]
@@ -9,8 +10,6 @@
             [frontend.db.query-dsl :as query-dsl]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
-            [frontend.fs :as fs]
-            [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common.editor :as editor-common-handler]
@@ -19,14 +18,12 @@
             [frontend.handler.file-based.repeated :as repeated]
             [frontend.handler.file-based.status :as status]
             [frontend.handler.property.file :as property-file]
-            [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.file-based.clock :as clock]
             [frontend.util.file-based.drawer :as drawer]
-            [goog.crypt.base64 :as base64]
             [logseq.common.path :as path]
             [logseq.common.util :as common-util]
             [logseq.common.util.block-ref :as block-ref]
@@ -302,51 +299,28 @@
    (p/all
     (for [[index ^js file] (map-indexed vector files)]
       ;; WARN file name maybe fully qualified path when paste file
-      (p/let [file-name (util/node-path.basename (.-name file))
-              [file-stem ext-full ext-base] (if file-name
-                                              (let [ext-base (util/node-path.extname file-name)
-                                                    ext-full (if-not (config/extname-of-supported? ext-base)
-                                                               (util/full-path-extname file-name) ext-base)]
-                                                [(subs file-name 0 (- (count file-name)
-                                                                      (count ext-full))) ext-full ext-base])
-                                              ["" "" ""])
-              filename  (str (gen-filename index file-stem) ext-full)
-              file-rpath  (str asset-dir-rpath "/" filename)
-              matched-alias (assets-handler/get-matched-alias-by-ext ext-base)
-              file-rpath (cond-> file-rpath
-                           (not (nil? matched-alias))
-                           (string/replace #"^[.\/\\]*assets[\/\\]+" ""))
-              dir (or (:dir matched-alias) repo-dir)]
-        (if (util/electron?)
-          (do (js/console.debug "Debug: Copy Asset #" dir file-rpath)
-              (-> (if-let [from (not-empty (.-path file))]
-                    (js/window.apis.copyFileToAssets dir file-rpath from)
-                    (p/let [content (.arrayBuffer file)]
-                      (fs/write-file! repo repo-dir file-rpath content {:skip-compare? true})))
-                  (p/then
-                   (fn [dest]
-                     [file-rpath
-                      (if (string? dest) (js/File. #js[] dest) file)
-                      (path/path-join dir file-rpath)
-                      matched-alias]))
-                  (p/catch #(js/console.error "Debug: Copy Asset Error#" %))))
+      (let [file-name (util/node-path.basename (.-name file))
+            [file-stem ext-full ext-base] (if file-name
+                                            (let [ext-base (util/node-path.extname file-name)
+                                                  ext-full (if-not (config/extname-of-supported? ext-base)
+                                                             (util/full-path-extname file-name) ext-base)]
+                                              [(subs file-name 0 (- (count file-name)
+                                                                    (count ext-full))) ext-full ext-base])
+                                            ["" "" ""])
+            filename  (str (gen-filename index file-stem) ext-full)
+            file-rpath  (str asset-dir-rpath "/" filename)
+            matched-alias (assets-handler/get-matched-alias-by-ext ext-base)
+            file-rpath (cond-> file-rpath
+                         (not (nil? matched-alias))
+                         (string/replace #"^[.\/\\]*assets[\/\\]+" ""))
+            dir (or (:dir matched-alias) repo-dir)]
 
-          (->
-           (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
-                  (cond
-                    (mobile-util/native-platform?)
-                   ;; capacitor fs accepts Blob, File implements Blob
-                    (p/let [buffer (.arrayBuffer file)
-                            content (base64/encodeByteArray (js/Uint8Array. buffer))
-                            fpath (path/path-join dir file-rpath)]
-                      (capacitor-fs/<write-file-with-base64 fpath content))
-
-                    :else                ; nfs
-                    (fs/write-file! repo dir file-rpath (.stream file) nil))
-                  [file-rpath file (path/path-join dir file-rpath) matched-alias])
-           (p/catch (fn [error]
-                      (prn :paste-file-error)
-                      (js/console.error error))))))))))
+        (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
+               (p/let [content (.arrayBuffer file)
+                       file-fpath (path/path-join dir file-rpath)]
+                 ;; file based version support electron only
+                 (ipc/ipc "writeFile" repo file-fpath content))
+               [file-rpath file (path/path-join dir file-rpath) matched-alias]))))))
 
 ;; assets/journals_2021_02_03_1612350230540_0.png
 (defn resolve-relative-path

+ 5 - 5
src/main/frontend/handler/file_based/events.cljs

@@ -289,11 +289,11 @@
                                  (let [dir (config/get-repo-dir repo)]
                                    (p/let [content (fs/read-file dir file)]
                                      (let [new-content (string/replace content (str id) (str (random-uuid)))]
-                                       (p/let [_ (fs/write-file! repo
-                                                                 dir
-                                                                 file
-                                                                 new-content
-                                                                 {})]
+                                       (p/let [_ (fs/write-plain-text-file! repo
+                                                                            dir
+                                                                            file
+                                                                            new-content
+                                                                            {})]
                                          (reset! resolved? true))))))
                      :class "inline mx-1")
           "it."]])]]))

+ 43 - 43
src/main/frontend/handler/file_based/file.cljs

@@ -1,28 +1,28 @@
 (ns frontend.handler.file-based.file
   "Provides util handler fns for file graph files"
   (:refer-clojure :exclude [load-file])
-  (:require [frontend.config :as config]
+  (:require [electron.ipc :as ipc]
+            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.file-based.model :as file-model]
             [frontend.fs :as fs]
-            [frontend.fs.nfs :as nfs]
             [frontend.fs.capacitor-fs :as capacitor-fs]
-            [frontend.handler.file-based.reset-file :as reset-file-handler]
+            [frontend.fs.nfs :as nfs]
             [frontend.handler.common.config-edn :as config-edn-common-handler]
-            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.file-based.reset-file :as reset-file-handler]
             [frontend.handler.global-config :as global-config-handler]
+            [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.mobile.util :as mobile-util]
             [frontend.schema.handler.global-config :as global-config-schema]
             [frontend.schema.handler.repo-config :as repo-config-schema]
             [frontend.state :as state]
             [frontend.util :as util]
-            [logseq.common.util :as common-util]
-            [electron.ipc :as ipc]
             [lambdaisland.glogi :as log]
-            [promesa.core :as p]
-            [frontend.mobile.util :as mobile-util]
+            [logseq.common.config :as common-config]
             [logseq.common.path :as path]
-            [logseq.common.config :as common-config]))
+            [logseq.common.util :as common-util]
+            [promesa.core :as p]))
 
 ;; TODO: extract all git ops using a channel
 
@@ -32,9 +32,9 @@
    (p/let [content (fs/read-file (config/get-repo-dir repo-url) path)]
      content)
    (p/catch
-       (fn [e]
-         (println "Load file failed: " path)
-         (js/console.error e)))))
+    (fn [e]
+      (println "Load file failed: " path)
+      (js/console.error e)))))
 
 (defn- load-multiple-files
   [repo-url paths]
@@ -64,7 +64,7 @@
     (-> (p/all (load-multiple-files repo-url files))
         (p/then (fn [contents]
                   (let [file-contents (cond->
-                                        (zipmap files contents)
+                                       (zipmap files contents)
 
                                         (seq images)
                                         (merge (zipmap images (repeat (count images) ""))))
@@ -115,7 +115,7 @@
         path-dir (config/get-repo-dir repo)
         write-file-options' (merge write-file-options
                                    (when original-content {:old-content original-content}))]
-    (fs/write-file! repo path-dir path content write-file-options')))
+    (fs/write-plain-text-file! repo path-dir path content write-file-options')))
 
 (defn alter-global-file
   "Does pre-checks on a global file, writes if it's not already written
@@ -126,18 +126,18 @@
     (do
       (detect-deprecations path content)
       (when (validate-file path content)
-       (-> (p/let [_ (when-not from-disk?
-                       (fs/write-file! "" nil path content {:skip-compare? true}))]
-                  (p/do! (global-config-handler/restore-global-config!)
-                         (state/pub-event! [:shortcut/refresh])))
-           (p/catch (fn [error]
-                      (state/pub-event! [:notification/show
-                                         {:content (str "Failed to write to file " path ", error: " error)
-                                          :status :error}])
-                      (log/error :write/failed error)
-                      (state/pub-event! [:capture-error
-                                         {:error error
-                                          :payload {:type :write-file/failed-for-alter-file}}]))))))
+        (-> (p/let [_ (when-not from-disk?
+                        (fs/write-plain-text-file! "" nil path content {:skip-compare? true}))]
+              (p/do! (global-config-handler/restore-global-config!)
+                     (state/pub-event! [:shortcut/refresh])))
+            (p/catch (fn [error]
+                       (state/pub-event! [:notification/show
+                                          {:content (str "Failed to write to file " path ", error: " error)
+                                           :status :error}])
+                       (log/error :write/failed error)
+                       (state/pub-event! [:capture-error
+                                          {:error error
+                                           :payload {:type :write-file/failed-for-alter-file}}]))))))
     (log/error :msg "alter-global-file does not support this file" :file path)))
 
 (defn alter-file
@@ -209,23 +209,23 @@
                        (when path
                          (let [path (common-util/path-normalize path)
                                original-content (get file->content path)]
-                          (-> (p/let [_ (or
-                                         (util/electron?)
-                                         (nfs/check-directory-permission! repo))]
-                                (fs/write-file! repo (config/get-repo-dir repo) path content
-                                                {:old-content original-content}))
-                              (p/catch (fn [error]
-                                         (state/pub-event! [:notification/show
-                                                            {:content (str "Failed to save the file " path ". Error: "
-                                                                           (str error))
-                                                             :status :error
-                                                             :clear? false}])
-                                         (state/pub-event! [:capture-error
-                                                            {:error error
-                                                             :payload {:type :write-file/failed}}])
-                                         (log/error :write-file/failed {:path path
-                                                                        :content content
-                                                                        :error error})))))))
+                           (-> (p/let [_ (or
+                                          (util/electron?)
+                                          (nfs/check-directory-permission! repo))]
+                                 (fs/write-plain-text-file! repo (config/get-repo-dir repo) path content
+                                                            {:old-content original-content}))
+                               (p/catch (fn [error]
+                                          (state/pub-event! [:notification/show
+                                                             {:content (str "Failed to save the file " path ". Error: "
+                                                                            (str error))
+                                                              :status :error
+                                                              :clear? false}])
+                                          (state/pub-event! [:capture-error
+                                                             {:error error
+                                                              :payload {:type :write-file/failed}}])
+                                          (log/error :write-file/failed {:path path
+                                                                         :content content
+                                                                         :error error})))))))
         finish-handler (fn []
                          (when finish-handler
                            (finish-handler)))]

+ 13 - 13
src/main/frontend/handler/global_config.cljs

@@ -2,14 +2,14 @@
   "This ns is a system component that encapsulates global config functionality.
   Unlike repo config, this also manages a directory for configuration. This
   component depends on a repo."
-  (:require [frontend.fs :as fs]
-            [frontend.state :as state]
-            [promesa.core :as p]
-            [shadow.resource :as rc]
+  (:require [borkdude.rewrite-edn :as rewrite]
             [clojure.edn :as edn]
             [electron.ipc :as ipc]
-            [borkdude.rewrite-edn :as rewrite]
-            [logseq.common.path :as path]))
+            [frontend.fs :as fs]
+            [frontend.state :as state]
+            [logseq.common.path :as path]
+            [promesa.core :as p]
+            [shadow.resource :as rc]))
 
 ;; Use defonce to avoid broken state on dev reload
 ;; Also known as home directory a.k.a. '~'
@@ -50,30 +50,30 @@
         config-path (global-config-path)]
     (p/let [_ (fs/mkdir-if-not-exists config-dir)
             file-exists? (fs/create-if-not-exists repo-url nil config-path default-content)]
-           (when-not file-exists?
-             (set-global-config-state! default-content)))))
+      (when-not file-exists?
+        (set-global-config-state! default-content)))))
 
 (defn restore-global-config!
   "Sets global config state from config file"
   []
   (let [config-path (global-config-path)]
     (p/let [config-content (fs/read-file nil config-path)]
-           (set-global-config-state! config-content))))
+      (set-global-config-state! config-content))))
 
 (defn set-global-config-kv!
   [k v]
   (let [result (rewrite/parse-string
-                 (or (state/get-global-config-str-content) "{}"))
+                (or (state/get-global-config-str-content) "{}"))
         ks (if (sequential? k) k [k])
         v (cond->> v
-                   (map? v)
-                   (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
+            (map? v)
+            (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
         new-result (if (and (= 1 (count ks))
                             (nil? v))
                      (rewrite/dissoc result (first ks))
                      (rewrite/assoc-in result ks v))
         new-str-content (str new-result)]
-    (fs/write-file! nil nil (global-config-path) new-str-content {:skip-compare? true})
+    (fs/write-plain-text-file! nil nil (global-config-path) new-str-content {:skip-compare? true})
     (state/set-global-config! (rewrite/sexpr new-result) new-str-content)))
 
 (defn start

+ 28 - 14
src/main/frontend/handler/plugin.cljs

@@ -67,6 +67,13 @@
   (invoke-exported-api :unlink_installed_web_plugin key)
   (invoke-exported-api :unlink_plugin_user_settings key))
 
+(defn assets-theme-to-file
+  [theme]
+  (when theme
+    (cond-> theme
+      (util/electron?)
+      (update :url #(some-> % (string/replace-first "assets://" "file://"))))))
+
 (defn load-plugin-preferences
   []
   (-> (invoke-exported-api :load_user_preferences)
@@ -211,7 +218,6 @@
                    (let [e (or evt ctx)]
                      (when-let [{:keys [status payload only-check]} (bean/->clj e)]
                        (case (keyword status)
-
                          :completed
                          (let [{:keys [id dst name title theme web-pkg]} payload
                                name (or title name "Untitled")]
@@ -333,6 +339,7 @@
                      :action (fn []
                                (state/pub-event!
                                 [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
+
     palette-cmd))
 
 (defn simple-cmd-keybinding->shortcut-args
@@ -483,7 +490,7 @@
 (defn select-a-plugin-theme
   [pid]
   (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
-    (when-let [theme (first themes)]
+    (when-let [theme (assets-theme-to-file (first themes))]
       (js/LSPluginCore.selectTheme (bean/->js theme)))))
 
 (defn update-plugin-settings-state
@@ -642,7 +649,7 @@
             dotroot (get-ls-dotdir-root)
             filepath (util/node-path.join dotroot dirname (str key ".json"))]
         (if (util/electron?)
-          (fs/write-file! repo nil filepath (js/JSON.stringify data nil 2) {:skip-compare? true})
+          (fs/write-plain-text-file! repo nil filepath (js/JSON.stringify data nil 2) {:skip-compare? true})
           (idb/set-item! filepath data))))))
 
 (defn make-fn-to-unlink-dotdir-json
@@ -882,9 +889,16 @@
                                     (clear-commands! pid)
                                     (unregister-plugin-themes pid)))
 
-                  (.on "themes-changed" (fn [^js themes]
-                                          (swap! state/state assoc :plugin/installed-themes
-                                                 (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+                  (.on "theme-selected" (fn [^js theme]
+                                          (let [theme (bean/->clj theme)
+                                                theme (assets-theme-to-file theme)
+                                                url   (:url theme)
+                                                mode  (:mode theme)]
+                                            (when mode
+                                              (state/set-custom-theme! mode theme)
+                                              (state/set-theme-mode! mode))
+                                            (state/set-state! :plugin/selected-theme url)
+                                            (hook-plugin-app :theme-changed theme))))
 
                   (.on "theme-selected" (fn [^js theme ^js opts]
                                           (let [theme (bean/->clj theme)
@@ -939,16 +953,16 @@
         plugins-async)
 
       (p/then
-        (fn [plugins-async]
+       (fn [plugins-async]
           ;; true indicate for preboot finished
-          (state/set-state! :plugin/indicator-text true)
+         (state/set-state! :plugin/indicator-text true)
           ;; wait for the plugin register async messages
-          (js/setTimeout
-            (fn [] (callback)
-              (some-> (seq plugins-async)
-                (p/delay 16)
-                (p/then #(.register js/LSPluginCore (bean/->js plugins-async) true))))
-            (if (util/electron?) 64 0))))
+         (js/setTimeout
+          (fn [] (callback)
+            (some-> (seq plugins-async)
+                    (p/delay 16)
+                    (p/then #(.register js/LSPluginCore (bean/->js plugins-async) true))))
+          (if (util/electron?) 64 0))))
       (p/catch
        (fn [^js e]
          (log/error :setup-plugin-system-error e)

+ 2 - 2
src/main/frontend/handler/plugin_config.cljs

@@ -40,14 +40,14 @@ when a plugin is installed, updated or removed"
                               str)]
          ;; fs protocols require repo and dir when they aren't necessary. For this component,
          ;; neither is needed so these are blank and nil respectively
-    (fs/write-file! "" nil (plugin-config-path) updated-content {:skip-compare? true})))
+    (fs/write-plain-text-file! "" nil (plugin-config-path) updated-content {:skip-compare? true})))
 
 (defn remove-plugin
   "Removes a plugin from plugin.edn"
   [plugin-id]
   (p/let [content (fs/read-file "" (plugin-config-path))
           updated-content (-> content rewrite/parse-string (rewrite/dissoc (keyword plugin-id)) str)]
-    (fs/write-file! "" nil (plugin-config-path) updated-content {:skip-compare? true})))
+    (fs/write-plain-text-file! "" nil (plugin-config-path) updated-content {:skip-compare? true})))
 
 (defn- create-plugin-config-file-if-not-exists
   []

+ 6 - 10
src/main/frontend/util/persist_var.cljs

@@ -1,15 +1,14 @@
 (ns frontend.util.persist-var
   "System-component-like ns that provides an atom-like abstraction over an edn
   file"
-  (:require [frontend.config :as config]
-            [frontend.state :as state]
+  (:require [cljs.reader :as reader]
+            [frontend.config :as config]
             [frontend.fs :as fs]
+            [frontend.state :as state]
             [frontend.util :as util]
-            [cljs.reader :as reader]
             [promesa.core :as p]))
 
-
-(defn- load-rpath 
+(defn- load-rpath
   "Returns the relative path to the file that stores the persist-var"
   [location]
   (str config/app-name "/" location ".edn"))
@@ -67,7 +66,7 @@
             repo (state/get-current-repo)
             content (str (get-in @*value [repo :value]))
             dir (config/get-repo-dir repo)]
-        (fs/write-file! repo dir path content {:skip-compare? true}))))
+        (fs/write-plain-text-file! repo dir path content {:skip-compare? true}))))
 
   IDeref
   (-deref [_this]
@@ -83,7 +82,6 @@
   (-pr-writer [_ w _opts]
     (write-all w (str "#PersistVar[" @*value ", loc: " location "]"))))
 
-
 (def *all-persist-vars (atom []))
 
 (defn load-vars []
@@ -106,6 +104,4 @@
 (comment
   (do
     (def bbb (persist-var 1 "aaa"))
-    (-save bbb)
-
-    ))
+    (-save bbb)))

+ 4 - 5
src/main/logseq/api.cljs

@@ -9,8 +9,8 @@
             [frontend.db :as db]
             [frontend.db.async :as db-async]
             [frontend.db.conn :as conn]
-            [frontend.db.model :as db-model]
             [frontend.db.file-based.model :as file-model]
+            [frontend.db.model :as db-model]
             [frontend.db.query-custom :as query-custom]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-react :as query-react]
@@ -223,7 +223,7 @@
   (fn [path ^js data]
     (let [repo ""
           path (util/node-path.join path "package.json")]
-      (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
+      (fs/write-plain-text-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
 
 (def ^:export save_focused_code_editor_content
   (fn []
@@ -243,7 +243,7 @@
           user-path-root (util/node-path.dirname user-path)
           exist?         (fs/file-exists? user-path-root "")
           _              (when-not exist? (fs/mkdir-recur! user-path-root))
-          _              (fs/write-file! repo nil user-path content {:skip-compare? true})]
+          _              (fs/write-plain-text-file! repo nil user-path content {:skip-compare? true})]
     user-path))
 
 (defn ^:export write_dotdir_file
@@ -379,7 +379,7 @@
             path (plugin-handler/get-ls-dotdir-root)
             path (util/node-path.join path "preferences.json")]
         (if (util/electron?)
-          (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true})
+          (fs/write-plain-text-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true})
           (idb/set-item! path data))))))
 
 (def ^:export load_plugin_user_settings
@@ -666,7 +666,6 @@
                           :journal? journal
                           :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
                           :format format}
-
                           (not db-base?)
                           (assoc :properties properties))))
             _ (when (and db-base? (seq properties))

+ 6 - 6
src/main/logseq/sdk/git.cljs

@@ -1,10 +1,10 @@
 (ns logseq.sdk.git
   (:require [cljs-bean.core :as bean]
-            [promesa.core :as p]
-            [frontend.state :as state]
-            [frontend.handler.shell :as shell]
             [frontend.config :as config]
-            [frontend.fs :as fs]))
+            [frontend.fs :as fs]
+            [frontend.handler.shell :as shell]
+            [frontend.state :as state]
+            [promesa.core :as p]))
 
 (defn ^:export exec_command
   [^js args]
@@ -18,11 +18,11 @@
             dir (config/get-repo-dir repo)
             _ (fs/create-if-not-exists repo dir file)
             content (fs/read-file dir file)]
-           content)))
+      content)))
 
 (defn ^:export save_ignore_file
   [content]
   (when-let [repo (and (string? content) (state/get-current-repo))]
     (p/let [file ".gitignore"
             dir (config/get-repo-dir repo)
-            _ (fs/write-file! repo dir file content {:skip-compare? true})])))
+            _ (fs/write-plain-text-file! repo dir file content {:skip-compare? true})])))

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 366 - 328
static/yarn.lock


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 309 - 381
yarn.lock


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است