Core outliner operations refactoring (#4880)

* Add outliner nested transact!

Copied the code mostly from https://github.com/logseq/logseq/pull/4671
by zhiyuan

* refactor: insert-blocks

* fix: insert-blocks

* fix: move cursor to the last block when inserting

* fix: replace the current block when inserting and its content is empty

* keep only :insert-blocks

* expose only :delete-blocks

* Use existing implementations for move-nodes-up-down and

indent/outdent.

* fix editing state not updated immediately

* fix editing status

* fix: avoid recursive copy

* fix: inserting blocks after an empty block

* Implement move-blocks with insert-blocks

* fix: block left

* Implement move-blocks-up-down with move-blocks

* fix: paste text

* Implement indent-outdent-blocks with move-blocks

* fix: indent/outdent

* feat: multiple blocks drag && drop

* fix: indent/outdent blocks

* fix: drag drop

* Port unit tests for outliner.core

* enhance: open collapsed parent when indenting blocks

* refactor: block selection

* fix: indent/outdent blocks with different levels

* Add instrument on invalid outliner structure

* fix: can't write a block if the page has any outdated blocks

* fix: editing status for empty page

* fix: multiple drag & drop

* fix: drag & drop disallows moving from parents to its child

* fix: public property

* fix: can't delete first empty block

* Remove unused code

* fix: e2e tests

A workaround is to not select/highlight the block when pressing esc if it has
fenced code.

* remove unused code

* Add batch transaction test

* fix: update :block/page when dragging targets' children to another page

* Add more tests

* Simplify extract

* Replace db/get-conn with db/get-db

* Simplify extracting blocks from ast

* Code cleanup

* Code cleanup

* Add outliner core fuzzy tests

* Remove unused code

* fix: cursor not jump to the upper block when pressing Enter in the beginning

* fix: Enter in the beginning of a non-empty block

* Fix lint warnings

* Add editor random e2e tests

* Fix typo

* enhance: move some fns and add some comments

* enhance(outliner): add page-block? util

* fix: increase td width to prevent content overflow

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* First pass at file tests for file-sync

Each action usually passes by 5th try

* Fix two incorrect calls caught by tests

* More test improvements

- Easier auth setup
- subdirectory is configurable
- list graphs api also exercised

* Address cleanup from #3839

- Remove unused translation key
- Delete or TODO commented code
- Capitalize notifications to users

* fix quick capture template not working

* enhance(sync): add logout

* enhance: add logout i18n

* fix(plugin): sometimes plugin settings of gui not work when entry from app settings

* enable show-brackets? toggle for orgmode [[file:./pages/demo.org][demo]]

* fix(sync): fix unfinishable sync loop

* feature: logseq protocol; refactor persistGraph

* fix: deeplink support

* fix: broadcast persist graph on opening new graph with logseq protocol

* feat: logseq protocol open action for page-name and uuid

* fix: logseq protocol graph param validation

* ux: copy logseq URL of block

* enhance: remove the redundant 'open' from logseq protocol (v0.1)

* ux: page dropdown button for copy page URL

* chore: logseq protocol comments

* don't create new contents file when changing format

Logseq now creates a new contents file when users try to toggle the
preferred format, which causes file duplications error.

* fix pasting in src block not working on iOS

close https://github.com/logseq/logseq/issues/4914

* fix playing video goes into editing mode on iOS

* fix copy to clipboard failure on iOS

* add Podfile item

* fix mobile toolbar order not persisting after restart

* test(e2e): add test for backspace and cursor pos (#4896)

* test(e2e): add test for backspace and cursor pos
* fix(test): refine, fix wrong helper

* fix(ui): warn about illegal git commit interval

* enhance(editor): allow global git cmd shortcut

* style(settings): line-space of general/journals

* enhance(editor): accept enter in dummy block

Fix #4931

* fix editing state not updated immediately

* fix: can't write a block if the page has any outdated blocks

TODO: clean outdated blocks

* fix: editing status for empty page

* Random tree for outliner core tests

* Add pre assertions and fn docs based on Zhiyuan's suggestions

* Made some changes based on Gabriel's suggestions

* fix: tests

* fix: save current block before moving

* Updated the timeout to 100ms based on llcc's suggestion

https://github.com/logseq/logseq/pull/4880#discussion_r851966301

* api-insert-new-block! supports replace-empty-target?

* fix: replace all :reuse-last-block? usage

Co-authored-by: rcmerci <rcmerci@gmail.com>
Co-authored-by: Yue Yang <g1enyy0ung@gmail.com>
Co-authored-by: Gabriel Horner <gabriel@logseq.com>
Co-authored-by: llcc <lzhes43@gmail.com>
Co-authored-by: charlie <xyhp915@qq.com>
Co-authored-by: Junyi Du <junyidu.cn@gmail.com>
Co-authored-by: Andelf <andelf@gmail.com>
This commit is contained in:
Tienson Qin
2022-04-19 11:14:38 +08:00
committed by GitHub
parent b18c225c02
commit 904eff6d9d
44 changed files with 1856 additions and 1790 deletions

View File

@@ -341,7 +341,7 @@
refs (distinct (concat (:refs block) ref-blocks))]
(assoc block :refs refs))))
(defn block-keywordize
(defn- block-keywordize
[block]
(medley/map-keys
(fn [k]
@@ -350,7 +350,7 @@
(keyword "block" k)))
block))
(defn safe-blocks
(defn- sanity-blocks-data
[blocks]
(map (fn [block]
(if (map? block)
@@ -414,15 +414,13 @@
block))
(defn- get-block-content
[utf8-content block format block-content]
(let [meta (:meta block)
content (or block-content
(if-let [end-pos (:end-pos meta)]
(utf8/substring utf8-content
(:start-pos meta)
end-pos)
(utf8/substring utf8-content
(:start-pos meta))))
[utf8-content block format meta]
(let [content (if-let [end-pos (:end_pos meta)]
(utf8/substring utf8-content
(:start_pos meta)
end-pos)
(utf8/substring utf8-content
(:start_pos meta)))
content (when content
(let [content (text/remove-level-spaces content format)]
(if (or (:pre-block? block)
@@ -465,25 +463,24 @@
block-tags->pages
(update :refs (fn [col] (remove nil? col)))))
(defn extract-blocks*
[blocks body pre-block-properties encoded-content with-body?]
(defn with-pre-block-if-exists
[blocks body pre-block-properties encoded-content]
(let [first-block (first blocks)
first-block-start-pos (get-in first-block [:block/meta :start-pos])
first-block-start-pos (get-in first-block [:block/meta :start_pos])
;; Add pre-block
blocks (if (or (> first-block-start-pos 0)
(empty? blocks))
(cons
(merge
(let [content (utf8/substring encoded-content 0 first-block-start-pos)
{:keys [properties properties-order]} @pre-block-properties
{:keys [properties properties-order]} pre-block-properties
id (get-custom-id-or-new-id {:properties properties})
property-refs (->> (get-page-refs-from-properties properties)
(map :block/original-name))
block {:uuid id
:content content
:level 1
:meta {:start-pos 0
:end-pos (or first-block-start-pos
(utf8/length encoded-content))}
:properties properties
:properties-order properties-order
:refs property-refs
@@ -494,120 +491,102 @@
(block-keywordize block))
(select-keys first-block [:block/format :block/page]))
blocks)
blocks)
blocks (map (fn [block]
(if with-body?
block
(dissoc block :block/body))) blocks)]
blocks)]
(with-path-refs blocks)))
(defn ^:large-vars/cleanup-todo extract-blocks
([blocks content with-id? format]
(extract-blocks blocks content with-id? format false))
([blocks content with-id? format with-body?]
(try
(defn- construct-block
[block properties timestamps body encoded-content format pos-meta with-id?]
(let [id (get-custom-id-or-new-id properties)
ref-pages-in-properties (->> (:page-refs properties)
(remove string/blank?))
block (second block)
unordered? (:unordered block)
markdown-heading? (and (:size block) (= :markdown format))
block (if markdown-heading?
(assoc block
:type :heading
:level (if unordered? (:level block) 1)
:heading-level (or (:size block) 6))
block)
block (cond->
(assoc block
:uuid id
:refs ref-pages-in-properties
:format format
:meta pos-meta)
(seq (:properties properties))
(assoc :properties (:properties properties))
(seq (:properties-order properties))
(assoc :properties-order (:properties-order properties)))
block (if (get-in block [:properties :collapsed])
(assoc block :collapsed? true)
block)
block (assoc block
:content (get-block-content encoded-content block format pos-meta))
block (if (seq timestamps)
(merge block (timestamps->scheduled-and-deadline timestamps))
block)
block (assoc block :body body)
block (with-page-block-refs block with-id?)
{:keys [created-at updated-at]} (:properties properties)
block (cond-> block
(and created-at (integer? created-at))
(assoc :block/created-at created-at)
(and updated-at (integer? updated-at))
(assoc :block/updated-at updated-at))]
(dissoc block :title :body :anchor)))
(defn extract-blocks
"Extract headings from mldoc ast.
Args:
`blocks`: mldoc ast.
`content`: markdown or org-mode text.
`with-id?`: If `with-id?` equals to true, all the referenced pages will have new db ids.
`format`: content's format, it could be either :markdown or :org-mode."
[blocks content with-id? format]
{:pre [(seq blocks) (string? content) (boolean? with-id?) (contains? #{:markdown :org} format)]}
(try
(let [encoded-content (utf8/encode content)
last-pos (utf8/length encoded-content)
pre-block-properties (atom nil)
[blocks body]
[blocks body pre-block-properties]
(loop [headings []
blocks (reverse blocks)
timestamps {}
properties {}
last-pos last-pos
last-level 1000
children []
block-all-content []
body []]
(if (seq blocks)
(let [[block {:keys [start_pos _end_pos] :as block-content}] (first blocks)
block-content (when (string? block-content) block-content)
unordered? (:unordered (second block))
markdown-heading? (and (:size (second block)) (= :markdown format))]
(let [[block pos-meta] (first blocks)
;; fix start_pos
pos-meta (assoc pos-meta :end_pos
(if (seq headings)
(get-in (last headings) [:meta :start_pos])
nil))]
(cond
(paragraph-timestamp-block? block)
(let [timestamps (extract-timestamps block)
timestamps' (merge timestamps timestamps)]
(recur headings (rest blocks) timestamps' properties last-pos last-level children (conj block-all-content block-content) body))
(recur headings (rest blocks) timestamps' properties body))
(property/properties-ast? block)
(let [properties (extract-properties format (second block))]
(recur headings (rest blocks) timestamps properties last-pos last-level children (conj block-all-content block-content) body))
(recur headings (rest blocks) timestamps properties body))
(heading-block? block)
(let [id (get-custom-id-or-new-id properties)
ref-pages-in-properties (->> (:page-refs properties)
(remove string/blank?))
block (second block)
block (if markdown-heading?
(assoc block
:type :heading
:level (if unordered? (:level block) 1)
:heading-level (or (:size block) 6))
block)
level (:level block)
[children current-block-children]
(cond
(< level last-level)
(let [current-block-children (set (->> (filter #(< level (second %)) children)
(map first)
(map (fn [id]
[:block/uuid id]))))
others (vec (remove #(< level (second %)) children))]
[(conj others [id level])
current-block-children])
(>= level last-level)
[(conj children [id level])
#{}])
block (cond->
(assoc block
:uuid id
:refs ref-pages-in-properties
:children (or current-block-children [])
:format format)
(seq (:properties properties))
(assoc :properties (:properties properties))
(seq (:properties-order properties))
(assoc :properties-order (:properties-order properties)))
block (if (get-in block [:properties :collapsed])
(assoc block :collapsed? true)
block)
block (-> block
(assoc-in [:meta :start-pos] start_pos)
(assoc-in [:meta :end-pos] last-pos)
((fn [block]
(assoc block
:content (get-block-content encoded-content block format (and block-content (string/join "\n" (reverse (conj block-all-content block-content)))))))))
block (if (seq timestamps)
(merge block (timestamps->scheduled-and-deadline timestamps))
block)
block (assoc block :body body)
block (with-page-block-refs block with-id?)
last-pos' (get-in block [:meta :start-pos])
{:keys [created-at updated-at]} (:properties properties)
block (cond-> block
(and created-at (integer? created-at))
(assoc :block/created-at created-at)
(and updated-at (integer? updated-at))
(assoc :block/updated-at updated-at))]
(recur (conj headings block) (rest blocks) {} {} last-pos' (:level block) children [] []))
(let [block (construct-block block properties timestamps body encoded-content format pos-meta with-id?)]
(recur (conj headings block) (rest blocks) {} {} []))
:else
(recur headings (rest blocks) timestamps properties last-pos last-level children
(conj block-all-content block-content)
(conj body block))))
(do
(when (seq properties)
(reset! pre-block-properties properties))
[(-> (reverse headings)
safe-blocks) body])))]
(extract-blocks* blocks body pre-block-properties encoded-content with-body?))
(recur headings (rest blocks) timestamps properties (conj body block))))
[(-> (reverse headings)
sanity-blocks-data)
body
properties]))
result (with-pre-block-if-exists blocks body pre-block-properties encoded-content)]
(map #(dissoc % :block/meta) result))
(catch js/Error e
(js/console.error "extract-blocks-failed")
(log/error :exception e)))))
(log/error :exception e))))
(defn with-parent-and-left
[page-id blocks]
@@ -615,7 +594,6 @@
parents [{:page/id page-id ; db id or a map {:block/name "xxx"}
:block/level 0
:block/level-spaces 0}]
_sibling nil
result []]
(if (empty? blocks)
(map #(dissoc % :block/level-spaces) result)
@@ -623,7 +601,7 @@
level-spaces (:block/level-spaces block)
{:block/keys [uuid level parent] :as last-parent} (last parents)
parent-spaces (:block/level-spaces last-parent)
[blocks parents sibling result]
[blocks parents result]
(cond
(= level-spaces parent-spaces) ; sibling
(let [block (assoc block
@@ -632,7 +610,7 @@
:block/level level)
parents' (conj (vec (butlast parents)) block)
result' (conj result block)]
[others parents' block result'])
[others parents' result'])
(> level-spaces parent-spaces) ; child
(let [parent (if uuid [:block/uuid uuid] (:page/id last-parent))
@@ -649,7 +627,7 @@
(assoc :block/level (inc level)))
parents' (conj parents block)
result' (conj result block)]
[others parents' block result'])
[others parents' result'])
(< level-spaces parent-spaces)
(cond
@@ -660,7 +638,7 @@
:block/level (dec level)
:block/left [:block/uuid (:block/uuid left)])
(rest blocks))]
[blocks parents' left result])
[blocks parents' result])
:else
(let [[f r] (split-with (fn [p] (<= (:block/level-spaces p) level-spaces)) parents)
@@ -677,8 +655,8 @@
parents' (->> (concat f [block]) vec)
result' (conj result block)]
[others parents' block result'])))]
(recur blocks parents sibling result)))))
[others parents' result'])))]
(recur blocks parents result)))))
(defn parse-block
([block]