Просмотр исходного кода

Merge branch 'master' into feat/integrated-title-bar

Konstantinos Kaloutas 2 лет назад
Родитель
Сommit
f90eef2b1a

+ 1 - 0
.carve/ignore

@@ -48,6 +48,7 @@ frontend.mixins/perf-measure-mixin
 frontend.mobile.util/get-idevice-statusbar-height
 frontend.mobile.util/get-idevice-statusbar-height
 ;; Used in macro
 ;; Used in macro
 frontend.modules.outliner.datascript/transact!
 frontend.modules.outliner.datascript/transact!
+frontend.modules.outliner.core/*transaction-opts*
 ;; Referenced in comment
 ;; Referenced in comment
 frontend.page/route-view
 frontend.page/route-view
 ;; placeholder fn
 ;; placeholder fn

+ 2 - 1
deps/common/src/logseq/common/graph.cljs

@@ -66,7 +66,8 @@
       keyword))
       keyword))
 
 
 (defn get-files
 (defn get-files
-  "Given a graph's root dir, returns a list of all files that it recognizes"
+  "Given a graph's root dir, returns a list of all files that it recognizes.
+   Graph dir must be an absolute path in order for ignoring to work correctly"
   [graph-dir]
   [graph-dir]
   (->> (readdir graph-dir)
   (->> (readdir graph-dir)
        (remove (partial ignored-path? graph-dir))
        (remove (partial ignored-path? graph-dir))

+ 8 - 7
deps/graph-parser/src/logseq/graph_parser/cli.cljs

@@ -24,14 +24,15 @@
     files))
     files))
 
 
 (defn- build-graph-files
 (defn- build-graph-files
-  "Given a graph directory, return allowed file paths and their contents in preparation
+  "Given a graph directory, return absolute, allowed file paths and their contents in preparation
    for parsing"
    for parsing"
-  [dir config]
-  (->> (common-graph/get-files dir)
-       (map #(hash-map :file/path %))
-       graph-parser/filter-files
-       (remove-hidden-files dir config)
-       (mapv #(assoc % :file/content (slurp (:file/path %))))))
+  [dir* config]
+  (let [dir (path/resolve dir*)]
+    (->> (common-graph/get-files dir)
+        (map #(hash-map :file/path %))
+        graph-parser/filter-files
+        (remove-hidden-files dir config)
+        (mapv #(assoc % :file/content (slurp (:file/path %)))))))
 
 
 (defn- read-config
 (defn- read-config
   "Reads repo-specific config from logseq/config.edn"
   "Reads repo-specific config from logseq/config.edn"

+ 4 - 4
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -69,11 +69,11 @@
        ffirst))
        ffirst))
 
 
 (defn- query-assertions
 (defn- query-assertions
-  [db files]
+  [db graph-dir files]
   (testing "Query based stats"
   (testing "Query based stats"
     (is (= (->> files
     (is (= (->> files
                 ;; logseq files aren't saved under :block/file
                 ;; logseq files aren't saved under :block/file
-                (remove #(string/includes? % (str "/" gp-config/app-name "/")))
+                (remove #(string/includes? % (str graph-dir "/" gp-config/app-name "/")))
                 ;; edn files being listed in docs by parse-graph aren't graph files
                 ;; edn files being listed in docs by parse-graph aren't graph files
                 (remove #(and (not (gp-config/whiteboard? %)) (string/ends-with? % ".edn")))
                 (remove #(and (not (gp-config/whiteboard? %)) (string/ends-with? % ".edn")))
                 set)
                 set)
@@ -148,7 +148,7 @@
   logseq app. It is important to run these in both contexts to ensure that the
   logseq app. It is important to run these in both contexts to ensure that the
   functionality in frontend.handler.repo and logseq.graph-parser remain the
   functionality in frontend.handler.repo and logseq.graph-parser remain the
   same"
   same"
-  [db files]
+  [db graph-dir files]
   ;; Counts assertions help check for no major regressions. These counts should
   ;; Counts assertions help check for no major regressions. These counts should
   ;; only increase over time as the docs graph rarely has deletions
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
   (testing "Counts"
@@ -168,4 +168,4 @@
                  db)))
                  db)))
         "Advanced query count"))
         "Advanced query count"))
 
 
-  (query-assertions db files))
+  (query-assertions db graph-dir files))

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

@@ -1,16 +1,30 @@
-(ns logseq.graph-parser.cli-test
-  (:require [cljs.test :refer [deftest is testing]]
+(ns ^:node-only logseq.graph-parser.cli-test
+  (:require [cljs.test :refer [deftest is testing async use-fixtures]]
             [logseq.graph-parser.cli :as gp-cli]
             [logseq.graph-parser.cli :as gp-cli]
             [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
             [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            ["fs" :as fs]
+            ["process" :as process]
+            ["path" :as path]))
+
+(use-fixtures
+  :each
+  ;; Cleaning tmp/ before leaves last tmp/ after a test run for dev and debugging
+  {:before
+   #(async done
+           (if (fs/existsSync "tmp")
+             (fs/rm "tmp" #js {:recursive true} (fn [err]
+                                                  (when err (js/console.log err))
+                                                  (done)))
+             (done)))})
 
 
 ;; Integration test that test parsing a large graph like docs
 ;; Integration test that test parsing a large graph like docs
 (deftest ^:integration parse-graph
 (deftest ^:integration parse-graph
   (let [graph-dir "test/docs-0.9.2"
   (let [graph-dir "test/docs-0.9.2"
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.9.2")
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.9.2")
-        {:keys [conn files asts]} (gp-cli/parse-graph graph-dir {:verbose false})] ;; legacy parsing
+        {:keys [conn files asts]} (gp-cli/parse-graph graph-dir {:verbose false})]
 
 
-    (docs-graph-helper/docs-graph-assertions @conn files)
+    (docs-graph-helper/docs-graph-assertions @conn graph-dir files)
 
 
     (testing "Asts"
     (testing "Asts"
       (is (seq asts) "Asts returned are non-zero")
       (is (seq asts) "Asts returned are non-zero")
@@ -25,3 +39,37 @@
                             (string/includes? (:file %) (str graph-dir "/logseq/")))
                             (string/includes? (:file %) (str graph-dir "/logseq/")))
                           asts))
                           asts))
           "Parsed files shouldn't have empty asts"))))
           "Parsed files shouldn't have empty asts"))))
+
+(defn- create-logseq-graph
+  "Creates a minimal mock graph"
+  [dir]
+  (fs/mkdirSync (path/join dir "logseq") #js {:recursive true})
+  (fs/mkdirSync (path/join dir "journals"))
+  (fs/mkdirSync (path/join dir "pages")))
+
+(deftest ^:focus build-graph-files
+  (create-logseq-graph "tmp/test-graph")
+  ;; Create files that are recognized
+  (fs/writeFileSync "tmp/test-graph/pages/foo.md" "")
+  (fs/writeFileSync "tmp/test-graph/journals/2023_05_09.md" "")
+  ;; Create file that are ignored and filtered out
+  (fs/writeFileSync "tmp/test-graph/pages/foo.json" "")
+  (fs/mkdirSync (path/join "tmp/test-graph" "logseq" "bak"))
+  (fs/writeFileSync "tmp/test-graph/logseq/bak/baz.md" "")
+
+  (testing "ignored files from common-graph"
+    (is (= (map #(path/join (process/cwd) "tmp/test-graph" %) ["journals/2023_05_09.md" "pages/foo.md"])
+           (map :file/path (#'gp-cli/build-graph-files (path/resolve "tmp/test-graph") {})))
+        "Correct paths returned for absolute dir")
+    (process/chdir "tmp/test-graph")
+    (is (= (map #(path/join (process/cwd) %) ["journals/2023_05_09.md" "pages/foo.md"])
+           (map :file/path (#'gp-cli/build-graph-files "." {})))
+        "Correct paths returned for relative current dir")
+    (process/chdir "../.."))
+
+  (testing ":hidden config"
+    (fs/mkdirSync (path/join "tmp/test-graph" "script"))
+    (fs/writeFileSync "tmp/test-graph/script/README.md" "")
+    (is (= (map #(path/join (process/cwd) "tmp/test-graph" %) ["journals/2023_05_09.md" "pages/foo.md"])
+           (map :file/path (#'gp-cli/build-graph-files "tmp/test-graph" {:hidden ["script"]})))
+        "Correct paths returned")))

+ 3 - 0
deps/publishing/src/logseq/publishing/export.cljs

@@ -78,6 +78,7 @@
                                          :as options}]
                                          :as options}]
   (let [custom-css-path (node-path/join repo-path "logseq" "custom.css")
   (let [custom-css-path (node-path/join repo-path "logseq" "custom.css")
         export-css-path (node-path/join repo-path "logseq" "export.css")
         export-css-path (node-path/join repo-path "logseq" "export.css")
+        custom-js-path (node-path/join repo-path "logseq" "custom.js")
         output-static-dir (node-path/join output-dir "static")
         output-static-dir (node-path/join output-dir "static")
         index-html-path (node-path/join output-dir "index.html")]
         index-html-path (node-path/join output-dir "index.html")]
     (-> (p/let [_ (fs/mkdirSync output-static-dir #js {:recursive true})
     (-> (p/let [_ (fs/mkdirSync output-static-dir #js {:recursive true})
@@ -87,6 +88,8 @@
                 _ (fs/writeFileSync (node-path/join output-static-dir "css" "export.css")  export-css)
                 _ (fs/writeFileSync (node-path/join output-static-dir "css" "export.css")  export-css)
                 custom-css (if (fs/existsSync custom-css-path) (str (fs/readFileSync custom-css-path)) "")
                 custom-css (if (fs/existsSync custom-css-path) (str (fs/readFileSync custom-css-path)) "")
                 _ (fs/writeFileSync (node-path/join output-static-dir "css" "custom.css") custom-css)
                 _ (fs/writeFileSync (node-path/join output-static-dir "css" "custom.css") custom-css)
+                custom-js (if (fs/existsSync custom-js-path) (str (fs/readFileSync custom-js-path)) "")
+                _ (fs/writeFileSync (node-path/join output-static-dir "js" "custom.js") custom-js)
                 _ (cleanup-js-dir output-static-dir)]
                 _ (cleanup-js-dir output-static-dir)]
                (notification-fn {:type "success"
                (notification-fn {:type "success"
                                  :payload (str "Export public pages and publish assets to " output-dir " successfully 🎉")}))
                                  :payload (str "Export public pages and publish assets to " output-dir " successfully 🎉")}))

+ 2 - 1
deps/publishing/src/logseq/publishing/html.cljs

@@ -126,7 +126,8 @@ necessary db filtering"
             [:script {:src "static/js/highlight.min.js"}]
             [:script {:src "static/js/highlight.min.js"}]
             [:script {:src "static/js/katex.min.js"}]
             [:script {:src "static/js/katex.min.js"}]
             [:script {:src "static/js/html2canvas.min.js"}]
             [:script {:src "static/js/html2canvas.min.js"}]
-            [:script {:src "static/js/code-editor.js"}]])))))
+            [:script {:src "static/js/code-editor.js"}]
+            [:script {:src "static/js/custom.js"}]])))))
 
 
 (defn build-html
 (defn build-html
   "Given the graph's db, filters the db using the given options and returns the
   "Given the graph's db, filters the db using the given options and returns the

+ 11 - 1
deps/publishing/test/logseq/publishing/export_test.cljs

@@ -73,7 +73,7 @@
          (let [original-paths (map path/basename (get-files-recursively "tmp/static"))
          (let [original-paths (map path/basename (get-files-recursively "tmp/static"))
                copied-paths (map path/basename (get-files-recursively "tmp/published-graph"))
                copied-paths (map path/basename (get-files-recursively "tmp/published-graph"))
                new-files (set/difference (set copied-paths) (set original-paths))]
                new-files (set/difference (set copied-paths) (set original-paths))]
-           (is (= #{"index.html" "custom.css" "export.css"}
+           (is (= #{"index.html" "custom.css" "export.css" "custom.js"}
                   new-files)
                   new-files)
                "A published graph has the correct new files")
                "A published graph has the correct new files")
            (is (= "<div>WOOT</div>"
            (is (= "<div>WOOT</div>"
@@ -97,6 +97,16 @@
                 (str (fs/readFileSync "tmp/published-graph/static/css/export.css")))
                 (str (fs/readFileSync "tmp/published-graph/static/css/export.css")))
              "export.css is copied correctly")))
              "export.css is copied correctly")))
 
 
+(deftest-async create-export-with-js-files
+  (create-static-dir "tmp/static")
+  (create-logseq-graph "tmp/test-graph")
+  (fs/writeFileSync "tmp/test-graph/logseq/custom.js" "// foo")
+
+  (p/let [_ (create-export "tmp/static" "tmp/test-graph" "tmp/published-graph" {})]
+         (is (= "// foo"
+                (str (fs/readFileSync "tmp/published-graph/static/js/custom.js")))
+             "custom.js is copied correctly")))
+
 (deftest-async create-export-with-assets
 (deftest-async create-export-with-assets
   (create-static-dir "tmp/static")
   (create-static-dir "tmp/static")
   (create-logseq-graph "tmp/test-graph")
   (create-logseq-graph "tmp/test-graph")

+ 74 - 0
e2e-tests/blockref.spec.ts

@@ -0,0 +1,74 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage, enterNextBlock, modKey, editNthBlock, moveCursorToBeginning, moveCursorToEnd } from './utils'
+import { dispatch_kb_events } from './util/keyboard-events'
+
+// Create a random page with some pre-defined blocks
+// - a
+// - b
+//   id:: UUID
+// - ((id))
+async function setUpBlocks(page, block) {
+  await createRandomPage(page)
+
+  await block.mustFill('a')
+  await block.enterNext()
+  await block.mustFill('b')
+  await page.keyboard.press(modKey + '+c')
+  await page.waitForTimeout(100)
+  await block.enterNext()
+  await page.keyboard.press(modKey + '+v')
+  await page.waitForTimeout(100)
+}
+
+test('backspace at the beginning of a refed block #9406', async ({ page, block }) => {
+  await setUpBlocks(page, block)
+  await editNthBlock(page, 1)
+  await moveCursorToBeginning(page)
+  await page.keyboard.press('Backspace')
+  await expect(page.locator('textarea >> nth=0')).toHaveText("ab")
+  await expect(await block.selectionStart()).toEqual(1)
+  await expect(page.locator('.block-ref >> text="ab"')).toHaveCount(1);
+})
+
+test('delete at the end of a prev block before a refed block #9406', async ({ page, block }) => {
+  await setUpBlocks(page, block)
+  await editNthBlock(page, 0)
+  await moveCursorToEnd(page)
+  await page.keyboard.press('Delete')
+  await expect(page.locator('textarea >> nth=0')).toHaveText("ab")
+  await expect(await block.selectionStart()).toEqual(1)
+  await expect(page.locator('.block-ref >> text="ab"')).toHaveCount(1);
+})
+
+test('delete selected blocks, block ref should be replaced by content #9406', async ({ page, block }) => {
+  await setUpBlocks(page, block)
+  await editNthBlock(page, 0)
+  await page.waitForTimeout(100)
+  await page.keyboard.down('Shift')
+  await page.keyboard.press('ArrowDown')
+  await page.keyboard.press('ArrowDown')
+  await page.keyboard.up('Shift')
+  await block.waitForSelectedBlocks(2)
+  await page.keyboard.press('Backspace')
+  await expect(page.locator('.ls-block')).toHaveCount(1)
+  await editNthBlock(page, 0)
+  await expect(page.locator('textarea >> nth=0')).toHaveText("b")
+})
+
+test('delete and undo #9406', async ({ page, block }) => {
+  await setUpBlocks(page, block)
+  await editNthBlock(page, 0)
+  await page.waitForTimeout(100)
+  await page.keyboard.down('Shift')
+  await page.keyboard.press('ArrowDown')
+  await page.keyboard.press('ArrowDown')
+  await page.keyboard.up('Shift')
+  await block.waitForSelectedBlocks(2)
+  await page.keyboard.press('Backspace')
+  await expect(page.locator('.ls-block')).toHaveCount(1)
+  await page.keyboard.press(modKey + '+z')
+  await page.waitForTimeout(100)
+  await expect(page.locator('.ls-block')).toHaveCount(3)
+  await expect(page.locator('.block-ref >> text="b"')).toHaveCount(1);
+})

+ 25 - 1
e2e-tests/utils.ts

@@ -31,6 +31,26 @@ export async function lastBlock(page: Page): Promise<Locator> {
   return page.locator('textarea >> nth=0')
   return page.locator('textarea >> nth=0')
 }
 }
 
 
+/**
+ * Move the cursor to the beginning of the current editor
+ * @param page The Playwright Page object.
+ */
+export async function moveCursorToBeginning(page: Page): Promise<Locator> {
+  await page.press('textarea >> nth=0', modKey + '+a') // select all
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  return page.locator('textarea >> nth=0')
+}
+
+/**
+ * Move the cursor to the end of the current editor
+ * @param page The Playwright Page object.
+ */
+export async function moveCursorToEnd(page: Page): Promise<Locator> {
+  await page.press('textarea >> nth=0', modKey + '+a') // select all
+  await page.press('textarea >> nth=0', 'ArrowRight')
+  return page.locator('textarea >> nth=0')
+}
+
 /**
 /**
  * Press Enter and create the next block.
  * Press Enter and create the next block.
  * @param page The Playwright Page object.
  * @param page The Playwright Page object.
@@ -155,8 +175,12 @@ export async function loadLocalGraph(page: Page, path: string): Promise<void> {
   console.log('Graph loaded for ' + path)
   console.log('Graph loaded for ' + path)
 }
 }
 
 
+export async function editNthBlock(page: Page, n) {
+  await page.click(`.ls-block .block-content >> nth=${n}`)
+}
+
 export async function editFirstBlock(page: Page) {
 export async function editFirstBlock(page: Page) {
-  await page.click('.ls-block .block-content >> nth=0')
+  await editNthBlock(page, 0)
 }
 }
 
 
 /**
 /**

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

@@ -513,7 +513,7 @@
 				DEVELOPMENT_TEAM = K378MFWK59;
 				DEVELOPMENT_TEAM = K378MFWK59;
 				ENABLE_BITCODE = NO;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 0.9.6;
 				MARKETING_VERSION = 0.9.6;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
@@ -540,7 +540,7 @@
 				DEVELOPMENT_TEAM = K378MFWK59;
 				DEVELOPMENT_TEAM = K378MFWK59;
 				ENABLE_BITCODE = NO;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 0.9.6;
 				MARKETING_VERSION = 0.9.6;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;

+ 14 - 6
src/main/frontend/commands.cljs

@@ -278,7 +278,8 @@
                [:editor/exit]] query-doc]
                [:editor/exit]] query-doc]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
      ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
      ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
-     ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
+     ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
+                                                     :backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
                     [:codemirror/focus]] "Insert a calculator"]
      ["Draw" (fn []
      ["Draw" (fn []
                (let [file (draw/file-name)
                (let [file (draw/file-name)
@@ -402,17 +403,24 @@
         edit-content (gobj/get input "value")
         edit-content (gobj/get input "value")
         current-pos (cursor/pos input)
         current-pos (cursor/pos input)
         prefix (subs edit-content 0 current-pos)
         prefix (subs edit-content 0 current-pos)
+        surfix (subs edit-content current-pos)
         new-value (str prefix
         new-value (str prefix
                        value
                        value
-                       (subs edit-content current-pos))
+                       surfix)
         new-pos (- (+ (count prefix)
         new-pos (- (+ (count prefix)
                       (count value)
                       (count value)
                       (or forward-pos 0))
                       (or forward-pos 0))
                    (or backward-pos 0))]
                    (or backward-pos 0))]
-    (state/set-block-content-and-last-pos! id new-value new-pos)
-    (cursor/move-cursor-to input new-pos)
-    (when check-fn
-      (check-fn new-value (dec (count prefix)) new-pos))))
+    (state/set-edit-content! (state/get-edit-input-id)
+                             (str prefix value))
+    ;; HACK: save scroll-pos of current pos, then add trailing content
+    (let [scroll-container (util/nearest-scrollable-container input)
+          scroll-pos (.-scrollTop scroll-container)]
+      (state/set-block-content-and-last-pos! id new-value new-pos)
+      (cursor/move-cursor-to input new-pos)
+      (set! (.-scrollTop scroll-container) scroll-pos)
+      (when check-fn
+        (check-fn new-value (dec (count prefix)) new-pos)))))
 
 
 (defn simple-replace!
 (defn simple-replace!
   [id value selected
   [id value selected

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

@@ -2309,7 +2309,8 @@
 
 
 (rum/defc block-content < rum/reactive
 (rum/defc block-content < rum/reactive
   [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
   [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
-  (let [{:block/keys [title body] :as block} (if (:block/title block) block
+  (let [content (property/remove-built-in-properties format content)
+        {:block/keys [title body] :as block} (if (:block/title block) block
                                                  (merge block (block/parse-title-and-body uuid format pre-block? content)))
                                                  (merge block (block/parse-title-and-body uuid format pre-block? content)))
         collapsed? (util/collapsed? block)
         collapsed? (util/collapsed? block)
         plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))
         plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))

+ 89 - 54
src/main/frontend/handler/editor.cljs

@@ -353,8 +353,16 @@
         block (apply dissoc block db-schema/retract-attributes)]
         block (apply dissoc block db-schema/retract-attributes)]
     (profile
     (profile
      "Save block: "
      "Save block: "
-     (let [block' (wrap-parse-block block)
-           opts' (merge opts {:outliner-op :save-block})]
+     (let [original-uuid (:block/uuid (db/entity (:db/id block)))
+           uuid-changed? (not= (:block/uuid block) original-uuid)
+           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' (merge opts (cond-> {:outliner-op :save-block}
+                               uuid-changed?
+                               (assoc :uuid-changed {:from (:block/uuid block)
+                                                     :to original-uuid})))]
        (outliner-tx/transact!
        (outliner-tx/transact!
         opts'
         opts'
         (outliner-core/save-block! block'))
         (outliner-core/save-block! block'))
@@ -383,7 +391,8 @@
          content (-> (property/remove-built-in-properties format content)
          content (-> (property/remove-built-in-properties format content)
                      (drawer/remove-logbook))]
                      (drawer/remove-logbook))]
      (cond
      (cond
-       (another-block-with-same-id-exists? uuid block-id)
+       (and (another-block-with-same-id-exists? uuid block-id)
+            (not (= :delete (:editor/op opts))))
        (notification/show!
        (notification/show!
         [:p.content
         [:p.content
          (util/format "Block with the id %s already exists!" block-id)]
          (util/format "Block with the id %s already exists!" block-id)]
@@ -760,6 +769,9 @@
         (let [original-content (util/trim-safe (:block/content block))
         (let [original-content (util/trim-safe (:block/content block))
               value' (-> (property/remove-built-in-properties format original-content)
               value' (-> (property/remove-built-in-properties format original-content)
                          (drawer/remove-logbook))
                          (drawer/remove-logbook))
+              value (->> value
+                         (property/remove-properties format)
+                         (drawer/remove-logbook))
               new-value (str value' value)
               new-value (str value' value)
               tail-len (count value)
               tail-len (count value)
               pos (max
               pos (max
@@ -767,10 +779,12 @@
                      (gobj/get (utf8/encode original-content) "length")
                      (gobj/get (utf8/encode original-content) "length")
                      0)
                      0)
                    0)
                    0)
-              f (fn [] (edit-block! block pos id
-                                    {:custom-content new-value
-                                     :tail-len tail-len
-                                     :move-cursor? false}))]
+              f (fn []
+                  (edit-block! (db/pull (:db/id block))
+                               pos
+                               id
+                               {:custom-content new-value
+                                :tail-len tail-len}))]
           (when move? (f))
           (when move? (f))
           {:prev-block block
           {:prev-block block
            :new-content new-value
            :new-content new-value
@@ -788,9 +802,9 @@
        (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
        (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
          (when (> page-blocks-count 1)
          (when (> page-blocks-count 1)
-           (let [block (db/entity [:block/uuid block-id])
-                 has-children? (seq (:block/_parent block))
-                 block (db/pull (:db/id block))
+           (let [block-e (db/entity [:block/uuid block-id])
+                 has-children? (seq (:block/_parent block-e))
+                 block (db/pull (:db/id block-e))
                  left (tree/-get-left (outliner-core/block block))
                  left (tree/-get-left (outliner-core/block block))
                  left-has-children? (and left
                  left-has-children? (and left
                                          (when-let [block-id (:block/uuid (:data left))]
                                          (when-let [block-id (:block/uuid (:data left))]
@@ -803,14 +817,20 @@
                        {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        concat-prev-block? (boolean (and prev-block new-content))
                        concat-prev-block? (boolean (and prev-block new-content))
                        transact-opts (cond->
                        transact-opts (cond->
-                                       {:outliner-op :delete-block}
+                                       {:outliner-op :delete-blocks}
                                        concat-prev-block?
                                        concat-prev-block?
                                        (assoc :concat-data
                                        (assoc :concat-data
                                               {:last-edit-block (:block/uuid block)}))]
                                               {:last-edit-block (:block/uuid block)}))]
                    (outliner-tx/transact! transact-opts
                    (outliner-tx/transact! transact-opts
-                     (when concat-prev-block?
-                       (save-block! repo prev-block new-content))
-                     (delete-block-aux! block delete-children?))
+                     (if concat-prev-block?
+                       (let [prev-block' (if (seq (:block/_refs block-e))
+                                           (assoc prev-block
+                                                  :block/uuid (:block/uuid block)
+                                                  :block/additional-properties (:block/properties block))
+                                           prev-block)]
+                         (delete-block-aux! block delete-children?)
+                         (save-block! repo prev-block' new-content {:editor/op :delete}))
+                       (delete-block-aux! block delete-children?)))
                    (move-fn)))))))))
                    (move-fn)))))))))
    (state/set-editor-op! nil)))
    (state/set-editor-op! nil)))
 
 
@@ -1224,28 +1244,24 @@
   (let [value (string/trim value)]
   (let [value (string/trim value)]
     ;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever
     ;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever
     ;; maybe we shouldn't save the block/file in "will-unmount" event?
     ;; maybe we shouldn't save the block/file in "will-unmount" event?
-    (save-block-if-changed! block value
-                            (merge
-                             {:init-properties (:block/properties block)}
-                             opts))))
+    (save-block-if-changed! block value opts)))
 
 
 (defn save-block!
 (defn save-block!
   ([repo block-or-uuid content]
   ([repo block-or-uuid content]
     (save-block! repo block-or-uuid content {}))
     (save-block! repo block-or-uuid content {}))
-  ([repo block-or-uuid content {:keys [properties] :or {}}]
+  ([repo block-or-uuid content {:keys [properties] :as opts}]
    (let [block (if (or (uuid? block-or-uuid)
    (let [block (if (or (uuid? block-or-uuid)
                        (string? block-or-uuid))
                        (string? block-or-uuid))
                  (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)]
                  (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)]
      (save-block!
      (save-block!
-       {:block block :repo repo}
-       (if (seq properties)
-          (property/insert-properties (:block/format block) content properties)
-        content)
-     )))
-  ([{:keys [block repo] :as _state} value]
+      {:block block :repo repo :opts (dissoc opts :properties)}
+      (if (seq properties)
+        (property/insert-properties (:block/format block) content properties)
+        content))))
+  ([{:keys [block repo opts] :as _state} value]
    (let [repo (or repo (state/get-current-repo))]
    (let [repo (or repo (state/get-current-repo))]
      (when (db/entity repo [:block/uuid (:block/uuid block)])
      (when (db/entity repo [:block/uuid (:block/uuid block)])
-       (save-block-aux! block value {})))))
+       (save-block-aux! block value opts)))))
 
 
 (defn save-blocks!
 (defn save-blocks!
   [blocks]
   [blocks]
@@ -1539,7 +1555,7 @@
          "$" "$"
          "$" "$"
          ":" ":"))
          ":" ":"))
 
 
-(defn autopair
+(defn- autopair
   [input-id prefix _format _option]
   [input-id prefix _format _option]
   (let [value (get autopair-map prefix)
   (let [value (get autopair-map prefix)
         selected (util/get-selected-text)
         selected (util/get-selected-text)
@@ -1576,11 +1592,11 @@
 (defn- autopair-left-paren?
 (defn- autopair-left-paren?
   [input key]
   [input key]
   (and (= key "(")
   (and (= key "(")
-       (or
-         (surround-by? input :start "")
-         (surround-by? input " " "")
-         (surround-by? input "]" "")
-         (surround-by? input "(" ""))))
+       (or (surround-by? input :start "")
+           (surround-by? input "\n" "")
+           (surround-by? input " " "")
+           (surround-by? input "]" "")
+           (surround-by? input "(" ""))))
 
 
 (defn wrapped-by?
 (defn wrapped-by?
   [input before end]
   [input before end]
@@ -1967,7 +1983,9 @@
                   exclude-properties
                   exclude-properties
                   target-block
                   target-block
                   sibling?
                   sibling?
-                  keep-uuid?]
+                  keep-uuid?
+                  cut-paste?
+                  revert-cut-txs]
            :or {exclude-properties []}}]
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
                         (some-> (db/pull [:block/uuid (:block/uuid editing-block)])
                         (some-> (db/pull [:block/uuid (:block/uuid editing-block)])
@@ -1982,8 +2000,7 @@
         empty-target? (string/blank? (:block/content target-block))
         empty-target? (string/blank? (:block/content target-block))
         paste-nested-blocks? (nested-blocks blocks)
         paste-nested-blocks? (nested-blocks blocks)
         target-block-has-children? (db/has-children? (:block/uuid target-block))
         target-block-has-children? (db/has-children? (:block/uuid target-block))
-        replace-empty-target? (if (and paste-nested-blocks? empty-target?
-                                       target-block-has-children?)
+        replace-empty-target? (if (and paste-nested-blocks? empty-target? target-block-has-children?)
                                 false
                                 false
                                 true)
                                 true)
         target-block' (if replace-empty-target? target-block
         target-block' (if replace-empty-target? target-block
@@ -2009,13 +2026,15 @@
         (outliner-core/save-block! editing-block)))
         (outliner-core/save-block! editing-block)))
 
 
     (outliner-tx/transact!
     (outliner-tx/transact!
-      {:outliner-op :insert-blocks}
+      {:outliner-op :insert-blocks
+       :additional-tx revert-cut-txs}
       (when target-block'
       (when target-block'
         (let [format (or (:block/format target-block') (state/get-preferred-format))
         (let [format (or (:block/format target-block') (state/get-preferred-format))
               blocks' (map (fn [block]
               blocks' (map (fn [block]
                              (paste-block-cleanup block page exclude-properties format content-update-fn keep-uuid?))
                              (paste-block-cleanup block page exclude-properties format content-update-fn keep-uuid?))
                         blocks)
                         blocks)
               result (outliner-core/insert-blocks! blocks' target-block' {:sibling? sibling?
               result (outliner-core/insert-blocks! blocks' target-block' {:sibling? sibling?
+                                                                          :cut-paste? cut-paste?
                                                                           :outliner-op :paste
                                                                           :outliner-op :paste
                                                                           :replace-empty-target? replace-empty-target?
                                                                           :replace-empty-target? replace-empty-target?
                                                                           :keep-uuid? keep-uuid?})]
                                                                           :keep-uuid? keep-uuid?})]
@@ -2205,10 +2224,18 @@
            s1 (subs value 0 selected-start)
            s1 (subs value 0 selected-start)
            s2 (subs value selected-end)]
            s2 (subs value selected-end)]
        (state/set-edit-content! (state/get-edit-input-id)
        (state/set-edit-content! (state/get-edit-input-id)
-                                (str s1 insertion s2))
-       (cursor/move-cursor-to input (+ selected-start (count insertion)))))))
+                                (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
 (defn- keydown-new-line
+  "Insert newline to current cursor position"
   []
   []
   (insert "\n"))
   (insert "\n"))
 
 
@@ -2434,7 +2461,6 @@
             (profile
             (profile
              "Insert block"
              "Insert block"
              (outliner-tx/transact! {:outliner-op :insert-blocks}
              (outliner-tx/transact! {:outliner-op :insert-blocks}
-               (save-current-block!)
                (insert-new-block! state)))))))))
                (insert-new-block! state)))))))))
 
 
 (defn- inside-of-single-block
 (defn- inside-of-single-block
@@ -2585,8 +2611,7 @@
   (state/set-edit-content! (state/get-edit-input-id) (.-value input)))
   (state/set-edit-content! (state/get-edit-input-id) (.-value input)))
 
 
 (defn- delete-concat [current-block]
 (defn- delete-concat [current-block]
-  (let [input-id (state/get-edit-input-id)
-        ^js input (state/get-input)
+  (let [^js input (state/get-input)
         current-pos (cursor/pos input)
         current-pos (cursor/pos input)
         value (gobj/get input "value")
         value (gobj/get input "value")
         right (outliner-core/get-right-sibling (:db/id current-block))
         right (outliner-core/get-right-sibling (:db/id current-block))
@@ -2608,17 +2633,27 @@
 
 
       :else
       :else
       (let [edit-block (state/get-edit-block)
       (let [edit-block (state/get-edit-block)
-            transact-opts {:outliner-op :delete-block
+            transact-opts {:outliner-op :delete-blocks
                            :concat-data {:last-edit-block (:block/uuid edit-block)
                            :concat-data {:last-edit-block (:block/uuid edit-block)
                                          :end? true}}
                                          :end? true}}
-            new-content (str value "" (:block/content next-block))
-            repo (state/get-current-repo)]
+            next-block-has-refs? (some? (:block/_refs (db/entity (:db/id next-block))))
+            new-content (if next-block-has-refs?
+                          (str value ""
+                               (->> (:block/content next-block)
+                                    (property/remove-properties (:block/format next-block))
+                                    (drawer/remove-logbook)))
+                          (str value "" (:block/content next-block)))
+            repo (state/get-current-repo)
+            edit-block' (if next-block-has-refs?
+                          (assoc edit-block
+                                 :block/uuid (:block/uuid next-block)
+                                 :block/additional-properties (dissoc (:block/properties next-block) :block/uuid))
+                          edit-block)]
         (outliner-tx/transact! transact-opts
         (outliner-tx/transact! transact-opts
-          (save-block! repo edit-block new-content)
-          (delete-block-aux! next-block false))
-
-        (state/set-edit-content! input-id new-content)
-        (cursor/move-cursor-to input current-pos)))))
+          (delete-block-aux! next-block false)
+          (save-block! repo edit-block' new-content {:editor/op :delete}))
+        (let [block (if next-block-has-refs? next-block edit-block)]
+          (edit-block! block current-pos (:block/uuid block)))))))
 
 
 (defn keydown-delete-handler
 (defn keydown-delete-handler
   [_e]
   [_e]
@@ -2832,11 +2867,11 @@
             (contains? key)
             (contains? key)
             (or (autopair-left-paren? input key)))
             (or (autopair-left-paren? input key)))
         (let [curr (get-current-input-char input)
         (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)))
+              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)))
 
 
         (let [sym "$"]
         (let [sym "$"]
           (and (= key sym)
           (and (= key sym)

+ 4 - 2
src/main/frontend/handler/editor/lifecycle.cljs

@@ -36,8 +36,10 @@
   [state]
   [state]
   (let [{:keys [value]} (get-state)]
   (let [{:keys [value]} (get-state)]
     (editor-handler/clear-when-saved!)
     (editor-handler/clear-when-saved!)
-    ;; TODO: ugly
-    (when-not (contains? #{:insert :indent-outdent :auto-save :undo :redo :delete} (state/get-editor-op))
+    (when (and
+           (not (contains? #{:insert :indent-outdent :auto-save :undo :redo :delete} (state/get-editor-op)))
+           ;; Don't trigger auto-save if the latest op is undo or redo
+           (not (contains? #{:undo :redo} (state/get-editor-latest-op))))
       (editor-handler/save-block! (get-state) value)))
       (editor-handler/save-block! (get-state) value)))
   state)
   state)
 
 

+ 3 - 4
src/main/frontend/handler/editor/property.cljs

@@ -38,9 +38,8 @@
 (defn edit-block!
 (defn edit-block!
   ([block pos id]
   ([block pos id]
    (edit-block! block pos id nil))
    (edit-block! block pos id nil))
-  ([block pos id {:keys [custom-content tail-len move-cursor? retry-times]
+  ([block pos id {:keys [custom-content tail-len retry-times]
                   :or {tail-len 0
                   :or {tail-len 0
-                       move-cursor? true
                        retry-times 0}
                        retry-times 0}
                   :as opts}]
                   :as opts}]
    (when-not (> retry-times 2)
    (when-not (> retry-times 2)
@@ -70,7 +69,7 @@
                            (drawer/remove-logbook))]
                            (drawer/remove-logbook))]
            (clear-selection!)
            (clear-selection!)
            (if edit-input-id
            (if edit-input-id
-             (state/set-editing! edit-input-id content block text-range move-cursor?)
+             (state/set-editing! edit-input-id content block text-range)
              ;; Block may not be rendered yet
              ;; Block may not be rendered yet
              (js/setTimeout (fn [] (edit-block! block pos id (update opts :retry-times inc))) 10))))))))
              (js/setTimeout (fn [] (edit-block! block pos id (update opts :retry-times inc))) 10))))))))
 
 
@@ -138,4 +137,4 @@
 (defn set-block-property!
 (defn set-block-property!
   [block-id key value]
   [block-id key value]
   (let [key (keyword key)]
   (let [key (keyword key)]
-    (batch-set-block-property! [[block-id key value]])))
+    (batch-set-block-property! [[block-id key value]])))

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

@@ -105,6 +105,15 @@
   [text]
   [text]
   (boolean (util/safe-re-find #"(?m)^\s*\*+\s+" text)))
   (boolean (util/safe-re-find #"(?m)^\s*\*+\s+" text)))
 
 
+(defn- get-revert-cut-tx
+  "Get reverted previous cut tx when paste"
+  [blocks]
+  (let [{:keys [retracted-block-ids revert-tx]} (get-in @state/state [:editor/last-replace-ref-content-tx (state/get-current-repo)])
+        recent-cut-block-ids (->> retracted-block-ids (map second) (set))]
+    (state/set-state! [:editor/last-replace-ref-content-tx (state/get-current-repo)] nil)
+    (when (= (set (map :block/uuid blocks)) recent-cut-block-ids)
+      (seq revert-tx))))
+
 (defn- paste-copied-blocks-or-text
 (defn- paste-copied-blocks-or-text
   ;; todo: logseq/whiteboard-shapes is now text/html
   ;; todo: logseq/whiteboard-shapes is now text/html
   [text e html]
   [text e html]
@@ -120,7 +129,12 @@
                               (commands/simple-insert! input-id text nil)))
                               (commands/simple-insert! input-id text nil)))
            internal-paste? (seq copied-blocks)]
            internal-paste? (seq copied-blocks)]
        (if internal-paste?
        (if internal-paste?
-         (editor-handler/paste-blocks copied-blocks {})
+         (let [revert-cut-tx (get-revert-cut-tx copied-blocks)
+               cut-paste? (boolean (seq revert-cut-tx))
+               keep-uuid? cut-paste?]
+           (editor-handler/paste-blocks copied-blocks {:revert-cut-tx revert-cut-tx
+                                                       :cut-paste? cut-paste?
+                                                       :keep-uuid? keep-uuid?}))
          (let [shape-refs-text (when (and (not (string/blank? html))
          (let [shape-refs-text (when (and (not (string/blank? html))
                                           (get-whiteboard-tldr-from-text html))
                                           (get-whiteboard-tldr-from-text html))
                                  ;; text should always be prepared block-ref generated in tldr
                                  ;; text should always be prepared block-ref generated in tldr

+ 7 - 3
src/main/frontend/modules/editor/undo_redo.cljs

@@ -7,7 +7,9 @@
             [frontend.util.page :as page-util]
             [frontend.util.page :as page-util]
             [frontend.state :as state]
             [frontend.state :as state]
             [clojure.set :as set]
             [clojure.set :as set]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.util.drawer :as drawer]
+            [frontend.util.property :as property]))
 
 
 ;;;; APIs
 ;;;; APIs
 
 
@@ -157,8 +159,10 @@
   "Prevent block auto-save during undo/redo."
   "Prevent block auto-save during undo/redo."
   []
   []
   (when-let [block (state/get-edit-block)]
   (when-let [block (state/get-edit-block)]
-    (state/set-edit-content! (state/get-edit-input-id)
-                             (:block/content (db/entity (:db/id block))))))
+    (when-let [content (:block/content (db/entity (:db/id block)))]
+      (let [content' (-> (property/remove-built-in-properties (:block/format block) content)
+                         (drawer/remove-logbook))]
+        (state/set-edit-content! (state/get-edit-input-id) content')))))
 
 
 (defn- get-next-tx-editor-cursor
 (defn- get-next-tx-editor-cursor
   [tx-id]
   [tx-id]

+ 8 - 3
src/main/frontend/modules/file/core.cljs

@@ -32,8 +32,10 @@
     content))
     content))
 
 
 (defn transform-content
 (defn transform-content
-  [{:block/keys [collapsed? format pre-block? unordered content left page parent properties]} level {:keys [heading-to-list?]}]
-  (let [heading (:heading properties)
+  [{:block/keys [collapsed? format pre-block? unordered content left page parent properties] :as b} level {:keys [heading-to-list?]}]
+  (let [block-ref-not-saved? (and (seq (:block/_refs (db/entity (:db/id b))))
+                                  (not (string/includes? content (str (:block/uuid b)))))
+        heading (:heading properties)
         markdown? (= :markdown format)
         markdown? (= :markdown format)
         content (or content "")
         content (or content "")
         pre-block? (or pre-block?
         pre-block? (or pre-block?
@@ -80,7 +82,10 @@
                                     (string/blank? new-content))
                                     (string/blank? new-content))
                               ""
                               ""
                               " ")]
                               " ")]
-                    (str prefix sep new-content)))]
+                    (str prefix sep new-content)))
+        content (if block-ref-not-saved?
+                  (property/insert-property format content :id (str (:block/uuid b)))
+                  content)]
     content))
     content))
 
 
 
 

+ 11 - 2
src/main/frontend/modules/outliner/core.cljs

@@ -528,10 +528,11 @@
                     For example, if `blocks` are from internal copy, the uuids
                     For example, if `blocks` are from internal copy, the uuids
                     need to be changed, but there's no need for drag & drop.
                     need to be changed, but there's no need for drag & drop.
       `outliner-op`: what's the current outliner operation.
       `outliner-op`: what's the current outliner operation.
+      `cut-paste?`: whether it's pasted from cut blocks
       `replace-empty-target?`: If the `target-block` is an empty block, whether
       `replace-empty-target?`: If the `target-block` is an empty block, whether
                                to replace it, it defaults to be `false`.
                                to replace it, it defaults to be `false`.
     ``"
     ``"
-  [blocks target-block {:keys [sibling? keep-uuid? outliner-op replace-empty-target?] :as opts}]
+  [blocks target-block {:keys [sibling? keep-uuid? outliner-op replace-empty-target? cut-paste?] :as opts}]
   {:pre [(seq blocks)
   {:pre [(seq blocks)
          (s/valid? ::block-map-or-entity target-block)]}
          (s/valid? ::block-map-or-entity target-block)]}
   (let [target-block' (get-target-block target-block)
   (let [target-block' (get-target-block target-block)
@@ -587,7 +588,10 @@
                       (when-let [left (last (filter (fn [b] (= 1 (:block/level b))) tx))]
                       (when-let [left (last (filter (fn [b] (= 1 (:block/level b))) tx))]
                         [{:block/uuid (tree/-get-id next)
                         [{:block/uuid (tree/-get-id next)
                           :block/left (:db/id left)}]))
                           :block/left (:db/id left)}]))
-            full-tx (util/concat-without-nil uuids-tx tx next-tx)]
+            cut-target-tx (when (and cut-paste? replace-empty-target?)
+                            [{:db/id (:db/id target-block')
+                              :block/uuid (:block/uuid (first blocks'))}])
+            full-tx (util/concat-without-nil uuids-tx tx next-tx cut-target-tx)]
         (when (and replace-empty-target? (state/editing?))
         (when (and replace-empty-target? (state/editing?))
           (state/set-edit-content! (state/get-edit-input-id) (:block/content (first blocks))))
           (state/set-edit-content! (state/get-edit-input-id) (:block/content (first blocks))))
         {:tx-data full-tx
         {:tx-data full-tx
@@ -859,6 +863,11 @@
   see also `frontend.modules.outliner.transaction/transact!`"
   see also `frontend.modules.outliner.transaction/transact!`"
   nil)
   nil)
 
 
+(def ^:private ^:dynamic #_:clj-kondo/ignore *transaction-opts*
+  "Stores transaction opts that are generated by one or more write-operations,
+  see also `frontend.modules.outliner.transaction/transact!`"
+  nil)
+
 (defn- op-transact!
 (defn- op-transact!
   [fn-var & args]
   [fn-var & args]
   {:pre [(var? fn-var)]}
   {:pre [(var? fn-var)]}

+ 84 - 4
src/main/frontend/modules/outliner/datascript.cljc

@@ -10,7 +10,11 @@
                      [frontend.config :as config]
                      [frontend.config :as config]
                      [logseq.graph-parser.util :as gp-util]
                      [logseq.graph-parser.util :as gp-util]
                      [lambdaisland.glogi :as log]
                      [lambdaisland.glogi :as log]
-                     [frontend.search :as search])))
+                     [frontend.search :as search]
+                     [clojure.string :as string]
+                     [frontend.util :as util]
+                     [frontend.util.property :as property]
+                     [logseq.graph-parser.util.block-ref :as block-ref])))
 
 
 #?(:cljs
 #?(:cljs
    (defn new-outliner-txs-state [] (atom [])))
    (defn new-outliner-txs-state [] (atom [])))
@@ -51,6 +55,71 @@
      [tx-report]
      [tx-report]
      (get-in tx-report [:tempids :db/current-tx])))
      (get-in tx-report [:tempids :db/current-tx])))
 
 
+#?(:cljs
+   (defn update-block-refs
+     [txs opts]
+     (if-let [changed (:uuid-changed opts)]
+       (let [{:keys [from to]} changed
+             from-e (db/entity [:block/uuid from])
+             to-e (db/entity [:block/uuid to])
+             from-id (:db/id from-e)
+             to-id (:db/id to-e)
+             from-refs (:block/_refs from-e)
+             from-path-refs (:block/_path-refs from-e)
+             to-refs (:block/_refs to-e)
+             from-refs-txs (mapcat (fn [ref]
+                                     (let [id (:db/id ref)]
+                                       [[:db/retract id :block/refs from-id]
+                                        [:db/add id :block/refs to-id]])) from-refs)
+             from-path-refs-txs (mapcat (fn [ref]
+                                          (let [id (:db/id ref)]
+                                            [[:db/retract id :block/path-refs from-id]
+                                             [:db/add id :block/path-refs to-id]])) from-path-refs)
+             to-refs-txs (mapcat (fn [ref]
+                                        (let [id (:db/id ref)
+                                              new-content (string/replace (:block/content ref)
+                                                                          (block-ref/->block-ref to)
+                                                                          (block-ref/->block-ref from))]
+                                          [[:db/add id :block/content new-content]])) to-refs)]
+         (concat txs from-refs-txs from-path-refs-txs to-refs-txs))
+       txs)))
+
+#?(:cljs
+   (defn replace-ref-with-content
+     [txs opts]
+     (if (and (= :delete-blocks (:outliner-op opts))
+              (empty? (:uuid-changed opts)))
+       (let [retracted-block-ids (->> (keep (fn [tx]
+                                              (when (and (vector? tx)
+                                                         (= :db.fn/retractEntity (first tx)))
+                                                (second tx))) txs))
+             retracted-blocks (map db/entity retracted-block-ids)
+             retracted-tx (->> (for [block retracted-blocks]
+                                 (let [refs (:block/_refs block)]
+                                   (map (fn [ref]
+                                          (let [id (:db/id ref)
+                                                block-content (property/remove-properties (:block/format block) (:block/content block))
+                                                new-content (-> (:block/content ref)
+                                                                (string/replace (re-pattern (util/format "(?i){{embed \\(\\(%s\\)\\)\\s?}}" (str (:block/uuid block))))
+                                                                                block-content)
+                                                                (string/replace (block-ref/->block-ref (str (:block/uuid block)))
+                                                                                block-content))]
+                                            {:tx [[:db/retract (:db/id ref) :block/refs (:db/id block)]
+                                                  [:db/retract (:db/id ref) :block/path-refs (:db/id block)]
+                                                  [:db/add id :block/content new-content]]
+                                             :revert-tx [[:db/add (:db/id ref) :block/refs (:db/id block)]
+                                                         [:db/add (:db/id ref) :block/path-refs (:db/id block)]
+                                                         [:db/add id :block/content (:block/content ref)]]})) refs)))
+                               (apply concat))
+             retracted-tx' (mapcat :tx retracted-tx)
+             revert-tx (mapcat :revert-tx retracted-tx)]
+         (when (seq retracted-tx')
+           (state/set-state! [:editor/last-replace-ref-content-tx (state/get-current-repo)]
+                             {:retracted-block-ids retracted-block-ids
+                              :revert-tx revert-tx}))
+         (concat txs retracted-tx'))
+       txs)))
+
 #?(:cljs
 #?(:cljs
    (defn transact!
    (defn transact!
      [txs opts before-editor-cursor]
      [txs opts before-editor-cursor]
@@ -58,15 +127,26 @@
            txs (map (fn [m] (if (map? m)
            txs (map (fn [m] (if (map? m)
                               (dissoc m
                               (dissoc m
                                       :block/children :block/meta :block/top? :block/bottom? :block/anchor
                                       :block/children :block/meta :block/top? :block/bottom? :block/anchor
-                                      :block/title :block/body :block/level :block/container :db/other-tx)
-                              m)) txs)]
+                                      :block/title :block/body :block/level :block/container :db/other-tx
+                                      :block/additional-properties)
+                              m)) txs)
+           txs (cond-> txs
+                 (:uuid-changed opts)
+                 (update-block-refs opts)
+
+                 (and (= :delete-blocks (:outliner-op opts))
+                      (empty? (:uuid-changed opts)))
+                 (replace-ref-with-content opts)
+
+                 true
+                 (distinct))]
        (when (and (seq txs)
        (when (and (seq txs)
                   (not (:skip-transact? opts))
                   (not (:skip-transact? opts))
                   (not (contains? (:file/unlinked-dirs @state/state)
                   (not (contains? (:file/unlinked-dirs @state/state)
                                   (config/get-repo-dir (state/get-current-repo)))))
                                   (config/get-repo-dir (state/get-current-repo)))))
 
 
          ;; (prn "[DEBUG] Outliner transact:")
          ;; (prn "[DEBUG] Outliner transact:")
-         ;; (frontend.util/pprint txs)
+         ;; (frontend.util/pprint {:txs txs :opts opts})
 
 
          (try
          (try
            (let [repo (get opts :repo (state/get-current-repo))
            (let [repo (get opts :repo (state/get-current-repo))

+ 13 - 4
src/main/frontend/modules/outliner/transaction.cljc

@@ -23,22 +23,31 @@
     (move-blocks! ...)
     (move-blocks! ...)
     (delete-blocks! ...))"
     (delete-blocks! ...))"
   [opts & body]
   [opts & body]
-  (assert (or (map? opts) (symbol? opts)) (str "opts is not a map or symbol, type: " (type opts) ))
+  (assert (or (map? opts) (symbol? opts)) (str "opts is not a map or symbol, type: " (type opts)))
   `(let [transact-data# frontend.modules.outliner.core/*transaction-data*
   `(let [transact-data# frontend.modules.outliner.core/*transaction-data*
+         transaction-opts# frontend.modules.outliner.core/*transaction-opts*
          opts# (if transact-data#
          opts# (if transact-data#
                  (assoc ~opts :nested-transaction? true)
                  (assoc ~opts :nested-transaction? true)
                  ~opts)
                  ~opts)
          before-editor-cursor# (frontend.state/get-current-edit-block-and-position)]
          before-editor-cursor# (frontend.state/get-current-edit-block-and-position)]
      (if transact-data#
      (if transact-data#
-       (do ~@body)
-       (binding [frontend.modules.outliner.core/*transaction-data* (transient [])]
+       (do
+         (when transaction-opts#
+           (conj! transaction-opts# opts#))
+         ~@body)
+       (binding [frontend.modules.outliner.core/*transaction-data* (transient [])
+                 frontend.modules.outliner.core/*transaction-opts* (transient [])]
+         (conj! frontend.modules.outliner.core/*transaction-opts* transaction-opts# opts#)
          ~@body
          ~@body
          (let [r# (persistent! frontend.modules.outliner.core/*transaction-data*)
          (let [r# (persistent! frontend.modules.outliner.core/*transaction-data*)
                tx# (mapcat :tx-data r#)
                tx# (mapcat :tx-data r#)
                ;; FIXME: should we merge all the tx-meta?
                ;; FIXME: should we merge all the tx-meta?
                tx-meta# (first (map :tx-meta r#))
                tx-meta# (first (map :tx-meta r#))
                all-tx# (concat tx# (:additional-tx opts#))
                all-tx# (concat tx# (:additional-tx opts#))
-               opts## (merge (dissoc opts# :additional-tx) tx-meta#)]
+               o# (persistent! frontend.modules.outliner.core/*transaction-opts*)
+               full-opts# (apply merge (reverse o#))
+               opts## (merge (dissoc full-opts# :additional-tx :current-block :nested-transaction?) tx-meta#)]
+
            (when (seq all-tx#) ;; If it's empty, do nothing
            (when (seq all-tx#) ;; If it's empty, do nothing
              (when-not (:nested-transaction? opts#) ; transact only for the whole transaction
              (when-not (:nested-transaction? opts#) ; transact only for the whole transaction
                (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts## before-editor-cursor#)]
                (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts## before-editor-cursor#)]

+ 13 - 3
src/main/frontend/state.cljs

@@ -105,6 +105,8 @@
 
 
      :config                                {}
      :config                                {}
      :block/component-editing-mode?         false
      :block/component-editing-mode?         false
+     :editor/op                             nil
+     :editor/latest-op                      nil
      :editor/hidden-editors                 #{}             ;; page names
      :editor/hidden-editors                 #{}             ;; page names
      :editor/draw-mode?                     false
      :editor/draw-mode?                     false
      :editor/action                         nil
      :editor/action                         nil
@@ -123,6 +125,9 @@
      :editor/on-paste?                      false
      :editor/on-paste?                      false
      :editor/last-key-code                  nil
      :editor/last-key-code                  nil
 
 
+     ;; Stores deleted refed blocks, indexed by repo
+     :editor/last-replace-ref-content-tx    nil
+
      ;; for audio record
      ;; for audio record
      :editor/record-status                  "NONE"
      :editor/record-status                  "NONE"
 
 
@@ -1694,13 +1699,18 @@ Similar to re-frame subscriptions"
 
 
 ;; TODO: Move those to the uni `state`
 ;; TODO: Move those to the uni `state`
 
 
-(defonce editor-op (atom nil))
 (defn set-editor-op!
 (defn set-editor-op!
   [value]
   [value]
-  (reset! editor-op value))
+  (set-state! :editor/op value)
+  (when value (set-state! :editor/latest-op value)))
+
 (defn get-editor-op
 (defn get-editor-op
   []
   []
-  @editor-op)
+  (:editor/op @state))
+
+(defn get-editor-latest-op
+  []
+  (:editor/latest-op @state))
 
 
 (defn get-events-chan
 (defn get-events-chan
   []
   []

+ 7 - 0
src/main/frontend/util.cljc

@@ -398,6 +398,13 @@
    (defn stop-propagation [e]
    (defn stop-propagation [e]
      (when e (.stopPropagation e))))
      (when e (.stopPropagation e))))
 
 
+#?(:cljs
+   (defn nearest-scrollable-container [^js/HTMLElement element]
+     (some #(when-let [overflow-y (.-overflowY (js/window.getComputedStyle %))]
+              (when (contains? #{"auto" "scroll" "overlay"} overflow-y)
+                %))
+           (take-while (complement nil?) (iterate #(.-parentElement %) element)))))
+
 #?(:cljs
 #?(:cljs
    (defn element-top [elem top]
    (defn element-top [elem top]
      (when elem
      (when elem

+ 5 - 5
src/test/frontend/handler/repo_conversion_test.cljs

@@ -17,11 +17,11 @@
                      :after test-helper/destroy-test-db!})
                      :after test-helper/destroy-test-db!})
 
 
 (defn- query-assertions-v067
 (defn- query-assertions-v067
-  [db files]
+  [db graph-dir files]
   (testing "Query based stats"
   (testing "Query based stats"
     (is (= (->> files
     (is (= (->> files
                 ;; logseq files aren't saved under :block/file
                 ;; logseq files aren't saved under :block/file
-                (remove #(string/includes? % (str "/" gp-config/app-name "/")))
+                (remove #(string/includes? % (str graph-dir "/" gp-config/app-name "/")))
                 set)
                 set)
            (->> (d/q '[:find (pull ?b [* {:block/file [:file/path]}])
            (->> (d/q '[:find (pull ?b [* {:block/file [:file/path]}])
                        :where [?b :block/name] [?b :block/file]]
                        :where [?b :block/name] [?b :block/file]]
@@ -93,7 +93,7 @@
   logseq app. It is important to run these in both contexts to ensure that the
   logseq app. It is important to run these in both contexts to ensure that the
   functionality in frontend.handler.repo and logseq.graph-parser remain the
   functionality in frontend.handler.repo and logseq.graph-parser remain the
   same"
   same"
-  [db files]
+  [db graph-dir files]
   ;; Counts assertions help check for no major regressions. These counts should
   ;; Counts assertions help check for no major regressions. These counts should
   ;; only increase over time as the docs graph rarely has deletions
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
   (testing "Counts"
@@ -113,7 +113,7 @@
                  db)))
                  db)))
         "Advanced query count"))
         "Advanced query count"))
 
 
-  (query-assertions-v067 db files))
+  (query-assertions-v067 db graph-dir files))
 
 
 (defn- convert-to-triple-lowbar
 (defn- convert-to-triple-lowbar
   [path]
   [path]
@@ -144,4 +144,4 @@
         db (conn/get-db test-helper/test-db)]
         db (conn/get-db test-helper/test-db)]
 
 
     ;; Result under new naming rule after conversion should be the same as the old one
     ;; Result under new naming rule after conversion should be the same as the old one
-    (docs-graph-assertions-v067 db (map :file/path files))))
+    (docs-graph-assertions-v067 db graph-dir (map :file/path files))))

+ 1 - 1
src/test/frontend/handler/repo_test.cljs

@@ -22,7 +22,7 @@
             (repo-handler/parse-files-and-load-to-db! test-helper/test-db files {:re-render? false :verbose false}))
             (repo-handler/parse-files-and-load-to-db! test-helper/test-db files {:re-render? false :verbose false}))
         db (conn/get-db test-helper/test-db)]
         db (conn/get-db test-helper/test-db)]
 
 
-    (docs-graph-helper/docs-graph-assertions db (map :file/path files))))
+    (docs-graph-helper/docs-graph-assertions db graph-dir (map :file/path files))))
 
 
 (deftest parse-files-and-load-to-db-with-block-refs-on-reload
 (deftest parse-files-and-load-to-db-with-block-refs-on-reload
   (testing "Refs to blocks on a page are retained if that page is reloaded"
   (testing "Refs to blocks on a page are retained if that page is reloaded"

+ 9 - 9
templates/config.edn

@@ -13,7 +13,7 @@
  :preferred-workflow :now
  :preferred-workflow :now
 
 
  ;; Exclude directories/files.
  ;; Exclude directories/files.
- ;; Example usage: 
+ ;; Example usage:
  ;; :hidden ["/archived" "/test.md" "../assets/archived"]
  ;; :hidden ["/archived" "/test.md" "../assets/archived"]
  :hidden []
  :hidden []
 
 
@@ -87,7 +87,7 @@
 
 
  ;; Specify the first day of the week.
  ;; Specify the first day of the week.
  ;; Available options:
  ;; Available options:
- ;;  - integer from 0 to 6 (Monday to Sunday) 
+ ;;  - integer from 0 to 6 (Monday to Sunday)
  ;; Default value: 6 (Sunday)
  ;; Default value: 6 (Sunday)
  :start-of-week 6
  :start-of-week 6
 
 
@@ -102,7 +102,7 @@
  ;; :custom-js-url "https://cdn.logseq.com/custom.js"
  ;; :custom-js-url "https://cdn.logseq.com/custom.js"
 
 
  ;; Set a custom Arweave gateway
  ;; Set a custom Arweave gateway
- ;; Default value: https://arweave.net
+ ;; Default gateway: https://arweave.net
  ;; :arweave/gateway "https://arweave.net"
  ;; :arweave/gateway "https://arweave.net"
 
 
  ;; Set bullet indentation when exporting
  ;; Set bullet indentation when exporting
@@ -154,7 +154,7 @@
  ;; Configure custom shortcuts.
  ;; Configure custom shortcuts.
  ;; Syntax:
  ;; Syntax:
  ;; 1. + indicates simultaneous key presses, e.g., `Ctrl+Shift+a`.
  ;; 1. + indicates simultaneous key presses, e.g., `Ctrl+Shift+a`.
- ;; 2. A space between keys represents key chords, e.g., `t s` means 
+ ;; 2. A space between keys represents key chords, e.g., `t s` means
  ;;    pressing `t` followed by `s`.
  ;;    pressing `t` followed by `s`.
  ;; 3. mod refers to `Ctrl` for Windows/Linux and `Command` for Mac.
  ;; 3. mod refers to `Ctrl` for Windows/Linux and `Command` for Mac.
  ;; 4. Use false to disable a specific shortcut.
  ;; 4. Use false to disable a specific shortcut.
@@ -280,7 +280,7 @@
  :ref/default-open-blocks-level 2
  :ref/default-open-blocks-level 2
 
 
  ;; Configure the threshold for linked references before collapsing.
  ;; Configure the threshold for linked references before collapsing.
- ;; Default value: 50
+ ;; Default value: 100
  :ref/linked-references-collapsed-threshold 50
  :ref/linked-references-collapsed-threshold 50
 
 
  ;; Graph view configuration.
  ;; Graph view configuration.
@@ -325,7 +325,7 @@
  ;; Properties that are ignored when parsing property values for references
  ;; Properties that are ignored when parsing property values for references
  ;; Example usage:
  ;; Example usage:
  ;; :ignored-page-references-keywords #{:author :website}
  ;; :ignored-page-references-keywords #{:author :website}
- 
+
  ;; logbook configuration.
  ;; logbook configuration.
  ;; :logbook/settings
  ;; :logbook/settings
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
@@ -341,7 +341,7 @@
  ;; Mobile features options
  ;; Mobile features options
  ;; Gestures
  ;; Gestures
  ;; Example usage:
  ;; Example usage:
- ;; :mobile 
+ ;; :mobile
  ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
  ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
 
 
  ;; Extra CodeMirror options
  ;; Extra CodeMirror options
@@ -353,11 +353,11 @@
  ;;  :readOnly      false} ; Default value: false
  ;;  :readOnly      false} ; Default value: false
 
 
  ;; Enable logical outdenting
  ;; Enable logical outdenting
- ;; Default value: false 
+ ;; Default value: false
  ;; :editor/logical-outdenting? false
  ;; :editor/logical-outdenting? false
 
 
  ;; Prefer pasting the file when text and a file are in the clipboard.
  ;; Prefer pasting the file when text and a file are in the clipboard.
- ;; Default value: false 
+ ;; Default value: false
  ;; :editor/preferred-pasting-file? false
  ;; :editor/preferred-pasting-file? false
 
 
  ;; Quick capture templates for receiving content from other apps.
  ;; Quick capture templates for receiving content from other apps.