1
0
Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master' into refactor/common.css

# Conflicts:
#	resources/css/common.css
charlie 4 жил өмнө
parent
commit
d118c9d201
34 өөрчлөгдсөн 2900 нэмэгдсэн , 380 устгасан
  1. 1 1
      resources/css/table.css
  2. 3 2
      src/main/frontend/components/block.cljs
  3. 7 23
      src/main/frontend/components/editor.cljs
  4. 3 2
      src/main/frontend/components/journal.cljs
  5. 6 4
      src/main/frontend/components/page.cljs
  6. 3 2
      src/main/frontend/components/repo.cljs
  7. 3 2
      src/main/frontend/components/right_sidebar.cljs
  8. 3 0
      src/main/frontend/components/right_sidebar.css
  9. 30 105
      src/main/frontend/db.cljs
  10. 83 0
      src/main/frontend/db/conn.cljs
  11. 54 0
      src/main/frontend/db/debug.cljs
  12. 1407 0
      src/main/frontend/db/model.cljs
  13. 91 0
      src/main/frontend/db/query_custom.cljs
  14. 213 0
      src/main/frontend/db/react.cljs
  15. 61 0
      src/main/frontend/db/utils.cljs
  16. 1 1
      src/main/frontend/db_mixins.cljs
  17. 70 53
      src/main/frontend/extensions/code.cljs
  18. 12 0
      src/main/frontend/format/mldoc.cljs
  19. 9 3
      src/main/frontend/fs.cljs
  20. 0 1
      src/main/frontend/handler.cljs
  21. 181 0
      src/main/frontend/handler/block.cljs
  22. 20 19
      src/main/frontend/handler/dnd.cljs
  23. 132 133
      src/main/frontend/handler/editor.cljs
  24. 32 0
      src/main/frontend/handler/editor/keyboards.cljs
  25. 3 3
      src/main/frontend/handler/expand.cljs
  26. 176 0
      src/main/frontend/handler/extract.cljs
  27. 30 8
      src/main/frontend/handler/file.cljs
  28. 177 0
      src/main/frontend/handler/graph.cljs
  29. 8 1
      src/main/frontend/handler/page.cljs
  30. 22 7
      src/main/frontend/handler/repo.cljs
  31. 48 0
      src/main/frontend/namespaces.cljc
  32. 5 0
      src/main/frontend/search.cljs
  33. 5 9
      src/main/frontend/state.cljs
  34. 1 1
      src/main/frontend/version.cljs

+ 1 - 1
resources/css/table.css

@@ -16,7 +16,7 @@ tr {
 th {
     font-size: 14px;
     font-weight: 400;
-    color: #039;
+    color: var(--ls-primary-text-color);
     border-bottom: 2px solid var(--ls-border-color);
     padding: 10px 8px;
 }

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

@@ -16,6 +16,7 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.ui :as ui]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.repeated :as repeated]
@@ -1801,7 +1802,7 @@
         sidebar? (:sidebar? config)
         ref? (:ref? config)
         custom-query? (:custom-query? config)
-        blocks->vec-tree #(if (or custom-query? ref?) % (db/blocks->vec-tree %))
+        blocks->vec-tree #(if (or custom-query? ref?) % (block-handler/blocks->vec-tree %))
         blocks (blocks->vec-tree blocks)]
     (when (seq blocks)
       [:div.blocks-container.flex-1
@@ -1812,7 +1813,7 @@
                                -10)}}
        (let [first-block (first blocks)
              blocks' (if (and (:block/pre-block? first-block)
-                              (db/pre-block-with-only-title? (:block/repo first-block) (:block/uuid first-block)))
+                              (block-handler/pre-block-with-only-title? (:block/repo first-block) (:block/uuid first-block)))
                        (rest blocks)
                        blocks)
              first-id (:block/uuid (first blocks'))]

+ 7 - 23
src/main/frontend/components/editor.cljs

@@ -5,7 +5,9 @@
             [frontend.handler.editor :as editor-handler :refer [get-state]]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.handler.file :as file]
+            [frontend.handler.block :as block-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.components.datetime :as datetime-comp]
             [promesa.core :as p]
             [frontend.state :as state]
@@ -194,7 +196,7 @@
                                        template-parent-level (:block/level block)
                                        pattern (config/get-block-pattern format)
                                        content
-                                       (db/get-block-full-content
+                                       (block-handler/get-block-full-content
                                         (state/get-current-repo)
                                         (:block/uuid block)
                                         (fn [{:block/keys [level content properties] :as block}]
@@ -645,32 +647,14 @@
 
                   ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event,
                   ;; which will hide the editor so no way for editing.
-                  (js/setTimeout
-                   (fn []
-                     (mixins/hide-when-esc-or-outside
-                      state
-                      :on-hide
-                      (fn [state e event]
-                        (let [target (.-target e)]
-                          (if (d/has-class? target "bottom-action") ;; FIXME: not particular case
-                            (.preventDefault e)
-                            (let [{:keys [on-hide format value block id repo dummy?]} (get-state state)]
-                              (when on-hide
-                                (on-hide value event))
-                              (when
-                               (or (= event :esc)
-                                   (= event :visibilitychange)
-                                   (and (= event :click)
-                                        (not (editor-handler/in-auto-complete? (gdom/getElement id)))))
-                                (state/clear-edit!))))))
-                      :node (gdom/getElement id)
-                      ;; :visibilitychange? true
-))
-                   100)
+                  (js/setTimeout #(keyboards-handler/esc-save! state) 100)
 
                   (when-let [element (gdom/getElement id)]
                     (.focus element)))
                 state)
+   :did-remount (fn [state]
+                  (keyboards-handler/esc-save! state)
+                  state)
    :will-unmount (fn [state]
                    (let [{:keys [id value format block repo dummy? config]} (get-state state)
                          file? (:file? config)]

+ 3 - 2
src/main/frontend/components/journal.cljs

@@ -18,7 +18,8 @@
             [frontend.components.page :as page]
             [frontend.components.onboarding :as onboarding]
             [goog.object :as gobj]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [frontend.handler.block :as block-handler]))
 
 (rum/defc blocks-inner < rum/static
   {:did-mount (fn [state]
@@ -56,7 +57,7 @@
   (let [raw-blocks (db/get-page-blocks repo page)
         document-mode? (state/sub :document/mode?)
         blocks (->>
-                (db/with-dummy-block raw-blocks format nil {:journal? true})
+                (block-handler/with-dummy-block raw-blocks format nil {:journal? true})
                 (db/with-block-refs-count repo))]
     (blocks-inner blocks page document-mode?)))
 

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

@@ -6,6 +6,7 @@
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.handler.graph :as graph-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.editor :as editor-handler]
             [frontend.state :as state]
@@ -34,7 +35,8 @@
             [cljs-time.core :as t]
             [cljs.pprint :as pprint]
             [frontend.context.i18n :as i18n]
-            [reitit.frontend.easy :as rfe]))
+            [reitit.frontend.easy :as rfe]
+            [frontend.handler.block :as block-handler]))
 
 (defn- get-page-name
   [state]
@@ -47,14 +49,14 @@
     (if block?
       (db/get-block-and-children repo block-id)
       (do
-        (db/add-page-to-recent! repo page-original-name)
+        (page-handler/add-page-to-recent! repo page-original-name)
         (db/get-page-blocks repo page-name)))))
 
 (rum/defc page-blocks-cp < rum/reactive
   db-mixins/query
   [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
   (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
-        page-blocks (db/with-dummy-block raw-page-blocks format
+        page-blocks (block-handler/with-dummy-block raw-page-blocks format
                       (if (empty? raw-page-blocks)
                         (let [content (db/get-file repo file-path)]
                           {:block/page {:db/id (:db/id page)}
@@ -416,7 +418,7 @@
         sidebar-open? (state/sub :ui/sidebar-open?)
         [width height] (rum/react layout)
         dark? (= theme "dark")
-        graph (db/build-global-graph theme (rum/react show-journal?))
+        graph (graph-handler/build-global-graph theme (rum/react show-journal?))
         dot-mode-value? (rum/react dot-mode?)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.relative#global-graph

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

@@ -95,7 +95,8 @@
              [:a
               {:on-click #(nfs-handler/refresh! repo
                                                 repo-handler/create-today-journal!)
-               :title (str "Sync files with the local directory: " (config/get-local-dir repo))}
+               :title (str "Sync files with the local directory: " (config/get-local-dir repo) ".\nVersion: "
+                           version/version)}
               svg/refresh]])
           (let [changed-files (state/sub [:repo/changed-files repo])
                 should-push? (seq changed-files)
@@ -181,7 +182,7 @@
                             (config/get-local-dir repo)
                             (if head?
                               (db/get-repo-path repo)
-                              (util/take-at-most (db/get-repo-name repo) 20))))]
+                              (util/take-at-most (repo-handler/get-repo-name repo) 20))))]
       (let [repos (->> (state/sub [:me :repos])
                        (remove (fn [r] (= config/local-repo (:url r)))))]
         (cond

+ 3 - 2
src/main/frontend/components/right_sidebar.cljs

@@ -8,6 +8,7 @@
             [frontend.components.onboarding :as onboarding]
             [frontend.handler.route :as route-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.graph :as graph-handler]
             [frontend.state :as state]
             [frontend.db :as db]
             [frontend.util :as util]
@@ -42,8 +43,8 @@
   (let [theme (:ui/theme @state/state)
         dark? (= theme "dark")
         graph (if (util/uuid-string? page)
-                (db/build-block-graph (uuid page) theme)
-                (db/build-page-graph page theme))]
+                (graph-handler/build-block-graph (uuid page) theme)
+                (graph-handler/build-page-graph page theme))]
     (when (seq (:nodes graph))
       [:div.sidebar-item.flex-col.flex-1
        (graph-2d/graph

+ 3 - 0
src/main/frontend/components/right_sidebar.css

@@ -0,0 +1,3 @@
+.white-theme .cp__right-sidebar-settings a {
+    color: var(--ls-primary-text-color);
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 30 - 105
src/main/frontend/db.cljs


+ 83 - 0
src/main/frontend/db/conn.cljs

@@ -0,0 +1,83 @@
+(ns frontend.db.conn
+  "Contains db connections."
+  (:require [clojure.string :as string]
+            [frontend.db-schema :as db-schema]
+            [frontend.util :as util]
+            [frontend.state :as state]
+            [frontend.config :as config]
+            [frontend.idb :as idb]
+            [datascript.core :as d]
+            [frontend.db.utils :as db-utils]))
+
+(defonce conns (atom {}))
+
+(defn get-repo-path
+  [url]
+  (if (util/starts-with? url "http")
+    (->> (take-last 2 (string/split url #"/"))
+         (string/join "/"))
+    url))
+
+(defn datascript-db
+  [repo]
+  (when repo
+    (str config/idb-db-prefix (get-repo-path repo))))
+
+(defn datascript-files-db
+  [repo]
+  (when repo
+    (str "logseq-files-db/" (get-repo-path repo))))
+
+(defn remove-db!
+  [repo]
+  (idb/remove-item! (datascript-db repo)))
+
+(defn remove-files-db!
+  [repo]
+  (idb/remove-item! (datascript-files-db repo)))
+
+(defn get-conn
+  ([]
+   (get-conn (state/get-current-repo) true))
+  ([repo-or-deref?]
+   (if (boolean? repo-or-deref?)
+     (get-conn (state/get-current-repo) repo-or-deref?)
+     (get-conn repo-or-deref? true)))
+  ([repo deref?]
+   (let [repo (if repo repo (state/get-current-repo))]
+     (when-let [conn (get @conns (datascript-db repo))]
+       (if deref?
+         @conn
+         conn)))))
+
+(defn get-files-conn
+  ([]
+   (get-files-conn (state/get-current-repo)))
+  ([repo]
+   (get @conns (datascript-files-db repo))))
+
+(defn reset-conn! [conn db]
+  (reset! conn db))
+
+(defn remove-conn!
+  [repo]
+  (swap! conns dissoc (datascript-db repo))
+  (swap! conns dissoc (datascript-files-db repo)))
+
+(defn start!
+  ([me repo]
+   (start! me repo {}))
+  ([me repo {:keys [db-type listen-handler]}]
+   (let [files-db-name (datascript-files-db repo)
+         files-db-conn (d/create-conn db-schema/files-db-schema)
+         db-name (datascript-db repo)
+         db-conn (d/create-conn db-schema/schema)]
+     (swap! conns assoc files-db-name files-db-conn)
+     (swap! conns assoc db-name db-conn)
+     (d/transact! db-conn [(cond-> {:schema/version db-schema/version}
+                             db-type
+                             (assoc :db/type db-type))])
+     (when me
+       (d/transact! db-conn [(db-utils/me-tx (d/db db-conn) me)]))
+
+     (when listen-handler (listen-handler repo)))))

+ 54 - 0
src/main/frontend/db/debug.cljs

@@ -0,0 +1,54 @@
+(ns frontend.db.debug
+  (:require [medley.core :as medley]
+            [frontend.db.model :as model]))
+
+;; shortcut for query a block with string ref
+(defn qb
+  [string-id]
+  (model/pull [:block/uuid (medley/uuid string-id)]))
+
+(comment
+  (defn debug!
+    []
+    (let [repos (->> (get-in @state/state [:me :repos])
+                     (map :url))]
+      (mapv (fn [repo]
+              {:repo/current (state/get-current-repo)
+               :repo repo
+               :git/cloned? (cloned? repo)
+               :git/status (get-key-value repo :git/status)
+               :git/error (get-key-value repo :git/error)})
+            repos)))
+
+  ;; filtered blocks
+
+  (def page-and-aliases #{22})
+  (def excluded-pages #{59})
+  (def include-pages #{106})
+  (def page-linked-blocks
+    (->
+     (d/q
+      '[:find (pull ?b [:block/uuid
+                        :block/title
+                        {:block/children ...}])
+        :in $ ?pages
+        :where
+        [?b :block/ref-pages ?ref-page]
+        [(contains? ?pages ?ref-page)]]
+      (get-conn)
+      page-and-aliases)
+     flatten))
+
+  (def page-linked-blocks-include-filter
+    (if (seq include-pages)
+      (filter (fn [{:block/keys [ref-pages]}]
+                (some include-pages (map :db/id ref-pages)))
+              page-linked-blocks)
+      page-linked-blocks))
+
+  (def page-linked-blocks-exclude-filter
+    (if (seq excluded-pages)
+      (remove (fn [{:block/keys [ref-pages]}]
+                (some excluded-pages (map :db/id ref-pages)))
+              page-linked-blocks-include-filter)
+      page-linked-blocks-include-filter)))

+ 1407 - 0
src/main/frontend/db/model.cljs

@@ -0,0 +1,1407 @@
+(ns frontend.db.model
+  "Core db functions."
+  (:require [frontend.db.conn :as conn]
+            [frontend.db.utils :as db-utils]
+            [datascript.core :as d]
+            [frontend.date :as date]
+            [medley.core :as medley]
+            [frontend.format :as format]
+            [frontend.state :as state]
+            [clojure.string :as string]
+            [clojure.set :as set]
+            [frontend.utf8 :as utf8]
+            [frontend.config :as config]
+            [frontend.format.block :as block]
+            [cljs.reader :as reader]
+            [cljs-time.core :as t]
+            [cljs-time.coerce :as tc]
+            [frontend.util :as util :refer [react] :refer-macros [profile]]
+            [frontend.db-schema :as db-schema]))
+
+;; TODO: extract to specific models and move data transform logic to the
+;; correponding handlers.
+
+(defn entity
+  ([id-or-lookup-ref]
+   (entity (state/get-current-repo) id-or-lookup-ref))
+  ([repo id-or-lookup-ref]
+   (when-let [db (conn/get-conn repo)]
+     (d/entity db id-or-lookup-ref))))
+
+(defn pull
+  ([eid]
+   (pull (state/get-current-repo) '[*] eid))
+  ([selector eid]
+   (pull (state/get-current-repo) selector eid))
+  ([repo selector eid]
+   (when-let [conn (conn/get-conn repo)]
+     (try
+       (d/pull conn
+               selector
+               eid)
+       (catch js/Error e
+         nil)))))
+
+(defn pull-many
+  ([eids]
+   (pull-many '[*] eids))
+  ([selector eids]
+   (pull-many (state/get-current-repo) selector eids))
+  ([repo selector eids]
+   (when-let [conn (conn/get-conn repo)]
+     (try
+       (d/pull-many conn selector eids)
+       (catch js/Error e
+         (js/console.error e))))))
+
+(defn transact!
+  ([tx-data]
+   (transact! (state/get-current-repo) tx-data))
+  ([repo-url tx-data]
+   (when-not config/publishing?
+     (let [tx-data (->> (util/remove-nils tx-data)
+                        (remove nil?))]
+       (when (seq tx-data)
+         (when-let [conn (conn/get-conn repo-url false)]
+           (d/transact! conn (vec tx-data))))))))
+
+(defn transact-files-db!
+  ([tx-data]
+   (transact! (state/get-current-repo) tx-data))
+  ([repo-url tx-data]
+   (when-not config/publishing?
+     (let [tx-data (->> (util/remove-nils tx-data)
+                        (remove nil?)
+                        (map #(dissoc % :file/handle :file/type)))]
+       (when (seq tx-data)
+         (when-let [conn (conn/get-files-conn repo-url)]
+           (d/transact! conn (vec tx-data))))))))
+
+;; Query atom of map of Key ([repo q inputs]) -> atom
+;; TODO: replace with LRUCache, only keep the latest 20 or 50 items?
+(defonce query-state (atom {}))
+
+(def ^:dynamic *query-component*)
+
+;; key -> components
+(defonce query-components (atom {}))
+
+(defonce blocks-count-cache (atom nil))
+
+(defn set-new-result!
+  [k new-result]
+  (when-let [result-atom (get-in @query-state [k :result])]
+    (reset! result-atom new-result)))
+
+;; KV
+(defn get-key-value
+  ([key]
+   (get-key-value (state/get-current-repo) key))
+  ([repo-url key]
+   (when-let [db (conn/get-conn repo-url)]
+     (some-> (d/entity db key)
+             key))))
+
+(defn kv
+  [key value]
+  {:db/id -1
+   :db/ident key
+   key value})
+
+(defn remove-key!
+  [repo-url key]
+  (transact! repo-url [[:db.fn/retractEntity [:db/ident key]]])
+  (set-new-result! [repo-url :kv key] nil))
+
+(defn clear-query-state!
+  []
+  (reset! query-state {}))
+
+;; remove block refs, block embeds, page embeds
+(defn clear-query-state-without-refs-and-embeds!
+  []
+  (let [state @query-state
+        state (->> (filter (fn [[[_repo k] v]]
+                             (contains? #{:blocks :block/block :custom} k)) state)
+                   (into {}))]
+    (reset! query-state state)))
+
+;; TODO: Add components which subscribed to a specific query
+(defn add-q!
+  [k query inputs result-atom transform-fn query-fn inputs-fn]
+  (swap! query-state assoc k {:query query
+                              :inputs inputs
+                              :result result-atom
+                              :transform-fn transform-fn
+                              :query-fn query-fn
+                              :inputs-fn inputs-fn})
+  result-atom)
+
+(defn remove-q!
+  [k]
+  (swap! query-state dissoc k))
+
+(defn add-query-component!
+  [key component]
+  (swap! query-components update key
+         (fn [components]
+           (distinct (conj components component)))))
+
+(defn remove-query-component!
+  [component]
+  (reset!
+   query-components
+   (->> (for [[k components] @query-components
+              :let [new-components (remove #(= component %) components)]]
+          (if (empty? new-components) ; no subscribed components
+            (do (remove-q! k)
+                nil)
+            [k new-components]))
+        (keep identity)
+        (into {}))))
+
+(defn get-page-blocks-cache-atom
+  [repo page-id]
+  (:result (get @query-state [repo :page/blocks page-id])))
+
+(defn get-block-blocks-cache-atom
+  [repo block-id]
+  (:result (get @query-state [repo :block/block block-id])))
+
+;; TODO: rename :custom to :query/custom
+(defn remove-custom-query!
+  [repo query]
+  (remove-q! [repo :custom query]))
+
+;; Reactive query
+
+
+(defn query-entity-in-component
+  ([id-or-lookup-ref]
+   (entity (state/get-current-repo) id-or-lookup-ref))
+  ([repo id-or-lookup-ref]
+   (let [k [:entity id-or-lookup-ref]
+         result-atom (:result (get @query-state k))]
+     (when-let [component *query-component*]
+       (add-query-component! k component))
+     (when-let [db (conn/get-conn repo)]
+       (let [result (d/entity db id-or-lookup-ref)
+             result-atom (or result-atom (atom nil))]
+         (set! (.-state result-atom) result)
+         (add-q! k nil nil result-atom identity identity identity))))))
+
+(defn q
+  [repo k {:keys [use-cache? files-db? transform-fn query-fn inputs-fn]
+           :or {use-cache? true
+                files-db? false
+                transform-fn identity}} query & inputs]
+  (let [kv? (and (vector? k) (= :kv (first k)))
+        k (vec (cons repo k))]
+    (when-let [conn (if files-db?
+                      (when-let [files-conn (conn/get-files-conn repo)]
+                        (deref files-conn))
+                      (conn/get-conn repo))]
+      (let [result-atom (:result (get @query-state k))]
+        (when-let [component *query-component*]
+          (add-query-component! k component))
+        (if (and use-cache? result-atom)
+          result-atom
+          (let [result (cond
+                         query-fn
+                         (query-fn conn)
+
+                         inputs-fn
+                         (let [inputs (inputs-fn)]
+                           (apply d/q query conn inputs))
+
+                         kv?
+                         (d/entity conn (last k))
+
+                         (seq inputs)
+                         (apply d/q query conn inputs)
+
+                         :else
+                         (d/q query conn))
+                result (transform-fn result)
+                result-atom (or result-atom (atom nil))]
+            ;; Don't notify watches now
+            (set! (.-state result-atom) result)
+            (add-q! k query inputs result-atom transform-fn query-fn inputs-fn)))))))
+
+(defn sub-key-value
+  ([key]
+   (sub-key-value (state/get-current-repo) key))
+  ([repo-url key]
+   (when (conn/get-conn repo-url)
+     (-> (q repo-url [:kv key] {} key key)
+         react
+         key))))
+
+(defn pull-block
+  [id]
+  (let [repo (state/get-current-repo)]
+    (when (conn/get-conn repo)
+      (->
+       (q repo [:blocks id] {}
+          '[:find (pull ?block [*])
+            :in $ ?id
+            :where
+            [?block :block/uuid ?id]]
+          id)
+       react
+       ffirst))))
+
+(defn get-all-tags
+  []
+  (let [repo (state/get-current-repo)]
+    (when (conn/get-conn repo)
+      (some->>
+       (q repo [:tags] {}
+          '[:find ?name ?h ?p
+            :where
+            [?t :tag/name ?name]
+            (or
+             [?h :block/tags ?t]
+             [?p :page/tags ?t])])
+       react
+       (seq)))))
+
+(defn get-tag-pages
+  [repo tag-name]
+  (d/q '[:find ?original-name ?name
+         :in $ ?tag
+         :where
+         [?e :tag/name ?tag]
+         [?page :page/tags ?e]
+         [?page :page/original-name ?original-name]
+         [?page :page/name ?name]]
+       (conn/get-conn repo)
+       tag-name))
+
+(defn get-all-tagged-pages
+  [repo]
+  (d/q '[:find ?page-name ?tag
+         :where
+         [?page :page/tags ?e]
+         [?e :tag/name ?tag]
+         [_ :page/name ?tag]
+         [?page :page/name ?page-name]]
+       (conn/get-conn repo)))
+
+(defn get-pages
+  [repo]
+  (->> (d/q
+        '[:find ?page-name
+          :where
+          [?page :page/original-name ?page-name]]
+        (conn/get-conn repo))
+       (map first)))
+
+(defn get-pages-with-modified-at
+  [repo]
+  (let [now-long (tc/to-long (t/now))]
+    (->> (d/q
+          '[:find ?page-name ?modified-at
+            :where
+            [?page :page/original-name ?page-name]
+            [(get-else $ ?page :page/last-modified-at 0) ?modified-at]]
+          (conn/get-conn repo))
+         (seq)
+         (sort-by (fn [[page modified-at]]
+                    [modified-at page]))
+         (reverse)
+         (remove (fn [[page modified-at]]
+                   (or (util/file-page? page)
+                       (and modified-at
+                            (> modified-at now-long))))))))
+
+(defn get-page-alias
+  [repo page-name]
+  (when-let [conn (and repo (conn/get-conn repo))]
+    (some->> (d/q '[:find ?alias
+                    :in $ ?page-name
+                    :where
+                    [?page :page/name ?page-name]
+                    [?page :page/alias ?alias]]
+                  conn
+                  page-name)
+             db-utils/seq-flatten
+             distinct)))
+
+(defn get-alias-page
+  [repo alias]
+  (when-let [conn (and repo (conn/get-conn repo))]
+    (some->> (d/q '[:find ?page
+                    :in $ ?alias
+                    :where
+                    [?page :page/alias ?alias]]
+                  conn
+                  alias)
+             db-utils/seq-flatten
+             distinct)))
+
+(defn get-page-alias-names
+  [repo page-name]
+  (let [alias-ids (get-page-alias repo page-name)]
+    (when (seq alias-ids)
+      (->> (pull-many repo '[:page/name] alias-ids)
+           (map :page/name)
+           distinct))))
+
+(defn get-files
+  [repo]
+  (when-let [conn (conn/get-conn repo)]
+    (->> (d/q
+          '[:find ?path ?modified-at
+            :where
+            [?file :file/path ?path]
+            [(get-else $ ?file :file/last-modified-at 0) ?modified-at]]
+          conn)
+         (seq)
+         (sort-by last)
+         (reverse))))
+
+(defn get-files-blocks
+  [repo-url paths]
+  (let [paths (set paths)
+        pred (fn [_db e]
+               (contains? paths e))]
+    (-> (d/q '[:find ?block
+               :in $ ?pred
+               :where
+               [?file :file/path ?path]
+               [(?pred $ ?path)]
+               [?block :block/file ?file]]
+             (conn/get-conn repo-url) pred)
+        db-utils/seq-flatten)))
+
+(defn get-file-blocks
+  [repo-url path]
+  (-> (d/q '[:find ?block
+             :in $ ?path
+             :where
+             [?file :file/path ?path]
+             [?block :block/file ?file]]
+           (conn/get-conn repo-url) path)
+      db-utils/seq-flatten))
+
+(defn get-file-after-blocks
+  [repo-url file-id end-pos]
+  (when end-pos
+    (let [pred (fn [db meta]
+                 (>= (:start-pos meta) end-pos))]
+      (-> (d/q '[:find (pull ?block [*])
+                 :in $ ?file-id ?pred
+                 :where
+                 [?block :block/file ?file-id]
+                 [?block :block/meta ?meta]
+                 [(?pred $ ?meta)]]
+               (conn/get-conn repo-url) file-id pred)
+          db-utils/seq-flatten
+          db-utils/sort-by-pos))))
+
+(defn get-file-after-blocks-meta
+  ([repo-url file-id end-pos]
+   (get-file-after-blocks-meta repo-url file-id end-pos false))
+  ([repo-url file-id end-pos content-level?]
+   (let [db (conn/get-conn repo-url)
+         blocks (d/datoms db :avet :block/file file-id)
+         eids (mapv :e blocks)
+         ks (if content-level?
+              '[:block/uuid :block/meta :block/content :block/level]
+              '[:block/uuid :block/meta])
+         blocks (pull-many repo-url ks eids)]
+     (->> (filter (fn [{:block/keys [meta]}]
+                    (>= (:start-pos meta) end-pos)) blocks)
+          db-utils/sort-by-pos))))
+
+(defn get-file-pages
+  [repo-url path]
+  (-> (d/q '[:find ?page
+             :in $ ?path
+             :where
+             [?file :file/path ?path]
+             [?page :page/file ?file]]
+           (conn/get-conn repo-url) path)
+      db-utils/seq-flatten))
+
+(defn set-file-last-modified-at!
+  [repo path last-modified-at]
+  (when (and repo path last-modified-at)
+    (when-let [conn (conn/get-files-conn repo)]
+      (d/transact! conn
+                   [{:file/path path
+                     :file/last-modified-at last-modified-at}]))))
+
+(defn get-file-last-modified-at
+  [repo path]
+  (when (and repo path)
+    (when-let [conn (conn/get-files-conn repo)]
+      (-> (d/entity (d/db conn) [:file/path path])
+          :file/last-modified-at))))
+
+(defn get-file
+  ([path]
+   (get-file (state/get-current-repo) path))
+  ([repo path]
+   (when (and repo path)
+     (->
+      (q repo [:file/content path]
+         {:files-db? true
+          :use-cache? true}
+         '[:find ?content
+           :in $ ?path
+           :where
+           [?file :file/path ?path]
+           [?file :file/content ?content]]
+         path)
+      react
+      ffirst))))
+
+(defn get-file-contents
+  [repo]
+  (when-let [conn (conn/get-files-conn repo)]
+    (->>
+     (d/q
+      '[:find ?path ?content
+        :where
+        [?file :file/path ?path]
+        [?file :file/content ?content]]
+      @conn)
+     (into {}))))
+
+(defn get-files-full
+  [repo]
+  (when-let [conn (conn/get-files-conn repo)]
+    (->>
+     (d/q
+      '[:find (pull ?file [*])
+        :where
+        [?file :file/path]]
+      @conn)
+     (flatten))))
+
+(defn get-custom-css
+  []
+  (get-file "logseq/custom.css"))
+
+(defn get-file-no-sub
+  ([path]
+   (get-file-no-sub (state/get-current-repo) path))
+  ([repo path]
+   (when (and repo path)
+     (when-let [conn (conn/get-files-conn repo)]
+       (->
+        (d/q
+         '[:find ?content
+           :in $ ?path
+           :where
+           [?file :file/path ?path]
+           [?file :file/content ?content]]
+         @conn
+         path)
+        ffirst)))))
+
+(defn get-block-by-uuid
+  [uuid]
+  (entity [:block/uuid uuid]))
+
+(defn get-page-format
+  [page-name]
+  (when-let [file (:page/file (entity [:page/name page-name]))]
+    (when-let [path (:file/path (entity (:db/id file)))]
+      (format/get-format path))))
+
+(defn page-alias-set
+  [repo-url page]
+  (when-let [page-id (:db/id (entity [:page/name page]))]
+    (let [aliases (get-page-alias repo-url page)
+          aliases (if (seq aliases)
+                    (set
+                     (concat
+                      (mapcat #(get-alias-page repo-url %) aliases)
+                      aliases))
+                    aliases)]
+      (set (conj aliases page-id)))))
+
+(defn get-block-refs-count
+  [repo]
+  (->> (d/q
+        '[:find ?id2 ?id1
+          :where
+          [?id1 :block/ref-blocks ?id2]]
+        (conn/get-conn repo))
+       (map first)
+       (frequencies)))
+
+(defn with-block-refs-count
+  [repo blocks]
+  (let [db-ids (map :db/id blocks)
+        refs (get-block-refs-count repo)]
+    (map (fn [block]
+           (assoc block :block/block-refs-count
+                  (get refs (:db/id block))))
+         blocks)))
+
+(defn page-blocks-transform
+  [repo-url result]
+  (let [result (db-utils/seq-flatten result)
+        sorted (db-utils/sort-by-pos result)]
+    (->> (db-utils/with-repo repo-url sorted)
+         (with-block-refs-count repo-url))))
+
+(defn sort-blocks
+  [blocks]
+  (let [pages-ids (map (comp :db/id :block/page) blocks)
+        pages (pull-many '[:db/id :page/last-modified-at :page/name :page/original-name] pages-ids)
+        pages-map (reduce (fn [acc p] (assoc acc (:db/id p) p)) {} pages)
+        blocks (map
+                (fn [block]
+                  (assoc block :block/page
+                         (get pages-map (:db/id (:block/page block)))))
+                blocks)]
+    (db-utils/sort-by-pos blocks)))
+
+(defn get-marker-blocks
+  [repo-url marker]
+  (let [marker (string/upper-case marker)]
+    (some->>
+     (q repo-url [:marker/blocks marker]
+        {:use-cache? true}
+        '[:find (pull ?h [*])
+          :in $ ?marker
+          :where
+          [?h :block/marker ?m]
+          [(= ?marker ?m)]]
+        marker)
+     react
+     db-utils/seq-flatten
+     db-utils/sort-by-pos
+     (db-utils/with-repo repo-url)
+     (with-block-refs-count repo-url)
+     (sort-blocks)
+     (db-utils/group-by-page))))
+
+(defn get-page-properties
+  [page]
+  (when-let [page (entity [:page/name page])]
+    (:page/properties page)))
+
+(defn add-properties!
+  [page-format properties-content properties]
+  (let [properties (medley/map-keys name properties)
+        lines (string/split-lines properties-content)
+        front-matter-format? (contains? #{:markdown} page-format)
+        lines (if front-matter-format?
+                (remove (fn [line]
+                          (contains? #{"---" ""} (string/trim line))) lines)
+                lines)
+        property-keys (keys properties)
+        prefix-f (case page-format
+                   :org (fn [k]
+                          (str "#+" (string/upper-case k) ": "))
+                   :markdown (fn [k]
+                               (str (string/lower-case k) ": "))
+                   identity)
+        exists? (atom #{})
+        lines (doall
+               (mapv (fn [line]
+                       (let [result (filter #(and % (util/starts-with? line (prefix-f %)))
+                                            property-keys)]
+                         (if (seq result)
+                           (let [k (first result)]
+                             (swap! exists? conj k)
+                             (str (prefix-f k) (get properties k)))
+                           line))) lines))
+        lines (concat
+               lines
+               (let [not-exists (remove
+                                 (fn [[k _]]
+                                   (contains? @exists? k))
+                                 properties)]
+                 (when (seq not-exists)
+                   (mapv
+                    (fn [[k v]] (str (prefix-f k) v))
+                    not-exists))))]
+    (util/format
+     (config/properties-wrapper-pattern page-format)
+     (string/join "\n" lines))))
+
+(defn get-page-blocks
+  ([page]
+   (get-page-blocks (state/get-current-repo) page nil))
+  ([repo-url page]
+   (get-page-blocks repo-url page nil))
+  ([repo-url page {:keys [use-cache? pull-keys]
+                   :or {use-cache? true
+                        pull-keys '[*]}}]
+   (let [page (string/lower-case page)
+         page-id (or (:db/id (entity repo-url [:page/name page]))
+                     (:db/id (entity repo-url [:page/original-name page])))
+         db (conn/get-conn repo-url)]
+     (when page-id
+       (some->
+        (q repo-url [:page/blocks page-id]
+           {:use-cache? use-cache?
+            :transform-fn #(page-blocks-transform repo-url %)
+            :query-fn (fn [db]
+                        (let [datoms (d/datoms db :avet :block/page page-id)
+                              block-eids (mapv :e datoms)]
+                          (pull-many repo-url pull-keys block-eids)))}
+           nil)
+        react)))))
+
+(defn get-page-blocks-no-cache
+  ([page]
+   (get-page-blocks-no-cache (state/get-current-repo) page nil))
+  ([repo-url page]
+   (get-page-blocks-no-cache repo-url page nil))
+  ([repo-url page {:keys [pull-keys]
+                   :or {pull-keys '[*]}}]
+   (let [page (string/lower-case page)
+         page-id (or (:db/id (entity repo-url [:page/name page]))
+                     (:db/id (entity repo-url [:page/original-name page])))
+         db (conn/get-conn repo-url)]
+     (when page-id
+       (let [datoms (d/datoms db :avet :block/page page-id)
+             block-eids (mapv :e datoms)]
+         (some->> (pull-many repo-url pull-keys block-eids)
+                  (page-blocks-transform repo-url)))))))
+
+(defn get-page-blocks-count
+  [repo page-id]
+  (when-let [db (conn/get-conn repo)]
+    (count (d/datoms db :avet :block/page page-id))))
+
+(defn get-block-parent
+  [repo block-id]
+  (when-let [conn (conn/get-conn repo)]
+    (d/entity conn [:block/children [:block/uuid block-id]])))
+
+;; non recursive query
+(defn get-block-parents
+  [repo block-id depth]
+  (when-let [conn (conn/get-conn repo)]
+    (loop [block-id block-id
+           parents (list)
+           d 1]
+      (if (> d depth)
+        parents
+        (if-let [parent (get-block-parent repo block-id)]
+          (recur (:block/uuid parent) (conj parents parent) (inc d))
+          parents)))))
+
+(defn get-block-page
+  [repo block-id]
+  (when-let [block (entity repo [:block/uuid block-id])]
+    (entity repo (:db/id (:block/page block)))))
+
+(defn get-block-page-end-pos
+  [repo page-name]
+  (or
+   (when-let [page-id (:db/id (entity repo [:page/name (string/lower-case page-name)]))]
+     (when-let [db (conn/get-conn repo)]
+       (let [block-eids (->> (d/datoms db :avet :block/page page-id)
+                             (mapv :e))]
+         (when (seq block-eids)
+           (let [blocks (pull-many repo '[:block/meta] block-eids)]
+             (-> (last (db-utils/sort-by-pos blocks))
+                 (get-in [:block/meta :end-pos])))))))
+   ;; TODO: need more thoughts
+   0))
+
+(defn get-blocks-by-priority
+  [repo priority]
+  (let [priority (string/capitalize priority)]
+    (when (conn/get-conn repo)
+      (->> (q repo [:priority/blocks priority] {}
+              '[:find (pull ?h [*])
+                :in $ ?priority
+                :where
+                [?h :block/priority ?priority]]
+              priority)
+           react
+           db-utils/seq-flatten
+           sort-blocks
+           db-utils/group-by-page))))
+
+(defn get-page-properties-content
+  [page]
+  (when-let [content (let [blocks (get-page-blocks page)]
+                       (and (:block/pre-block? (first blocks))
+                            (:block/content (first blocks))))]
+    (let [format (get-page-format page)]
+      (case format
+        :org
+        (->> (string/split-lines content)
+             (take-while (fn [line]
+                           (or (string/blank? line)
+                               (string/starts-with? line "#+"))))
+             (string/join "\n"))
+
+        :markdown
+        (str (subs content 0 (string/last-index-of content "---\n\n"))
+             "---\n\n")
+
+        content))))
+
+(defn block-and-children-transform
+  [result repo-url block-uuid level]
+  (some->> result
+           db-utils/seq-flatten
+           db-utils/sort-by-pos
+           (take-while (fn [h]
+                         (or
+                          (= (:block/uuid h)
+                             block-uuid)
+                          (> (:block/level h) level))))
+           (db-utils/with-repo repo-url)
+           (with-block-refs-count repo-url)))
+
+(defn get-block-children-ids
+  [repo block-uuid]
+  (when-let [conn (conn/get-conn repo)]
+    (let [eid (:db/id (entity repo [:block/uuid block-uuid]))]
+      (->> (d/q
+            '[:find ?e1
+              :in $ ?e2 %
+              :where (parent ?e2 ?e1)]
+            conn
+            eid
+             ;; recursive rules
+            '[[(parent ?e2 ?e1)
+               [?e2 :block/children ?e1]]
+              [(parent ?e2 ?e1)
+               [?t :block/children ?e1]
+               (parent ?e2 ?t)]])
+           (apply concat)))))
+
+(defn get-block-immediate-children
+  [repo block-uuid]
+  (when-let [conn (conn/get-conn repo)]
+    (let [ids (->> (:block/children (entity repo [:block/uuid block-uuid]))
+                   (map :db/id))]
+      (when (seq ids)
+        (pull-many repo '[*] ids)))))
+
+(defn get-block-children
+  [repo block-uuid]
+  (when-let [conn (conn/get-conn repo)]
+    (let [ids (get-block-children-ids repo block-uuid)]
+      (when (seq ids)
+        (pull-many repo '[*] ids)))))
+
+(defn get-block-and-children
+  ([repo block-uuid]
+   (get-block-and-children repo block-uuid true))
+  ([repo block-uuid use-cache?]
+   (let [block (entity repo [:block/uuid block-uuid])
+         page (:db/id (:block/page block))
+         pos (:start-pos (:block/meta block))
+         level (:block/level block)
+         pred (fn []
+                (let [block (entity repo [:block/uuid block-uuid])
+                      pos (:start-pos (:block/meta block))]
+                  (fn [data meta]
+                    (>= (:start-pos meta) pos))))]
+     (some-> (q repo [:block/block block-uuid]
+                {:use-cache? use-cache?
+                 :transform-fn #(block-and-children-transform % repo block-uuid level)
+                 :inputs-fn (fn []
+                              [page (pred)])}
+                '[:find (pull ?block [*])
+                  :in $ ?page ?pred
+                  :where
+                  [?block :block/page ?page]
+                  [?block :block/meta ?meta]
+                  [(?pred $ ?meta)]])
+             react))))
+
+;; TODO: performance
+(defn get-block-and-children-no-cache
+  [repo block-uuid]
+  (let [block (entity repo [:block/uuid block-uuid])
+        page (:db/id (:block/page block))
+        pos (:start-pos (:block/meta block))
+        level (:block/level block)
+        pred (fn [data meta]
+               (>= (:start-pos meta) pos))]
+    (-> (d/q
+         '[:find (pull ?block [*])
+           :in $ ?page ?pred
+           :where
+           [?block :block/page ?page]
+           [?block :block/meta ?meta]
+           [(?pred $ ?meta)]]
+         (conn/get-conn repo)
+         page
+         pred)
+        (block-and-children-transform repo block-uuid level))))
+
+(defn get-file-page
+  ([file-path]
+   (get-file-page file-path true))
+  ([file-path original-name?]
+   (when-let [repo (state/get-current-repo)]
+     (when-let [conn (conn/get-conn repo)]
+       (some->
+        (d/q
+         (if original-name?
+           '[:find ?page-name
+             :in $ ?path
+             :where
+             [?file :file/path ?path]
+             [?page :page/file ?file]
+             [?page :page/original-name ?page-name]]
+           '[:find ?page-name
+             :in $ ?path
+             :where
+             [?file :file/path ?path]
+             [?page :page/file ?file]
+             [?page :page/name ?page-name]])
+         conn file-path)
+        db-utils/seq-flatten
+        first)))))
+
+(defn get-page-file
+  [page-name]
+  (some-> (entity [:page/name page-name])
+          :page/file))
+
+(defn get-block-file
+  [block-id]
+  (let [page-id (some-> (entity [:block/uuid block-id])
+                        :block/page
+                        :db/id)]
+    (:page/file (entity page-id))))
+
+(defn get-file-page-id
+  [file-path]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [conn (conn/get-conn repo)]
+      (some->
+       (d/q
+        '[:find ?page
+          :in $ ?path
+          :where
+          [?file :file/path ?path]
+          [?page :page/file ?file]]
+        conn file-path)
+       db-utils/seq-flatten
+       first))))
+
+(defn get-page
+  [page-name]
+  (if (util/uuid-string? page-name)
+    (entity [:block/uuid (uuid page-name)])
+    (entity [:page/name page-name])))
+
+(defn get-page-name
+  [file ast]
+  ;; headline
+  (let [ast (map first ast)]
+    (if (util/starts-with? file "pages/contents.")
+      "Contents"
+      (let [first-block (last (first (filter block/heading-block? ast)))
+            property-name (when (and (= "Properties" (ffirst ast))
+                                     (not (string/blank? (:title (last (first ast))))))
+                            (:title (last (first ast))))
+            first-block-name (and first-block
+                                  ;; FIXME:
+                                  (str (last (first (:title first-block)))))
+            file-name (when-let [file-name (last (string/split file #"/"))]
+                        (when-let [file-name (first (util/split-last "." file-name))]
+                          (-> file-name
+                              (string/replace "-" " ")
+                              (string/replace "_" " ")
+                              (util/capitalize-all))))]
+        (or property-name
+            (if (= (state/page-name-order) "file")
+              (or file-name first-block-name)
+              (or first-block-name file-name)))))))
+
+(defn get-block-content
+  [utf8-content block]
+  (let [meta (:block/meta block)]
+    (if-let [end-pos (:end-pos meta)]
+      (utf8/substring utf8-content
+                      (:start-pos meta)
+                      end-pos)
+      (utf8/substring utf8-content
+                      (:start-pos meta)))))
+
+(defn get-journals-length
+  []
+  (let [today (db-utils/date->int (js/Date.))]
+    (d/q '[:find (count ?page) .
+           :in $ ?today
+           :where
+           [?page :page/journal? true]
+           [?page :page/journal-day ?journal-day]
+           [(<= ?journal-day ?today)]]
+         (conn/get-conn (state/get-current-repo))
+         today)))
+
+(defn get-latest-journals
+  ([n]
+   (get-latest-journals (state/get-current-repo) n))
+  ([repo-url n]
+   (when (conn/get-conn repo-url)
+     (let [date (js/Date.)
+           _ (.setDate date (- (.getDate date) (dec n)))
+           today (db-utils/date->int (js/Date.))
+           pages (->>
+                  (q repo-url [:journals] {:use-cache? false}
+                     '[:find ?page-name ?journal-day
+                       :in $ ?today
+                       :where
+                       [?page :page/name ?page-name]
+                       [?page :page/journal? true]
+                       [?page :page/journal-day ?journal-day]
+                       [(<= ?journal-day ?today)]]
+                     today)
+                  (react)
+                  (sort-by last)
+                  (reverse)
+                  (map first)
+                  (take n))]
+       (mapv
+        (fn [page]
+          [page
+           (get-page-format page)])
+        pages)))))
+
+;; get pages that this page referenced
+(defn get-page-referenced-pages
+  [repo page]
+  (when (conn/get-conn repo)
+    (let [pages (page-alias-set repo page)
+          page-id (:db/id (entity [:page/name page]))
+          ref-pages (->> (q repo [:page/ref-pages page-id] {:use-cache? false}
+                            '[:find ?ref-page-name
+                              :in $ ?pages
+                              :where
+                              [?block :block/page ?p]
+                              [(contains? ?pages ?p)]
+                              [?block :block/ref-pages ?ref-page]
+                              [?ref-page :page/name ?ref-page-name]]
+                            pages)
+                         react
+                         db-utils/seq-flatten)]
+      (mapv (fn [page] [page (get-page-alias repo page)]) ref-pages))))
+
+;; Ignore files with empty blocks for now
+(defn get-empty-pages
+  [repo]
+  (when-let [conn (conn/get-conn repo)]
+    (->
+     (d/q
+      '[:find ?page
+        :where
+        [?p :page/name ?page]
+        (not [?p :page/file])]
+      conn)
+     (db-utils/seq-flatten)
+     (distinct))))
+
+(defn get-pages-relation
+  [repo with-journal?]
+  (when-let [conn (conn/get-conn repo)]
+    (let [q (if with-journal?
+              '[:find ?page ?ref-page-name
+                :where
+                [?p :page/name ?page]
+                [?block :block/page ?p]
+                [?block :block/ref-pages ?ref-page]
+                [?ref-page :page/name ?ref-page-name]]
+              '[:find ?page ?ref-page-name
+                :where
+                [?p :page/journal? false]
+                [?p :page/name ?page]
+                [?block :block/page ?p]
+                [?block :block/ref-pages ?ref-page]
+                [?ref-page :page/name ?ref-page-name]])]
+      (->>
+       (d/q q conn)
+       (map (fn [[page ref-page-name]]
+              [page ref-page-name]))))))
+
+;; get pages who mentioned this page
+(defn get-pages-that-mentioned-page
+  [repo page]
+  (when (conn/get-conn repo)
+    (let [page-id (:db/id (entity [:page/name page]))
+          pages (page-alias-set repo page)
+          mentioned-pages (->> (q repo [:page/mentioned-pages page-id] {:use-cache? false}
+                                  '[:find ?mentioned-page-name
+                                    :in $ ?pages ?page-name
+                                    :where
+                                    [?block :block/ref-pages ?p]
+                                    [(contains? ?pages ?p)]
+                                    [?block :block/page ?mentioned-page]
+                                    [?mentioned-page :page/name ?mentioned-page-name]]
+                                  pages
+                                  page)
+                               react
+                               db-utils/seq-flatten)]
+      (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
+
+(defn get-page-referenced-blocks
+  [page]
+  (when-let [repo (state/get-current-repo)]
+    (when (conn/get-conn repo)
+      (let [page-id (:db/id (entity [:page/name page]))
+            pages (page-alias-set repo page)]
+        (->> (q repo [:page/refed-blocks page-id] {}
+                '[:find (pull ?block [*])
+                  :in $ ?pages
+                  :where
+                  [?block :block/ref-pages ?ref-page]
+                  [(contains? ?pages ?ref-page)]]
+                pages)
+             react
+             db-utils/seq-flatten
+             (remove (fn [block]
+                       (let [exclude-pages pages]
+                         (contains? exclude-pages (:db/id (:block/page block))))))
+             sort-blocks
+             db-utils/group-by-page)))))
+
+(defn get-date-scheduled-or-deadlines
+  [journal-title]
+  (when-let [date (date/journal-title->int journal-title)]
+    (when-let [repo (state/get-current-repo)]
+      (when-let [conn (conn/get-conn repo)]
+        (->> (q repo [:custom :scheduled-deadline journal-title] {}
+                '[:find (pull ?block [*])
+                  :in $ ?day
+                  :where
+                  (or
+                   [?block :block/scheduled ?day]
+                   [?block :block/deadline ?day])]
+                date)
+             react
+             db-utils/seq-flatten
+             sort-blocks
+             db-utils/group-by-page
+             (remove (fn [[page _blocks]]
+                       (= journal-title (:page/original-name page)))))))))
+
+(defn get-files-that-referenced-page
+  [page-id]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [db (conn/get-conn repo)]
+      (->> (d/q
+            '[:find ?path
+              :in $ ?page-id
+              :where
+              [?block :block/ref-pages ?page-id]
+              [?block :block/page ?p]
+              [?p :page/file ?f]
+              [?f :file/path ?path]]
+            db
+            page-id)
+           (db-utils/seq-flatten)))))
+
+(defn get-page-unlinked-references
+  [page]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [conn (conn/get-conn repo)]
+      (let [page-id (:db/id (entity [:page/name page]))
+            pages (page-alias-set repo page)
+            pattern (re-pattern (str "(?i)" page))]
+        (->> (d/q
+              '[:find (pull ?block [*])
+                :in $ ?pattern
+                :where
+                [?block :block/content ?content]
+                [(re-find ?pattern ?content)]]
+              conn
+              pattern)
+             db-utils/seq-flatten
+             (remove (fn [block]
+                       (let [ref-pages (set (map :db/id (:block/ref-pages block)))]
+                         (or
+                          (= (get-in block [:block/page :db/id]) page-id)
+                          (seq (set/intersection
+                                ref-pages
+                                pages))))))
+             sort-blocks
+             db-utils/group-by-page)))))
+
+(defn get-block-referenced-blocks
+  [block-uuid]
+  (when-let [repo (state/get-current-repo)]
+    (when (conn/get-conn repo)
+      (->> (q repo [:block/refed-blocks block-uuid] {}
+              '[:find (pull ?ref-block [*])
+                :in $ ?block-uuid
+                :where
+                [?block :block/uuid ?block-uuid]
+                [?ref-block :block/ref-blocks ?block]]
+              block-uuid)
+           react
+           db-utils/seq-flatten
+           sort-blocks
+           db-utils/group-by-page))))
+
+(defn get-matched-blocks
+  [match-fn limit]
+  (when-let [repo (state/get-current-repo)]
+    (let [pred (fn [db content]
+                 (match-fn content))]
+      (->> (d/q
+            '[:find ?block
+              :in $ ?pred
+              :where
+              [?block :block/content ?content]
+              [(?pred $ ?content)]]
+            (conn/get-conn)
+            pred)
+           (take limit)
+           db-utils/seq-flatten
+           (pull-many '[:block/uuid
+                        :block/content
+                        :block/properties
+                        :block/format
+                        {:block/page [:page/name]}])))))
+
+;; TODO: Does the result preserves the order of the arguments?
+(defn get-blocks-contents
+  [repo block-uuids]
+  (let [db (conn/get-conn repo)]
+    (pull-many repo '[:block/content]
+               (mapv (fn [id] [:block/uuid id]) block-uuids))))
+
+(defn journal-page?
+  [page-name]
+  (:page/journal? (entity [:page/name page-name])))
+
+(defn mark-repo-as-cloned!
+  [repo-url]
+  (transact!
+   [{:repo/url repo-url
+     :repo/cloned? true}]))
+
+(defn cloned?
+  [repo-url]
+  (when-let [conn (conn/get-conn repo-url)]
+    (->
+     (d/q '[:find ?cloned
+            :in $ ?repo-url
+            :where
+            [?repo :repo/url ?repo-url]
+            [?repo :repo/cloned? ?cloned]]
+          conn
+          repo-url)
+     ffirst)))
+
+(defn get-config
+  [repo-url]
+  (get-file repo-url (str config/app-name "/" config/config-file)))
+
+(defn reset-config!
+  [repo-url content]
+  (when-let [content (or content (get-config repo-url))]
+    (let [config (try
+                   (reader/read-string content)
+                   (catch js/Error e
+                     (println "Parsing config file failed: ")
+                     (js/console.dir e)
+                     {}))]
+      (state/set-config! repo-url config)
+      config)))
+
+(defn get-db-type
+  [repo]
+  (get-key-value repo :db/type))
+
+(defn local-native-fs?
+  [repo]
+  (= :local-native-fs (get-db-type repo)))
+
+(defn get-collapsed-blocks
+  []
+  (d/q
+   '[:find ?content
+     :where
+     [?h :block/collapsed? true]
+     [?h :block/content ?content]]
+   (conn/get-conn)))
+
+(defn get-public-pages
+  [db]
+  (-> (d/q
+       '[:find ?p
+         :where
+         [?p :page/properties ?d]
+         [(get ?d :public) ?pub]
+         [(= "true" ?pub)]]
+       db)
+      (db-utils/seq-flatten)))
+
+(defn rebuild-page-blocks-children
+  "For performance reason, we can update the :block/children value after every operation,
+  but it's hard to make sure that it's correct, also it needs more time to implement it.
+  We can improve it if the performance is really an issue."
+  [repo page]
+  (let [blocks (->>
+                (get-page-blocks-no-cache repo page {:pull-keys '[:db/id :block/uuid :block/level :block/pre-block? :block/meta]})
+                (remove :block/pre-block?)
+                (map #(select-keys % [:db/id :block/uuid :block/level]))
+                (reverse))
+        original-blocks blocks]
+    (loop [blocks blocks
+           tx []
+           children {}
+           last-level 10000]
+      (if (seq blocks)
+        (let [[{:block/keys [uuid level] :as block} & others] blocks
+              [tx children] (cond
+                              (< level last-level)        ; parent
+                              (let [cur-children (get children last-level)
+                                    tx (if (seq cur-children)
+                                         (vec
+                                          (concat
+                                           tx
+                                           (map
+                                            (fn [child]
+                                              [:db/add (:db/id block) :block/children [:block/uuid child]])
+                                            cur-children)))
+                                         tx)
+                                    children (-> children
+                                                 (dissoc last-level)
+                                                 (update level conj uuid))]
+                                [tx children])
+
+                              (> level last-level)        ; child of sibling
+                              (let [children (update children level conj uuid)]
+                                [tx children])
+
+                              :else                       ; sibling
+                              (let [children (update children last-level conj uuid)]
+                                [tx children]))]
+          (recur others tx children level))
+        ;; TODO: add top-level children to the "Page" block (we might remove the Page from db schema)
+        (when (seq tx)
+          (let [delete-tx (map (fn [block]
+                                 [:db/retract (:db/id block) :block/children])
+                               original-blocks)]
+            (->> (concat delete-tx tx)
+                 (remove nil?))))))))
+
+(defn get-all-templates
+  []
+  (let [pred (fn [db properties]
+               (some? (get properties "template")))]
+    (->> (d/q
+          '[:find ?b ?p
+            :in $ ?pred
+            :where
+            [?b :block/properties ?p]
+            [(?pred $ ?p)]]
+          (conn/get-conn)
+          pred)
+         (map (fn [[e m]]
+                [(get m "template") e]))
+         (into {}))))
+
+(defn template-exists?
+  [title]
+  (when title
+    (let [templates (keys (get-all-templates))]
+      (when (seq templates)
+        (let [templates (map string/lower-case templates)]
+          (contains? (set templates) (string/lower-case title)))))))
+
+(defn blocks-count
+  ([]
+   (blocks-count true))
+  ([cache?]
+   (if (and cache? @blocks-count-cache)
+     @blocks-count-cache
+     (let [n (count (d/datoms (conn/get-conn) :avet :block/uuid))]
+       (reset! blocks-count-cache n)
+       n))))
+
+;; block/uuid and block/content
+(defn get-all-block-contents
+  []
+  (when-let [conn (conn/get-conn)]
+    (->> (d/datoms conn :avet :block/uuid)
+         (map :v)
+         (map (fn [id]
+                (let [e (entity [:block/uuid id])]
+                  {:db/id (:db/id e)
+                   :block/uuid id
+                   :block/content (:block/content e)
+                   :block/format (:block/format e)}))))))
+
+(defn clean-export!
+  [db]
+  (let [remove? #(contains? #{"me" "recent" "file"} %)
+        filtered-db (d/filter db
+                              (fn [db datom]
+                                (let [ns (namespace (:a datom))]
+                                  (not (remove? ns)))))
+        datoms (d/datoms filtered-db :eavt)]
+    @(d/conn-from-datoms datoms db-schema/schema)))
+
+(defn filter-only-public-pages-and-blocks
+  [db]
+  (let [public-pages (get-public-pages db)
+        contents-id (:db/id (entity [:page/name "contents"]))]
+    (when (seq public-pages)
+      (let [public-pages (set (conj public-pages contents-id))
+            page-or-block? #(contains? #{"page" "block" "me" "recent" "file"} %)
+            filtered-db (d/filter db
+                                  (fn [db datom]
+                                    (let [ns (namespace (:a datom))]
+                                      (or
+                                       (not (page-or-block? ns))
+                                       (and (= ns "page")
+                                            (contains? public-pages (:e datom)))
+                                       (and (= ns "block")
+                                            (contains? public-pages (:db/id (:block/page (d/entity db (:e datom))))))))))
+            datoms (d/datoms filtered-db :eavt)]
+        @(d/conn-from-datoms datoms db-schema/schema)))))
+
+(defn delete-blocks
+  [repo-url files]
+  (when (seq files)
+    (let [blocks (get-files-blocks repo-url files)]
+      (mapv (fn [eid] [:db.fn/retractEntity eid]) blocks))))
+
+(defn delete-files
+  [files]
+  (mapv (fn [path] [:db.fn/retractEntity [:file/path path]]) files))
+
+(defn delete-file-blocks!
+  [repo-url path]
+  (let [blocks (get-file-blocks repo-url path)]
+    (mapv (fn [eid] [:db.fn/retractEntity eid]) blocks)))
+
+(defn delete-file-pages!
+  [repo-url path]
+  (let [pages (get-file-pages repo-url path)]
+    (mapv (fn [eid] [:db.fn/retractEntity eid]) pages)))
+
+(defn delete-file-tx
+  [repo-url file-path]
+  (->>
+   (concat
+    (delete-file-blocks! repo-url file-path)
+    (delete-file-pages! repo-url file-path)
+    [[:db.fn/retractEntity [:file/path file-path]]])
+   (remove nil?)))
+
+(defn delete-file!
+  [repo-url file-path]
+  (transact! repo-url (delete-file-tx repo-url file-path)))
+
+(defn delete-pages-by-files
+  [files]
+  (let [pages (->> (mapv get-file-page files)
+                   (remove nil?))]
+    (when (seq pages)
+      (mapv (fn [page] [:db.fn/retractEntity [:page/name page]]) (map string/lower-case pages)))))

+ 91 - 0
src/main/frontend/db/query_custom.cljs

@@ -0,0 +1,91 @@
+(ns frontend.db.query-custom
+  "Custom queries."
+  (:require [datascript.core :as d]
+            [frontend.db.utils :as db-utils :refer [date->int]]
+            [frontend.db.model :as model]
+            [cljs-time.core :as t]
+            [frontend.state :as state]
+            [clojure.string :as string]
+            [cljs.reader :as reader]
+            [frontend.extensions.sci :as sci]
+            [lambdaisland.glogi :as log]
+            [frontend.util :as util]))
+
+(defn- resolve-input
+  [input]
+  (cond
+    (= :today input)
+    (date->int (t/today))
+    (= :yesterday input)
+    (date->int (t/yesterday))
+    (= :tomorrow input)
+    (date->int (t/plus (t/today) (t/days 1)))
+    (= :current-page input)
+    (string/lower-case (state/get-current-page))
+    (and (keyword? input)
+         (re-find #"^\d+d(-before)?$" (name input)))
+    (let [input (name input)
+          days (util/parse-int (subs input 0 (dec (count input))))]
+      (date->int (t/minus (t/today) (t/days days))))
+    (and (keyword? input)
+         (re-find #"^\d+d(-after)?$" (name input)))
+    (let [input (name input)
+          days (util/parse-int (subs input 0 (dec (count input))))]
+      (date->int (t/plus (t/today) (t/days days))))
+
+    :else
+    input))
+
+(defn- custom-query-aux
+  [{:keys [query inputs result-transform] :as query'} query-opts]
+  (try
+    (let [inputs (map resolve-input inputs)
+          repo (state/get-current-repo)
+          k [:custom query']]
+      (apply model/q repo k query-opts query inputs))
+    (catch js/Error e
+      (println "Custom query failed: ")
+      (js/console.dir e))))
+
+(defn custom-query
+  ([query]
+   (custom-query query {}))
+  ([query query-opts]
+   (when-let [query' (cond
+                       (and (string? query)
+                            (not (string/blank? query)))
+                       (reader/read-string query)
+
+                       (map? query)
+                       query
+
+                       :else
+                       nil)]
+     (custom-query-aux query' query-opts))))
+
+(defn custom-query-result-transform
+  [query-result remove-blocks q]
+  (let [repo (state/get-current-repo)
+        result (db-utils/seq-flatten query-result)
+        block? (:block/uuid (first result))]
+    (if block?
+      (let [result (if (seq remove-blocks)
+                     (let [remove-blocks (set remove-blocks)]
+                       (remove (fn [h]
+                                 (contains? remove-blocks (:block/uuid h)))
+                               result))
+                     result)
+            result (some->> result
+                            (db-utils/with-repo repo)
+                            (model/with-block-refs-count repo)
+                            (model/sort-blocks))]
+        (if-let [result-transform (:result-transform q)]
+          (if-let [f (sci/eval-string (pr-str result-transform))]
+            (try
+              (sci/call-fn f result)
+              (catch js/Error e
+                (log/error :sci/call-error e)
+                result))
+            result)
+          (db-utils/group-by-page result)))
+      result)))

+ 213 - 0
src/main/frontend/db/react.cljs

@@ -0,0 +1,213 @@
+(ns frontend.db.react
+  "Transact the tx with some specified relationship so that the components will
+  be refreshed when subscribed data changed.
+  It'll be great if we can find an automatically resolving and performant
+  solution.
+  "
+  (:require [frontend.db.conn :as conn]
+            [frontend.db.utils :as db-utils]
+            [frontend.db.model :as model]
+            [frontend.state :as state]
+            [frontend.date :as date]
+            [frontend.util :as util :refer-macros [profile] :refer [react]]
+            [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.format :as format]
+            [cljs-time.core :as t]
+            [cljs-time.coerce :as tc]
+            [frontend.utf8 :as utf8]
+            [datascript.core :as d]
+            [lambdaisland.glogi :as log]))
+
+;; TODO: Extract several parts to handlers
+
+(defn get-current-page
+  []
+  (let [match (:route-match @state/state)
+        route-name (get-in match [:data :name])
+        tag? (= route-name :tag)
+        page (case route-name
+               :page
+               (get-in match [:path-params :name])
+
+               :file
+               (get-in match [:path-params :path])
+
+               :tag
+               (get-in match [:path-params :name])
+
+               (date/journal-name))]
+    (when page
+      (let [page-name (util/url-decode (string/lower-case page))]
+        (model/entity (if tag?
+                        [:tag/name page-name]
+                        [:page/name page-name]))))))
+
+(defn get-current-priority
+  []
+  (let [match (:route-match @state/state)
+        route-name (get-in match [:data :name])]
+    (when (= route-name :page)
+      (when-let [page-name (get-in match [:path-params :name])]
+        (and (contains? #{"a" "b" "c"} (string/lower-case page-name))
+             (string/upper-case page-name))))))
+
+(defn get-current-marker
+  []
+  (let [match (:route-match @state/state)
+        route-name (get-in match [:data :name])]
+    (when (= route-name :page)
+      (when-let [page-name (get-in match [:path-params :name])]
+        (and (util/marker? page-name)
+             (string/upper-case page-name))))))
+
+(defn get-handler-keys
+  [{:keys [key data]}]
+  (cond
+    (coll? key)
+    [key]
+
+    :else
+    (case key
+      (:block/change :block/insert)
+      (when-let [blocks (seq data)]
+        (let [pre-block? (:block/pre-block? (first blocks))
+              current-priority (get-current-priority)
+              current-marker (get-current-marker)
+              current-page-id (:db/id (get-current-page))
+              {:block/keys [page]} (first blocks)
+              handler-keys (->>
+                            (util/concat-without-nil
+                             (mapcat
+                              (fn [block]
+                                (when-let [page-id (:db/id (:block/page block))]
+                                  [[:blocks (:block/uuid block)]
+                                   [:page/blocks page-id]
+                                   [:page/ref-pages page-id]]))
+                              blocks)
+
+                             (when pre-block?
+                               [[:contents]
+                                [:page/published]])
+
+                             ;; affected priority
+                             (when current-priority
+                               [[:priority/blocks current-priority]])
+
+                             (when current-marker
+                               [[:marker/blocks current-marker]])
+
+                             (when current-page-id
+                               [[:page/ref-pages current-page-id]
+                                [:page/refed-blocks current-page-id]
+                                [:page/mentioned-pages current-page-id]])
+
+                             ;; refed-pages
+                             (apply concat
+                                    (for [{:block/keys [ref-pages]} blocks]
+                                      (map (fn [page]
+                                             (when-let [page (model/entity [:page/name (:page/name page)])]
+                                               [:page/refed-blocks (:db/id page)]))
+                                           ref-pages)))
+
+                             ;; refed-blocks
+                             (apply concat
+                                    (for [{:block/keys [ref-blocks]} blocks]
+                                      (map (fn [ref-block]
+                                             [:block/refed-blocks (last ref-block)])
+                                           ref-blocks))))
+                            (distinct))
+              refed-pages (map
+                           (fn [[k page-id]]
+                             (if (= k :page/refed-blocks)
+                               [:page/ref-pages page-id]))
+                           handler-keys)
+              custom-queries (some->>
+                              (filter (fn [v]
+                                        (and (= (first v) (state/get-current-repo))
+                                             (= (second v) :custom)))
+                                      (keys @model/query-state))
+                              (map (fn [v]
+                                     (vec (drop 1 v)))))
+              block-blocks (some->>
+                            (filter (fn [v]
+                                      (and (= (first v) (state/get-current-repo))
+                                           (= (second v) :block/block)))
+                                    (keys @model/query-state))
+                            (map (fn [v]
+                                   (vec (drop 1 v)))))]
+          (->>
+           (util/concat-without-nil
+            handler-keys
+            refed-pages
+            custom-queries
+            block-blocks)
+           distinct)))
+      [[key]])))
+
+(defn transact-react!
+  [repo-url tx-data {:keys [key data files-db?] :as handler-opts
+                     :or {files-db? false}}]
+  (when-not config/publishing?
+    (try
+      (let [repo-url (or repo-url (state/get-current-repo))
+            tx-data (->> (util/remove-nils tx-data)
+                         (remove nil?))
+            get-conn (fn [] (if files-db?
+                              (conn/get-files-conn repo-url)
+                              (conn/get-conn repo-url false)))]
+        (when (and (seq tx-data) (get-conn))
+          (let [tx-result (profile "Transact!" (d/transact! (get-conn) (vec tx-data)))
+                db (:db-after tx-result)
+                handler-keys (get-handler-keys handler-opts)]
+            (doseq [handler-key handler-keys]
+              (let [handler-key (vec (cons repo-url handler-key))]
+                (when-let [cache (get @model/query-state handler-key)]
+                  (let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
+                    (when (or query query-fn)
+                      (let [new-result (->
+                                        (cond
+                                          query-fn
+                                          (profile
+                                           "Query:"
+                                           (doall (query-fn db)))
+
+                                          inputs-fn
+                                          (let [inputs (inputs-fn)]
+                                            (apply d/q query db inputs))
+
+                                          (keyword? query)
+                                          (model/get-key-value repo-url query)
+
+                                          (seq inputs)
+                                          (apply d/q query db inputs)
+
+                                          :else
+                                          (d/q query db))
+                                        transform-fn)]
+                        (model/set-new-result! handler-key new-result))))))))))
+      (catch js/Error e
+        ;; FIXME: check error type and notice user
+        (log/error :db/transact! e)))))
+
+(defn set-key-value
+  [repo-url key value]
+  (if value
+    (transact-react! repo-url [(model/kv key value)]
+                     {:key [:kv key]})
+    (model/remove-key! repo-url key)))
+
+(defn set-file-content!
+  [repo path content]
+  (when (and repo path)
+    (let [tx-data {:file/path path
+                   :file/content content
+                   :file/last-modified-at (util/time-ms)}
+          tx-data (if (config/local-db? repo)
+                    (dissoc tx-data :file/last-modified-at)
+                    tx-data)]
+      (transact-react!
+       repo
+       [tx-data]
+       {:key [:file/content path]
+        :files-db? true}))))

+ 61 - 0
src/main/frontend/db/utils.cljs

@@ -0,0 +1,61 @@
+(ns frontend.db.utils
+  "Some utils are required by other namespace in frontend.db package."
+  (:require [datascript.core :as d]
+            [frontend.state :as state]
+            [clojure.string :as string]
+            [datascript.transit :as dt]
+            [frontend.util :as util]
+            [frontend.date :as date]))
+
+;; transit serialization
+
+(defn db->string [db]
+  (dt/write-transit-str db))
+
+(defn db->json [db]
+  (js/JSON.stringify
+   (into-array
+    (for [d (d/datoms db :eavt)]
+      #js [(:e d) (name (:a d)) (:v d)]))))
+
+(defn string->db [s]
+  (dt/read-transit-str s))
+
+(defn me-tx
+  [db {:keys [name email avatar]}]
+  (util/remove-nils {:me/name name
+                     :me/email email
+                     :me/avatar avatar}))
+
+(defn seq-flatten [col]
+  (flatten (seq col)))
+
+(defn sort-by-pos
+  [blocks]
+  (sort-by
+   #(get-in % [:block/meta :start-pos])
+   blocks))
+
+(defn group-by-page
+  [blocks]
+  (some->> blocks
+           (group-by :block/page)
+           (sort-by (fn [[p _blocks]] (:page/last-modified-at p)) >)))
+
+(defn get-tx-id [tx-report]
+  (get-in tx-report [:tempids :db/current-tx]))
+
+(defn get-max-tx-id
+  [db]
+  (:max-tx db))
+
+(defn date->int
+  [date]
+  (util/parse-int
+   (string/replace (date/ymd date) "/" "")))
+
+(defn with-repo
+  [repo blocks]
+  (map (fn [block]
+         (assoc block :block/repo repo))
+       blocks))

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

@@ -1,5 +1,5 @@
 (ns frontend.db-mixins
-  (:require [frontend.db :as db]))
+  (:require [frontend.db.model :as db]))
 
 (def query
   {:wrap-render

+ 70 - 53
src/main/frontend/extensions/code.cljs

@@ -15,6 +15,8 @@
             ["codemirror/addon/edit/closebrackets"]
             ["codemirror/mode/clojure/clojure"]
             ["codemirror/mode/javascript/javascript"]
+            ["codemirror/mode/clike/clike"]
+            ["codemirror/mode/vue/vue"]
             ["codemirror/mode/commonlisp/commonlisp"]
             ["codemirror/mode/coffeescript/coffeescript"]
             ["codemirror/mode/css/css"]
@@ -53,9 +55,14 @@
             format (:block/format block)
             ;; Get newest state
             pos-meta (:pos-meta state)
-            {:keys [start_pos end_pos]} @pos-meta
-            value (str "\n" (string/trimr value) "\n")
             content (:block/content block)
+            {:keys [start_pos end_pos]} @pos-meta
+            prev-content (utf8/substring (utf8/encode content)
+                                         0 start_pos)
+            value (str (if (not= "\n" (last prev-content))
+                         "\n")
+                       (string/trimr value)
+                       "\n")
             content' (utf8/insert! content
                                    start_pos
                                    end_pos
@@ -79,52 +86,69 @@
       :else
       nil)))
 
+(defn- text->cm-mode
+  [text]
+  (when text
+    (let [mode (string/lower-case text)]
+      (case mode
+        "html" "text/html"
+        "c" "text/x-csrc"
+        "c++" "text/x-c++src"
+        "java" "text/x-java"
+        "c#" "text/x-csharp"
+        "csharp" "text/x-csharp"
+        "objective-c" "text/x-objectivec"
+        "scala" "text/x-scala"
+        mode))))
+
 (defn render!
   [state]
-  (let [[config id attr code pos_meta] (:rum/args state)
-        original-mode (get attr :data-lang)
-        mode (or original-mode "javascript")
-        clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
-        mode (if clojure? "clojure" mode)
-        lisp? (or clojure?
-                  (contains? #{"scheme" "racket" "lisp"} mode))
-        textarea (gdom/getElement id)
-        editor (or
-                @(:editor-atom state)
-                (when textarea
-                  (from-textarea textarea
-                                 #js {:mode mode
-                                      :matchBrackets lisp?
-                                      :autoCloseBrackets true
-                                      :lineNumbers true
-                                      :extraKeys #js {"Esc" (fn [cm]
-                                                              (let [save! #(save-file-or-block-when-blur-or-esc! cm textarea config state)]
-                                                                (if-let [block-id (:block/uuid config)]
-                                                                  (let [block (db/pull [:block/uuid block-id])
-                                                                        value (.getValue cm)
-                                                                        textarea-value (gobj/get textarea "value")
-                                                                        changed? (not= value textarea-value)]
-                                                                    (if changed?
-                                                                      (save!)
-                                                                      (editor-handler/edit-block! block :max (:block/format block) block-id)))
-                                                                  (save!))))}})))]
-    (when editor
-      (let [element (.getWrapperElement editor)]
-        (.on editor "blur" (fn []
-                             (save-file-or-block-when-blur-or-esc! editor textarea config state)))
-        (.addEventListener element "click"
-                           (fn [e]
-                             (util/stop e)))
-        (.save editor)
-        (.refresh editor)))
-    editor))
+  (let [editor-atom (:editor-atom state)]
+    (if @editor-atom
+      @editor-atom
+      (let [[config id attr code pos_meta] (:rum/args state)
+           original-mode (get attr :data-lang)
+           mode (or original-mode "javascript")
+           clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
+           mode (if clojure? "clojure" (text->cm-mode mode))
+           lisp? (or clojure?
+                     (contains? #{"scheme" "racket" "lisp"} mode))
+           textarea (gdom/getElement id)
+           editor (or
+                   @(:editor-atom state)
+                   (when textarea
+                     (from-textarea textarea
+                                    #js {:mode mode
+                                         :matchBrackets lisp?
+                                         :autoCloseBrackets true
+                                         :lineNumbers true
+                                         :extraKeys #js {"Esc" (fn [cm]
+                                                                 (let [save! #(save-file-or-block-when-blur-or-esc! cm textarea config state)]
+                                                                   (if-let [block-id (:block/uuid config)]
+                                                                     (let [block (db/pull [:block/uuid block-id])
+                                                                           value (.getValue cm)
+                                                                           textarea-value (gobj/get textarea "value")
+                                                                           changed? (not= value textarea-value)]
+                                                                       (if changed?
+                                                                         (save!)
+                                                                         (editor-handler/edit-block! block :max (:block/format block) block-id)))
+                                                                     (save!))))}})))]
+       (when editor
+         (let [element (.getWrapperElement editor)]
+           (.on editor "blur" (fn []
+                                (save-file-or-block-when-blur-or-esc! editor textarea config state)))
+           (.addEventListener element "click"
+                              (fn [e]
+                                (util/stop e)))
+           (.save editor)
+           (.refresh editor)))
+       editor))))
 
 (defn- load-and-render!
   [state]
   (let [editor-atom (:editor-atom state)]
-    (js/setTimeout (fn []
-                     (let [editor (render! state)]
-                       (reset! editor-atom editor))) 10)))
+    (let [editor (render! state)]
+      (reset! editor-atom editor))))
 
 (rum/defcs editor < rum/reactive
   {:init (fn [state]
@@ -134,20 +158,13 @@
    :did-mount (fn [state]
                 (load-and-render! state)
                 state)
-   :did-update (fn [state]
-                 (when-let [editor-atom (:editor-atom state)]
-                   (let [editor @editor-atom
-                         code (nth (:rum/args state) 3)]
-                     (when editor
-                       (.setValue (.getDoc editor) code))))
-                 (when-let [pos-meta (:pos-meta state)]
-                   (reset! pos-meta (last (:rum/args state))))
-                 (load-and-render! state)
-                 state)}
+   :did-remount (fn [state]
+                  (load-and-render! state)
+                  state)}
   [state config id attr code pos_meta]
   [:div.extensions__code
    [:div.extensions__code-lang
-    (let [mode (get attr :data-lang "javascript")]
+    (let [mode (string/lower-case (get attr :data-lang "javascript"))]
       (if (= mode "text/x-clojure")
         "clojure"
         mode))]

+ 12 - 0
src/main/frontend/format/mldoc.cljs

@@ -173,3 +173,15 @@
 (defn plain->text
   [plains]
   (string/join (map last plains)))
+
+(defn parse-properties
+  [content format]
+  (let [ast (->> (->edn content
+                        (default-config format))
+                 (map first))
+        properties (let [properties (and (seq ast)
+                                         (= "Properties" (ffirst ast))
+                                         (last (first ast)))]
+                     (if (and properties (seq properties))
+                       properties))]
+    (into {} properties)))

+ 9 - 3
src/main/frontend/fs.cljs

@@ -1,6 +1,7 @@
 (ns frontend.fs
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
+            [frontend.state :as state]
             [clojure.string :as string]
             [frontend.idb :as idb]
             [frontend.db :as db]
@@ -174,13 +175,18 @@
                     new-created? (nil? last-modified-at)
                     not-changed? (= last-modified-at local-last-modified-at)
                     format (-> (util/get-file-ext path)
-                               (config/get-file-format))]
+                               (config/get-file-format))
+                    pending-writes (state/get-write-chan-length)]
               ;; (println {:last-modified-at last-modified-at
               ;;           :local-last-modified-at local-last-modified-at
               ;;           :not-changed? not-changed?
-              ;;           :new-created? new-created?})
+              ;;           :new-created? new-created?
+              ;;           :pending-writes pending-writes})
               (if (and local-content old-content new?
-                       (or not-changed? new-created?))
+                       (or
+                        (> pending-writes 0)
+                        not-changed?
+                        new-created?))
                 (do
                   (p/let [_ (utils/verifyPermission file-handle true)
                           _ (utils/writeFile file-handle content)

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

@@ -32,7 +32,6 @@
               (repo-handler/create-today-journal!))
             (when-let [repo (state/get-current-repo)]
               (when (and (search-db/empty? repo)
-                         (not (state/file-in-writing!))
                          (state/input-idle? repo))
                 (search/rebuild-indices!))))]
     (f)

+ 181 - 0
src/main/frontend/handler/block.cljs

@@ -0,0 +1,181 @@
+(ns frontend.handler.block
+  (:require [frontend.util :as util]
+            [clojure.walk :as walk]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [frontend.format.mldoc :as mldoc]
+            [frontend.date :as date]
+            [frontend.config :as config]
+            [datascript.core :as d]))
+
+(defn blocks->vec-tree [col]
+  (let [col (map (fn [h] (cond->
+                          h
+                           (not (:block/dummy? h))
+                           (dissoc h :block/meta))) col)
+        parent? (fn [item children]
+                  (and (seq children)
+                       (every? #(< (:block/level item) (:block/level %)) children)))]
+    (loop [col (reverse col)
+           children (list)]
+      (if (empty? col)
+        children
+        (let [[item & others] col
+              cur-level (:block/level item)
+              bottom-level (:block/level (first children))
+              pre-block? (:block/pre-block? item)]
+          (cond
+            (empty? children)
+            (recur others (list item))
+
+            (<= bottom-level cur-level)
+            (recur others (conj children item))
+
+            pre-block?
+            (recur others (cons item children))
+
+            (> bottom-level cur-level)      ; parent
+            (let [[children other-children] (split-with (fn [h]
+                                                          (> (:block/level h) cur-level))
+                                                        children)
+
+                  children (cons
+                            (assoc item :block/children children)
+                            other-children)]
+              (recur others children))))))))
+
+;; recursively with children content for tree
+(defn get-block-content-rec
+  ([block]
+   (get-block-content-rec block (fn [block] (:block/content block))))
+  ([block transform-fn]
+   (let [contents (atom [])
+         _ (walk/prewalk
+            (fn [form]
+              (when (map? form)
+                (when-let [content (:block/content form)]
+                  (swap! contents conj (transform-fn form))))
+              form)
+            block)]
+     (apply util/join-newline @contents))))
+
+;; with children content
+(defn get-block-full-content
+  ([repo block-id]
+   (get-block-full-content repo block-id (fn [block] (:block/content block))))
+  ([repo block-id transform-fn]
+   (let [blocks (db/get-block-and-children-no-cache repo block-id)]
+     (->> blocks
+          (map transform-fn)
+          (apply util/join-newline)))))
+
+(defn get-block-end-pos-rec
+  [repo block]
+  (let [children (:block/children block)]
+    (if (seq children)
+      (get-block-end-pos-rec repo (last children))
+      (if-let [end-pos (get-in block [:block/meta :end-pos])]
+        end-pos
+        (when-let [block (db/entity repo [:block/uuid (:block/uuid block)])]
+          (get-in block [:block/meta :end-pos]))))))
+
+(defn get-block-ids
+  [block]
+  (let [ids (atom [])
+        _ (walk/prewalk
+           (fn [form]
+             (when (map? form)
+               (when-let [id (:block/uuid form)]
+                 (swap! ids conj id)))
+             form)
+           block)]
+    @ids))
+
+(defn collapse-block!
+  [block]
+  (let [repo (:block/repo block)]
+    (db/transact! repo
+                  [{:block/uuid (:block/uuid block)
+                    :block/collapsed? true}])))
+
+(defn collapse-blocks!
+  [block-ids]
+  (let [repo (state/get-current-repo)]
+    (db/transact! repo
+                  (map
+                   (fn [id]
+                     {:block/uuid id
+                      :block/collapsed? true})
+                   block-ids))))
+
+(defn expand-block!
+  [block]
+  (let [repo (:block/repo block)]
+    (db/transact! repo
+                  [{:block/uuid (:block/uuid block)
+                    :block/collapsed? false}])))
+
+(defn expand-blocks!
+  [block-ids]
+  (let [repo (state/get-current-repo)]
+    (db/transact! repo
+                  (map
+                   (fn [id]
+                     {:block/uuid id
+                      :block/collapsed? false})
+                   block-ids))))
+
+(defn pre-block-with-only-title?
+  [repo block-id]
+  (when-let [block (db/entity repo [:block/uuid block-id])]
+    (let [properties (:page/properties (:block/page block))]
+      (and (:title properties)
+           (= 1 (count properties))
+           (let [ast (mldoc/->edn (:block/content block) (mldoc/default-config (:block/format block)))]
+             (or
+              (empty? (rest ast))
+              (every? (fn [[[typ break-lines]] _]
+                        (and (= typ "Paragraph")
+                             (every? #(= % ["Break_Line"]) break-lines))) (rest ast))))))))
+
+(defn with-dummy-block
+  ([blocks format]
+   (with-dummy-block blocks format {} {}))
+  ([blocks format default-option {:keys [journal? page-name]
+                                  :or {journal? false}}]
+   (let [format (or format (state/get-preferred-format) :markdown)
+         blocks (if (and journal?
+                      (seq blocks)
+                      (when-let [title (second (first (:block/title (first blocks))))]
+                        (date/valid-journal-title? title)))
+                  (rest blocks)
+                  blocks)
+         blocks (vec blocks)]
+     (cond
+       (and (seq blocks)
+         (or (and (> (count blocks) 1)
+               (:block/pre-block? (first blocks)))
+           (and (>= (count blocks) 1)
+             (not (:block/pre-block? (first blocks))))))
+       blocks
+
+       :else
+       (let [last-block (last blocks)
+             end-pos (get-in last-block [:block/meta :end-pos] 0)
+             dummy (merge last-block
+                     (let [uuid (d/squuid)]
+                       {:block/uuid uuid
+                        :block/title ""
+                        :block/content (config/default-empty-block format)
+                        :block/format format
+                        :block/level 2
+                        :block/priority nil
+                        :block/anchor (str uuid)
+                        :block/meta {:start-pos end-pos
+                                     :end-pos end-pos}
+                        :block/body nil
+                        :block/dummy? true
+                        :block/marker nil
+                        :block/pre-block? false})
+                     default-option)]
+         (conj blocks dummy))))))

+ 20 - 19
src/main/frontend/handler/dnd.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.dnd
   (:require [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.config :as config]
             [frontend.state :as state]
             [frontend.util :as util :refer-macros [profile]]
@@ -13,8 +14,8 @@
 
 (defn- remove-block-child!
   [target-block parent-block]
-  (let [child-ids (set (db/get-block-ids target-block))]
-    (db/get-block-content-rec
+  (let [child-ids (set (block-handler/get-block-ids target-block))]
+    (block-handler/get-block-content-rec
      parent-block
      (fn [{:block/keys [uuid level content]}]
        (if (contains? child-ids uuid)
@@ -33,7 +34,7 @@
         format (:block/format target-block)
         pattern (config/get-block-pattern format)
         block-changes (atom [])
-        all-content (db/get-block-content-rec
+        all-content (block-handler/get-block-content-rec
                      target-block
                      (fn [{:block/keys [uuid level content]
                            :as block}]
@@ -185,7 +186,7 @@
     (= direction :up)
     (let [offset-block-id (if nested?
                             (:block/uuid to-block)
-                            (last (db/get-block-ids to-block)))
+                            (last (block-handler/get-block-ids to-block)))
           offset-end-pos (get-end-pos
                           (db/entity repo [:block/uuid offset-block-id]))]
       (rebuild-dnd-blocks repo target-file target-child?
@@ -197,7 +198,7 @@
     (= direction :down)
     (let [offset-block-id (if nested?
                             (:block/uuid to-block)
-                            (last (db/get-block-ids to-block)))
+                            (last (block-handler/get-block-ids to-block)))
           target-start-pos (get-start-pos target-block)]
       (rebuild-dnd-blocks repo target-file target-child?
                           target-start-pos
@@ -213,7 +214,7 @@
     (let [old-file-content (db/get-file (:file/path (db/entity (:db/id (:block/file target-block)))))
           old-file-content (utf8/encode old-file-content)
           subs (fn [start-pos end-pos] (utf8/substring old-file-content start-pos end-pos))
-          bottom-content (db/get-block-content-rec bottom-block)
+          bottom-content (block-handler/get-block-content-rec bottom-block)
           top-content (remove-block-child! bottom-block top-block)
           top-area (subs 0 (get-start-pos top-block))
           bottom-area (subs
@@ -221,13 +222,13 @@
                          (and nested? (= direction :down))
                          (get-end-pos bottom-block)
                          target-child?
-                         (db/get-block-end-pos-rec repo top-block)
+                         (block-handler/get-block-end-pos-rec repo top-block)
                          :else
-                         (db/get-block-end-pos-rec repo bottom-block))
+                         (block-handler/get-block-end-pos-rec repo bottom-block))
                        nil)
           between-area (if (= direction :down)
-                         (subs (db/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
-                         (subs (db/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
+                         (subs (block-handler/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
+                         (subs (block-handler/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
           up-content (when (= direction :up)
                        (cond
                          nested?
@@ -235,7 +236,7 @@
                                             target-content
                                             (if target-child?
                                               (remove-block-child! target-block (:block/children to-block))
-                                              (db/get-block-content-rec (:block/children top-block))))
+                                              (block-handler/get-block-content-rec (:block/children top-block))))
                          (and top? target-child?)
                          (util/join-newline target-content (remove-block-child! target-block to-block))
 
@@ -299,7 +300,7 @@
       ;;             path
       ;;             new-file-content
       ;;             {:re-render-root? true})
-      )))
+)))
 
 (defn- move-block-in-different-files
   [repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
@@ -308,9 +309,9 @@
         target-file-content (db/get-file repo target-file-path)
         to-file (db/entity repo (:db/id (:block/file to-block)))
         to-file-path (:file/path to-file)
-        target-block-end-pos (db/get-block-end-pos-rec repo target-block)
+        target-block-end-pos (block-handler/get-block-end-pos-rec repo target-block)
         to-block-start-pos (get-start-pos to-block)
-        to-block-end-pos (db/get-block-end-pos-rec repo to-block)
+        to-block-end-pos (block-handler/get-block-end-pos-rec repo to-block)
         new-target-file-content (utf8/delete! target-file-content
                                               (get-start-pos target-block)
                                               target-block-end-pos)
@@ -348,7 +349,7 @@
                           :else
                           (let [offset-block-id (if nested?
                                                   (:block/uuid to-block)
-                                                  (last (db/get-block-ids to-block)))
+                                                  (last (block-handler/get-block-ids to-block)))
                                 offset-end-pos (get-end-pos
                                                 (db/entity repo [:block/uuid offset-block-id]))]
                             (rebuild-dnd-blocks repo to-file target-child?
@@ -376,9 +377,9 @@
         target-file-content (db/get-file target-block-repo target-file-path)
         to-file (db/entity to-block-repo (:db/id (:block/file to-block)))
         to-file-path (:file/path to-file)
-        target-block-end-pos (db/get-block-end-pos-rec target-block-repo target-block)
+        target-block-end-pos (block-handler/get-block-end-pos-rec target-block-repo target-block)
         to-block-start-pos (get-start-pos to-block)
-        to-block-end-pos (db/get-block-end-pos-rec to-block-repo to-block)
+        to-block-end-pos (block-handler/get-block-end-pos-rec to-block-repo to-block)
         new-target-file-content (utf8/delete! target-file-content
                                               (get-start-pos target-block)
                                               target-block-end-pos)
@@ -396,7 +397,7 @@
                                 (utf8/substring to-file-content separate-pos))))
         target-delete-tx (map (fn [id]
                                 [:db.fn/retractEntity [:block/uuid id]])
-                              (db/get-block-ids target-block))
+                              (block-handler/get-block-ids target-block))
         [target-modified-time to-modified-time]
         (let [modified-at (tc/to-long (t/now))]
           [[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
@@ -417,7 +418,7 @@
                           :else
                           (let [offset-block-id (if nested?
                                                   (:block/uuid to-block)
-                                                  (last (db/get-block-ids to-block)))
+                                                  (last (block-handler/get-block-ids to-block)))
                                 offset-end-pos (get-end-pos
                                                 (db/entity to-block-repo [:block/uuid offset-block-id]))]
                             (rebuild-dnd-blocks to-block-repo to-file target-child?

+ 132 - 133
src/main/frontend/handler/editor.cljs

@@ -9,6 +9,7 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.draw :as draw]
             [frontend.handler.expand :as expand]
+            [frontend.format.mldoc :as mldoc]
             [frontend.format :as format]
             [frontend.format.block :as block]
             [frontend.image :as image]
@@ -397,7 +398,7 @@
          page (db/entity repo (:db/id page))
          [old-properties new-properties] (when pre-block?
                                            [(:page/properties (db/entity (:db/id page)))
-                                            (db/parse-properties value format)])
+                                            (mldoc/parse-properties value format)])
          page-tags (when-let [tags (:tags new-properties)]
                      (util/->tags tags))
          page-alias (when-let [alias (:alias new-properties)]
@@ -460,7 +461,7 @@
                                     (text/remove-level-spaces value (keyword format)))]
                    (p/let [_ (fs/create-if-not-exists repo dir file-path content)
                            _ (git-handler/git-add repo path)]
-                     (db/reset-file! repo path content)
+                     (file-handler/reset-file! repo path content)
                      (ui-handler/re-render-root!)
 
                      ;; Continue to edit the last block
@@ -703,11 +704,11 @@
                                                                    (:page/name page)))]
               (p/let [_ (fs/create-if-not-exists repo dir file-path content)
                       _ (git-handler/git-add repo path)]
-                (db/reset-file! repo path
-                                (str content
-                                     (text/remove-level-spaces value (keyword format))
-                                     "\n"
-                                     snd-block-text))
+                (file-handler/reset-file! repo path
+                                          (str content
+                                               (text/remove-level-spaces value (keyword format))
+                                               "\n"
+                                               snd-block-text))
                 (ui-handler/re-render-root!)
 
                 ;; Continue to edit the last block
@@ -769,8 +770,7 @@
   [state]
   (when (and (not config/publishing?)
              ;; skip this operation if it's inserting
-             (not= :insert (state/get-editor-op))
-             (not (state/file-in-writing!)))
+             (not= :insert (state/get-editor-op)))
     (state/set-editor-op! :insert)
     (let [{:keys [block value format id config]} (get-state state)
           block-id (:block/uuid block)
@@ -967,8 +967,7 @@
   [state repo e]
   (let [{:keys [id block-id block-parent-id dummy? value pos format]} (get-state state)]
     (when (and block-id
-               (not= :block/delete (state/get-editor-op))
-               (not (state/file-in-writing!)))
+               (not= :block/delete (state/get-editor-op)))
       (state/set-editor-op! :block/delete)
       (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))
@@ -1353,32 +1352,34 @@
 
 (defn save-current-block-when-idle!
   []
-  (when-not (state/file-in-writing!)
-    (when-let [repo (state/get-current-repo)]
-      (when (state/input-idle? repo)
-        (state/set-editor-op! :auto-save)
-        (try
-          (let [input-id (state/get-edit-input-id)
-                block (state/get-edit-block)
-                db-block (when-let [block-id (:block/uuid block)]
-                           (db/pull [:block/uuid block-id]))
-                elem (and input-id (gdom/getElement input-id))
-                db-content (:block/content db-block)
-                db-content-without-heading (and db-content
-                                                (util/safe-subs db-content (:block/level db-block)))
-                value (and elem (gobj/get elem "value"))]
-            (when (and block value db-content-without-heading
-                       (or
-                        (not= (string/trim db-content-without-heading)
-                              (string/trim value))))
-              (let [cur-pos (util/get-input-pos elem)]
-                (save-block-aux! db-block value (:block/format db-block))
-               ;; Restore the cursor after saving the block
-                (when (and elem cur-pos)
-                  (util/set-caret-pos! elem cur-pos)))))
-          (catch js/Error error
-            (log/error :save-block-failed error)))
-        (state/set-editor-op! nil)))))
+  (when-let [repo (state/get-current-repo)]
+    (when (and (state/input-idle? repo)
+               (not (state/get-editor-show-page-search?))
+               (not (state/get-editor-show-page-search-hashtag?))
+               (not (state/get-editor-show-block-search?)))
+      (state/set-editor-op! :auto-save)
+      (try
+        (let [input-id (state/get-edit-input-id)
+              block (state/get-edit-block)
+              db-block (when-let [block-id (:block/uuid block)]
+                         (db/pull [:block/uuid block-id]))
+              elem (and input-id (gdom/getElement input-id))
+              db-content (:block/content db-block)
+              db-content-without-heading (and db-content
+                                              (util/safe-subs db-content (:block/level db-block)))
+              value (and elem (gobj/get elem "value"))]
+          (when (and block value db-content-without-heading
+                     (or
+                      (not= (string/trim db-content-without-heading)
+                            (string/trim value))))
+            (let [cur-pos (util/get-input-pos elem)]
+              (save-block-aux! db-block value (:block/format db-block))
+              ;; Restore the cursor after saving the block
+              (when (and elem cur-pos)
+                (util/set-caret-pos! elem cur-pos)))))
+        (catch js/Error error
+          (log/error :save-block-failed error)))
+      (state/set-editor-op! nil))))
 
 (defn on-up-down
   [state e up?]
@@ -1647,40 +1648,39 @@
   ([state direction]
    (adjust-block-level! state direction 100))
   ([state direction retry-limit]
-   (when-not (state/file-in-writing!)
-     (if (= :insert (state/get-editor-op))
-       (if (> retry-limit 0)
-         (js/setTimeout #(adjust-block-level! state direction (dec retry-limit)) 20)
-         (log/error :editor/indent-outdent-retry-max-limit {:direction direction}))
-       (do
-         (state/set-editor-op! :indent-outdent)
-         (let [{:keys [block block-parent-id value config]} (get-state state)
-               start-level (:start-level config)
-               format (:block/format block)
-               level (:block/level block)
-               previous-level (or (get-previous-block-level block-parent-id) 1)
-               [add? remove?] (case direction
-                                :left [false true]
-                                :right [true false]
-                                [(<= level previous-level)
-                                 (and (> level previous-level)
-                                      (> level 2))])
-               final-level (cond
-                             add? (inc level)
-                             remove? (if (> level 2)
-                                       (dec level)
-                                       level)
-                             :else level)
-               new-value (block/with-levels value format (assoc block :block/level final-level))]
-           (when (and
-                  (not (and (= direction :left)
-                            (get config :id)
-                            (util/uuid-string? (get config :id))
-                            (<= final-level start-level)))
-                  (<= (- final-level previous-level) 1))
-             (save-block-if-changed! block new-value
-                                     {:indent-left? (= direction :left)})))
-         (state/set-editor-op! nil))))))
+   (if (= :insert (state/get-editor-op))
+     (if (> retry-limit 0)
+       (js/setTimeout #(adjust-block-level! state direction (dec retry-limit)) 20)
+       (log/error :editor/indent-outdent-retry-max-limit {:direction direction}))
+     (do
+       (state/set-editor-op! :indent-outdent)
+       (let [{:keys [block block-parent-id value config]} (get-state state)
+             start-level (:start-level config)
+             format (:block/format block)
+             level (:block/level block)
+             previous-level (or (get-previous-block-level block-parent-id) 1)
+             [add? remove?] (case direction
+                              :left [false true]
+                              :right [true false]
+                              [(<= level previous-level)
+                               (and (> level previous-level)
+                                    (> level 2))])
+             final-level (cond
+                           add? (inc level)
+                           remove? (if (> level 2)
+                                     (dec level)
+                                     level)
+                           :else level)
+             new-value (block/with-levels value format (assoc block :block/level final-level))]
+         (when (and
+                (not (and (= direction :left)
+                          (get config :id)
+                          (util/uuid-string? (get config :id))
+                          (<= final-level start-level)))
+                (<= (- final-level previous-level) 1))
+           (save-block-if-changed! block new-value
+                                   {:indent-left? (= direction :left)})))
+       (state/set-editor-op! nil)))))
 
 (defn adjust-blocks-level!
   [blocks direction])
@@ -1701,68 +1701,67 @@
 
 (defn move-up-down
   [e up?]
-  (when-not (state/file-in-writing!)
-    (when-let [block-id (:block/uuid (state/get-edit-block))]
-      (let [block-parent-id (state/get-editing-block-dom-id)
-            block (db/entity [:block/uuid block-id])
-            meta (:block/meta block)
-            page (:block/page block)
-            block-dom-node (gdom/getElement block-parent-id)
-            prev-block (get-prev-block-non-collapsed block-dom-node)
-            next-block (get-next-block-non-collapsed block-dom-node)
-            repo (state/get-current-repo)
-            move-upwards-to-parent? (and up? prev-block (< (d/attr prev-block "level") (:block/level block)))
-            move-down-to-higher-level? (and (not up?) next-block (< (d/attr next-block "level") (:block/level block)))]
-        (when-let [sibling-block (cond
-                                   move-upwards-to-parent?
-                                   prev-block
-                                   move-down-to-higher-level?
-                                   next-block
-                                   :else
-                                   (let [f (if up? util/get-prev-block-with-same-level util/get-next-block-with-same-level)]
-                                     (f block-dom-node)))]
-          (when-let [sibling-block-id (d/attr sibling-block "blockid")]
-            (when-let [sibling-block (db/pull-block (medley/uuid sibling-block-id))]
-              (let [sibling-meta (:block/meta sibling-block)
-                    hc1 (db/get-block-and-children-no-cache repo (:block/uuid block))
-                    hc2 (if (or move-upwards-to-parent? move-down-to-higher-level?)
-                          [sibling-block]
-                          (db/get-block-and-children-no-cache repo (:block/uuid sibling-block)))]
-               ;; Same page and next to the other
-                (when (and
-                       (= (:db/id (:block/page block))
-                          (:db/id (:block/page sibling-block)))
-                       (or
-                        (and up? (= (:end-pos (:block/meta (last hc2))) (:start-pos (:block/meta (first hc1)))))
-                        (and (not up?) (= (:end-pos (:block/meta (last hc1))) (:start-pos (:block/meta (first hc2)))))))
-                  (let [hc1-content (block-and-children-content hc1)
-                        hc2-content (block-and-children-content hc2)
-                        file (db/get-block-file (:block/uuid block))
-                        file-path (:file/path file)
-                        old-file-content (db/get-file file-path)
-                        [start-pos end-pos new-content blocks] (if up?
-                                                                 [(:start-pos sibling-meta)
-                                                                  (get-in (last hc1) [:block/meta :end-pos])
-                                                                  (str hc1-content hc2-content)
-                                                                  (concat hc1 hc2)]
-                                                                 [(:start-pos meta)
-                                                                  (get-in (last hc2) [:block/meta :end-pos])
-                                                                  (str hc2-content hc1-content)
-                                                                  (concat hc2 hc1)])]
-                    (when (and start-pos end-pos)
-                      (let [new-file-content (utf8/insert! old-file-content start-pos end-pos new-content)
-                            modified-time (modified-time-tx page file)
-                            blocks-meta (rebuild-blocks-meta start-pos blocks)]
-                        (profile
-                         (str "Move block " (if up? "up: " "down: "))
-                         (repo-handler/transact-react-and-alter-file!
-                          repo
-                          (concat
-                           blocks-meta
-                           modified-time)
-                          {:key :block/change
-                           :data (map (fn [block] (assoc block :block/page page)) blocks)}
-                          [[file-path new-file-content]]))))))))))))))
+  (when-let [block-id (:block/uuid (state/get-edit-block))]
+    (let [block-parent-id (state/get-editing-block-dom-id)
+          block (db/entity [:block/uuid block-id])
+          meta (:block/meta block)
+          page (:block/page block)
+          block-dom-node (gdom/getElement block-parent-id)
+          prev-block (get-prev-block-non-collapsed block-dom-node)
+          next-block (get-next-block-non-collapsed block-dom-node)
+          repo (state/get-current-repo)
+          move-upwards-to-parent? (and up? prev-block (< (d/attr prev-block "level") (:block/level block)))
+          move-down-to-higher-level? (and (not up?) next-block (< (d/attr next-block "level") (:block/level block)))]
+      (when-let [sibling-block (cond
+                                 move-upwards-to-parent?
+                                 prev-block
+                                 move-down-to-higher-level?
+                                 next-block
+                                 :else
+                                 (let [f (if up? util/get-prev-block-with-same-level util/get-next-block-with-same-level)]
+                                   (f block-dom-node)))]
+        (when-let [sibling-block-id (d/attr sibling-block "blockid")]
+          (when-let [sibling-block (db/pull-block (medley/uuid sibling-block-id))]
+            (let [sibling-meta (:block/meta sibling-block)
+                  hc1 (db/get-block-and-children-no-cache repo (:block/uuid block))
+                  hc2 (if (or move-upwards-to-parent? move-down-to-higher-level?)
+                        [sibling-block]
+                        (db/get-block-and-children-no-cache repo (:block/uuid sibling-block)))]
+              ;; Same page and next to the other
+              (when (and
+                     (= (:db/id (:block/page block))
+                        (:db/id (:block/page sibling-block)))
+                     (or
+                      (and up? (= (:end-pos (:block/meta (last hc2))) (:start-pos (:block/meta (first hc1)))))
+                      (and (not up?) (= (:end-pos (:block/meta (last hc1))) (:start-pos (:block/meta (first hc2)))))))
+                (let [hc1-content (block-and-children-content hc1)
+                      hc2-content (block-and-children-content hc2)
+                      file (db/get-block-file (:block/uuid block))
+                      file-path (:file/path file)
+                      old-file-content (db/get-file file-path)
+                      [start-pos end-pos new-content blocks] (if up?
+                                                               [(:start-pos sibling-meta)
+                                                                (get-in (last hc1) [:block/meta :end-pos])
+                                                                (str hc1-content hc2-content)
+                                                                (concat hc1 hc2)]
+                                                               [(:start-pos meta)
+                                                                (get-in (last hc2) [:block/meta :end-pos])
+                                                                (str hc2-content hc1-content)
+                                                                (concat hc2 hc1)])]
+                  (when (and start-pos end-pos)
+                    (let [new-file-content (utf8/insert! old-file-content start-pos end-pos new-content)
+                          modified-time (modified-time-tx page file)
+                          blocks-meta (rebuild-blocks-meta start-pos blocks)]
+                      (profile
+                       (str "Move block " (if up? "up: " "down: "))
+                       (repo-handler/transact-react-and-alter-file!
+                        repo
+                        (concat
+                         blocks-meta
+                         modified-time)
+                        {:key :block/change
+                         :data (map (fn [block] (assoc block :block/page page)) blocks)}
+                        [[file-path new-file-content]])))))))))))))
 
 (defn expand!
   []

+ 32 - 0
src/main/frontend/handler/editor/keyboards.cljs

@@ -0,0 +1,32 @@
+(ns frontend.handler.editor.keyboards
+  (:require [frontend.state :as state]
+            [frontend.util :as util]
+            [frontend.handler.editor :as editor-handler]
+            [dommy.core :as d]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]
+            [frontend.mixins :as mixins]))
+
+;; TODO: don't depend on handler.editor
+
+(defn esc-save! [state]
+  (let [id (nth (:rum/args state) 1)]
+    (mixins/hide-when-esc-or-outside
+     state
+     :on-hide
+     (fn [state e event]
+       (let [target (.-target e)]
+         (if (d/has-class? target "bottom-action") ;; FIXME: not particular case
+           (.preventDefault e)
+           (let [{:keys [on-hide format value block id repo dummy?]} (editor-handler/get-state state)]
+             (when on-hide
+               (on-hide value event))
+             (when
+              (or (= event :esc)
+                  (= event :visibilitychange)
+                  (and (= event :click)
+                       (not (editor-handler/in-auto-complete? (gobj/get target "id")))))
+               (state/clear-edit!))))))
+     :node (gdom/getElement id)
+    ;; :visibilitychange? true
+)))

+ 3 - 3
src/main/frontend/handler/expand.cljs

@@ -4,7 +4,7 @@
             [goog.object :as gobj]
             [frontend.util :as util]
             [frontend.state :as state]
-            [frontend.db :as db]))
+            [frontend.handler.block :as block-handler]))
 
 (defn- hide!
   [element]
@@ -27,7 +27,7 @@
         (let [elements (d/by-class node "ls-block")]
           (doseq [element elements]
             (hide! element))))
-      (db/collapse-block! block))))
+      (block-handler/collapse-block! block))))
 
 (defn expand!
   [block]
@@ -41,7 +41,7 @@
           (doseq [element elements]
             (show! element)))
         (show! e))
-      (db/expand-block! block))))
+      (block-handler/expand-block! block))))
 
 (defn set-bullet-closed!
   [element]

+ 176 - 0
src/main/frontend/handler/extract.cljs

@@ -0,0 +1,176 @@
+(ns frontend.handler.extract
+  "Extract helper."
+  (:require [frontend.util :as util]
+            [frontend.db :as db]
+            [lambdaisland.glogi :as log]
+            [clojure.set :as set]
+            [frontend.utf8 :as utf8]
+            [frontend.date :as date]
+            [clojure.string :as string]
+            [frontend.format.mldoc :as mldoc]
+            [frontend.format.block :as block]
+            [frontend.format :as format]
+            [cljs-time.core :as t]
+            [cljs-time.coerce :as tc]))
+
+(defn- extract-page-list
+  [content]
+  (when-not (string/blank? content)
+    (->> (re-seq #"\[\[([^\]]+)]]" content)
+         (map last)
+         (remove nil?)
+         (map string/lower-case)
+         (distinct))))
+
+(defn- extract-pages-and-blocks
+  [repo-url format ast properties file content utf8-content journal? pages-fn]
+  (try
+    (let [now (tc/to-long (t/now))
+          blocks (block/extract-blocks ast (utf8/length utf8-content) utf8-content)
+          pages (pages-fn blocks ast)
+          ref-pages (atom #{})
+          ref-tags (atom #{})
+          blocks (doall
+                  (mapcat
+                   (fn [[page blocks]]
+                     (if page
+                       (map (fn [block]
+                              (let [block-ref-pages (seq (:block/ref-pages block))]
+                                (when block-ref-pages
+                                  (swap! ref-pages set/union (set block-ref-pages)))
+                                (-> block
+                                    (dissoc :ref-pages)
+                                    (assoc :block/content (db/get-block-content utf8-content block)
+                                           :block/file [:file/path file]
+                                           :block/format format
+                                           :block/page [:page/name (string/lower-case page)]
+                                           :block/ref-pages (mapv
+                                                             (fn [page]
+                                                               (block/page-with-journal page))
+                                                             block-ref-pages)))))
+                            blocks)))
+                   (remove nil? pages)))
+          pages (doall
+                 (map
+                  (fn [page]
+                    (let [page-file? (= page (string/lower-case file))
+                          other-alias (and (:alias properties)
+                                           (seq (remove #(= page %)
+                                                        (:alias properties))))
+                          other-alias (distinct
+                                       (remove nil? other-alias))
+                          journal-date-long (if journal?
+                                              (date/journal-title->long (string/capitalize page)))
+                          page-list (when-let [list-content (:list properties)]
+                                      (extract-page-list list-content))]
+                      (cond->
+                       (util/remove-nils
+                        {:page/name (string/lower-case page)
+                         :page/original-name page
+                         :page/file [:file/path file]
+                         :page/journal? journal?
+                         :page/journal-day (if journal?
+                                             (date/journal-title->int (string/capitalize page))
+                                             0)
+                         :page/created-at journal-date-long
+                         :page/last-modified-at journal-date-long})
+                        (seq properties)
+                        (assoc :page/properties properties)
+
+                        other-alias
+                        (assoc :page/alias
+                               (map
+                                (fn [alias]
+                                  (let [alias (string/lower-case alias)
+                                        aliases (->>
+                                                 (distinct
+                                                  (conj
+                                                   (remove #{alias} other-alias)
+                                                   page))
+                                                 (remove nil?))
+                                        aliases (if (seq aliases)
+                                                  (map
+                                                   (fn [alias]
+                                                     {:page/name alias})
+                                                   aliases))]
+                                    (if (seq aliases)
+                                      {:page/name alias
+                                       :page/alias aliases}
+                                      {:page/name alias})))
+                                other-alias))
+
+                        (or (:tags properties) (:roam_tags properties))
+                        (assoc :page/tags (let [tags (:tags properties)
+                                                roam-tags (:roam_tags properties)
+                                                tags (if (string? tags)
+                                                       (string/split tags #",")
+                                                       tags)
+                                                tags (->> (concat tags roam-tags)
+                                                          (remove nil?)
+                                                          (distinct))
+                                                tags (util/->tags tags)]
+                                            (swap! ref-tags set/union (set (map :tag/name tags)))
+                                            tags)))))
+                  (->> (map first pages)
+                       (remove nil?))))
+          pages (concat
+                 pages
+                 (map
+                  (fn [page]
+                    {:page/original-name page
+                     :page/name page})
+                  @ref-tags)
+                 (map
+                  (fn [page]
+                    {:page/original-name page
+                     :page/name (string/lower-case page)})
+                  @ref-pages))
+          block-ids (mapv (fn [block]
+                            {:block/uuid (:block/uuid block)})
+                          (remove nil? blocks))]
+      [(remove nil? pages)
+       block-ids
+       (remove nil? blocks)])
+    (catch js/Error e
+      (js/console.log e))))
+
+(defn extract-blocks-pages
+  [repo-url file content utf8-content]
+  (if (string/blank? content)
+    []
+    (let [journal? (util/starts-with? file "journals/")
+          format (format/get-format file)
+          ast (mldoc/->edn content
+                           (mldoc/default-config format))
+          first-block (first ast)
+          properties (let [properties (and (seq first-block)
+                                           (= "Properties" (ffirst first-block))
+                                           (last (first first-block)))]
+                       (if (and properties (seq properties))
+                         properties))]
+      (extract-pages-and-blocks
+       repo-url
+       format ast properties
+       file content utf8-content journal?
+       (fn [blocks ast]
+         [[(db/get-page-name file ast) blocks]])))))
+
+(defn extract-all-blocks-pages
+  [repo-url files]
+  (when (seq files)
+    (let [result (->> files
+                      (map
+                       (fn [{:file/keys [path content]} contents]
+                         (println "Parsing : " path)
+                         (when content
+                           (let [utf8-content (utf8/encode content)]
+                             (extract-blocks-pages repo-url path content utf8-content)))))
+                      (remove empty?))]
+      (when (seq result)
+        (let [[pages block-ids blocks] (apply map concat result)
+              block-ids-set (set block-ids)
+              blocks (map (fn [b]
+                            (-> b
+                                (update :block/ref-blocks #(set/intersection (set %) block-ids-set))
+                                (update :block/embed-blocks #(set/intersection (set %) block-ids-set)))) blocks)]
+          (apply concat [pages block-ids blocks]))))))

+ 30 - 8
src/main/frontend/handler/file.cljs

@@ -8,6 +8,7 @@
             [frontend.git :as git]
             [frontend.handler.common :as common-handler]
             [frontend.handler.git :as git-handler]
+            [frontend.handler.extract :as extract-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.route :as route-handler]
             [cljs-bean.core :as bean]
@@ -18,7 +19,11 @@
             [frontend.handler.project :as project-handler]
             [lambdaisland.glogi :as log]
             [clojure.core.async :as async]
+            [cljs.core.async.interop :refer-macros [<p!]]
             [goog.object :as gobj]
+            [cljs-time.core :as t]
+            [cljs-time.coerce :as tc]
+            [frontend.utf8 :as utf8]
             ["ignore" :as Ignore]))
 
 (defn load-file
@@ -118,6 +123,27 @@
         (p/catch (fn [error]
                    (log/error :load-files-error error))))))
 
+(defn reset-file!
+  [repo-url file content]
+  (let [new? (nil? (db/entity [:file/path file]))]
+    (db/set-file-content! repo-url file content)
+    (let [format (format/get-format file)
+          utf8-content (utf8/encode content)
+          file-content [{:file/path file}]
+          tx (if (contains? config/mldoc-support-formats format)
+               (let [delete-blocks (db/delete-file-blocks! repo-url file)
+                     [pages block-ids blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)]
+                 (concat file-content delete-blocks pages block-ids blocks))
+               file-content)
+          tx (concat tx [(let [t (tc/to-long (t/now))]
+                           (cond->
+                            {:file/path file
+                             :file/last-modified-at t}
+                             new?
+                             (assoc :file/created-at t)))])]
+      (db/transact! repo-url tx))))
+
+;; TODO: better name to separate from reset-file!
 (defn alter-file
   [repo path content {:keys [reset? re-render-root? add-history? update-status?]
                       :or {reset? true
@@ -131,7 +157,7 @@
           (db/transact! repo
                         [[:db/retract page-id :page/alias]
                          [:db/retract page-id :page/tags]]))
-        (db/reset-file! repo path content))
+        (reset-file! repo path content))
       (db/set-file-content! repo path content))
     (util/p-handle
      (fs/write-file repo (util/get-repo-dir repo) path content {:old-content original-content
@@ -172,7 +198,7 @@
   (when update-db?
     (doseq [[path content] files]
       (if reset?
-        (db/reset-file! repo path content)
+        (reset-file! repo path content)
         (db/set-file-content! repo path content))))
 
   (when-let [chan (state/get-file-write-chan)]
@@ -267,11 +293,7 @@
   (let [chan (state/get-file-write-chan)]
     (async/go-loop []
       (let [args (async/<! chan)]
-        (when-let [repo (state/get-current-repo)]
-          (when-not (config/local-db? repo)
-            (state/set-file-writing! true))
-          (p/let [_ (apply alter-files-handler! args)]
-            (state/set-file-writing! false)))
-        nil)
+        ;; return a channel
+        (<p! (apply alter-files-handler! args)))
       (recur))
     chan))

+ 177 - 0
src/main/frontend/handler/graph.cljs

@@ -0,0 +1,177 @@
+(ns frontend.handler.graph
+  (:require [frontend.db :as db]
+            [clojure.string :as string]
+            [frontend.util :as util]
+            [frontend.date :as date]
+            [frontend.state :as state]
+            [clojure.set :as set]))
+
+(defn- build-edges
+  [edges]
+  (map (fn [[from to]]
+         {:source from
+          :target to})
+       edges))
+
+(defn- get-connections
+  [page edges]
+  (count (filter (fn [{:keys [source target]}]
+                   (or (= source page)
+                       (= target page)))
+                 edges)))
+
+(defn- build-nodes
+  [dark? current-page edges nodes]
+  (mapv (fn [p]
+          (let [current-page? (= p current-page)
+                color (case [dark? current-page?]
+                        [false false] "#222222"
+                        [false true]  "#045591"
+                        [true false]  "#8abbbb"
+                        [true true]   "#ffffff")] ; FIXME: Put it into CSS
+            {:id p
+             :name p
+             :val (get-connections p edges)
+             :autoColorBy "group"
+             :group (js/Math.ceil (* (js/Math.random) 12))
+             :color color}))
+        (set (flatten nodes))))
+
+(defn- normalize-page-name
+  [{:keys [nodes links] :as g}]
+  (let [all-pages (->> (set (apply concat
+                                   [(map :id nodes)
+                                    (map :source links)
+                                    (map :target links)]))
+                       (map string/lower-case))
+        names (db/pull-many '[:page/name :page/original-name] (mapv (fn [page] [:page/name page]) all-pages))
+        names (zipmap (map :page/name names)
+                      (map (fn [x] (get x :page/original-name (util/capitalize-all (:page/name x)))) names))
+        nodes (mapv (fn [node] (assoc node :id (get names (:id node)))) nodes)
+        links (mapv (fn [{:keys [source target]}]
+                      {:source (get names source)
+                       :target (get names target)})
+                    links)]
+    {:nodes nodes
+     :links links}))
+
+(defn build-global-graph
+  [theme show-journal?]
+  (let [dark? (= "dark" theme)
+        current-page (:page/name (db/get-current-page))]
+    (when-let [repo (state/get-current-repo)]
+      (let [relation (db/get-pages-relation repo show-journal?)
+            tagged-pages (db/get-all-tagged-pages repo)
+            linked-pages (-> (concat
+                              relation
+                              tagged-pages)
+                             flatten
+                             set)
+            all-pages (db/get-pages repo)
+            other-pages (->> (remove linked-pages all-pages)
+                             (remove nil?))
+            other-pages (if show-journal? other-pages
+                            (remove date/valid-journal-title? other-pages))
+            other-pages (if (seq other-pages)
+                          (map string/lower-case other-pages)
+                          other-pages)
+            nodes (concat (seq relation)
+                          (seq tagged-pages)
+                          (if (seq other-pages)
+                            (map (fn [page]
+                                   [page])
+                                 other-pages)
+                            []))
+            edges (build-edges (remove
+                                (fn [[_ to]]
+                                  (nil? to))
+                                nodes))
+            nodes (build-nodes dark? current-page edges nodes)]
+        (normalize-page-name
+         {:nodes nodes
+          :links edges})))))
+
+(defn build-page-graph
+  [page theme]
+  (let [dark? (= "dark" theme)]
+    (when-let [repo (state/get-current-repo)]
+      (let [page (string/lower-case page)
+            page-entity (db/entity [:page/name page])
+            original-page-name (:page/original-name page-entity)
+            tags (->> (:page/tags page-entity)
+                      (map :tag/name))
+            tags (remove #(= page %) tags)
+            ref-pages (db/get-page-referenced-pages repo page)
+            mentioned-pages (db/get-pages-that-mentioned-page repo page)
+            edges (concat
+                   (map (fn [[p aliases]]
+                          [page p]) ref-pages)
+                   (map (fn [[p aliases]]
+                          [p page]) mentioned-pages)
+                   (map (fn [tag]
+                          [page tag])
+                        tags))
+            other-pages (->> (concat (map first ref-pages)
+                                     (map first mentioned-pages))
+                             (remove nil?)
+                             (set))
+            other-pages-edges (mapcat
+                               (fn [page]
+                                 (let [ref-pages (-> (map first (db/get-page-referenced-pages repo page))
+                                                     (set)
+                                                     (set/intersection other-pages))
+                                       mentioned-pages (-> (map first (db/get-pages-that-mentioned-page repo page))
+                                                           (set)
+                                                           (set/intersection other-pages))]
+                                   (concat
+                                    (map (fn [p] [page p]) ref-pages)
+                                    (map (fn [p] [p page]) mentioned-pages))))
+                               other-pages)
+            edges (->> (concat edges other-pages-edges)
+                       (remove nil?)
+                       (distinct)
+                       (build-edges))
+            nodes (->> (concat
+                        [page]
+                        (map first ref-pages)
+                        (map first mentioned-pages)
+                        tags)
+                       (remove nil?)
+                       (distinct)
+                       (build-nodes dark? page edges))]
+        (normalize-page-name
+         {:nodes nodes
+          :links edges})))))
+
+(defn build-block-graph
+  "Builds a citation/reference graph for a given block uuid."
+  [block theme]
+  (let [dark? (= "dark" theme)]
+    (when-let [repo (state/get-current-repo)]
+      (let [ref-blocks (db/get-block-referenced-blocks block)
+            edges (concat
+                   (map (fn [[p aliases]]
+                          [block p]) ref-blocks))
+            other-blocks (->> (concat (map first ref-blocks))
+                              (remove nil?)
+                              (set))
+            other-blocks-edges (mapcat
+                                (fn [block]
+                                  (let [ref-blocks (-> (map first (db/get-block-referenced-blocks block))
+                                                       (set)
+                                                       (set/intersection other-blocks))]
+                                    (concat
+                                     (map (fn [p] [block p]) ref-blocks))))
+                                other-blocks)
+            edges (->> (concat edges other-blocks-edges)
+                       (remove nil?)
+                       (distinct)
+                       (build-edges))
+            nodes (->> (concat
+                        [block]
+                        (map first ref-blocks))
+                       (remove nil?)
+                       (distinct)
+                       (build-nodes dark? block edges))]
+        {:nodes nodes
+         :links edges}))))

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

@@ -65,7 +65,7 @@
                (let [content (util/default-content-with-title format title)]
                  (p/let [_ (fs/create-if-not-exists repo dir file-path content)
                          _ (git-handler/git-add repo path)]
-                   (db/reset-file! repo path content)
+                   (file-handler/reset-file! repo path content)
                    (when redirect?
                      (route-handler/redirect! {:to :page
                                                :path-params {:name page}})
@@ -460,3 +460,10 @@
   [page-name]
   (page-add-properties! page-name {:published false})
   (notification/show! (util/format "Remove Page \"%s\" from Logseq server success" page-name) :success))
+
+(defn add-page-to-recent!
+  [repo page]
+  (let [pages (or (db/get-key-value repo :recent/pages)
+                  '())
+        new-pages (take 12 (distinct (cons page pages)))]
+    (db/set-key-value repo :recent/pages new-pages)))

+ 22 - 7
src/main/frontend/handler/repo.cljs

@@ -12,12 +12,14 @@
             [frontend.date :as date]
             [frontend.config :as config]
             [frontend.format :as format]
+            [frontend.search :as search]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.git :as git-handler]
             [frontend.handler.file :as file-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
             [frontend.handler.common :as common-handler]
+            [frontend.handler.extract :as extract-handler]
             [frontend.ui :as ui]
             [cljs.reader :as reader]
             [clojure.string :as string]
@@ -58,7 +60,7 @@
                 old-content (when file-exists?
                               (db/get-file repo-url path))
                 content (or old-content default-content)]
-            (db/reset-file! repo-url path content)
+            (file-handler/reset-file! repo-url path content)
             (db/reset-config! repo-url content)
             (when-not (= content old-content)
               (git-handler/git-add repo-url path))))))))
@@ -76,7 +78,7 @@
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (state/get-pages-directory)))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (db/reset-file! repo-url path default-content)
+        (file-handler/reset-file! repo-url path default-content)
         (git-handler/git-add repo-url path)))))
 
 (defn create-custom-theme
@@ -89,7 +91,7 @@
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (db/reset-file! repo-url path default-content)
+        (file-handler/reset-file! repo-url path default-content)
         (git-handler/git-add repo-url path)))))
 
 (defn create-dummy-notes-page
@@ -100,7 +102,7 @@
         file-path (str "/" path)]
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
             _file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
-      (db/reset-file! repo-url path content))))
+      (file-handler/reset-file! repo-url path content))))
 
 (defn create-today-journal-if-not-exists
   ([repo-url]
@@ -136,7 +138,7 @@
                _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
                file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
          (when-not file-exists?
-           (db/reset-file! repo-url path content)
+           (file-handler/reset-file! repo-url path content)
            (ui-handler/re-render-root!)
            (git-handler/git-add repo-url path)))))))
 
@@ -160,6 +162,14 @@
   (create-contents-file repo-url)
   (create-custom-theme repo-url))
 
+(defn- reset-contents-and-blocks!
+  [repo-url files blocks-pages delete-files delete-blocks]
+  (db/transact-files-db! repo-url files)
+  (let [files (map #(select-keys % [:file/path]) files)
+        all-data (-> (concat delete-files delete-blocks files blocks-pages)
+                     (util/remove-nils))]
+    (db/transact! repo-url all-data)))
+
 (defn parse-files-and-load-to-db!
   [repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts
                    :or {re-render? true}}]
@@ -172,9 +182,9 @@
                           (contains? config/mldoc-support-formats format)))
                       files)
         blocks-pages (if (seq parsed-files)
-                       (db/extract-all-blocks-pages repo-url parsed-files)
+                       (extract-handler/extract-all-blocks-pages repo-url parsed-files)
                        [])]
-    (db/reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
+    (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
     (let [config-file (str config/app-name "/" config/config-file)]
       (if (contains? (set file-paths) config-file)
         (when-let [content (some #(when (= (:file/path %) config-file)
@@ -564,6 +574,7 @@
 (defn rebuild-index!
   [url]
   (when url
+    (search/reset-indice! url)
     (db/remove-conn! url)
     (db/clear-query-state!)
     (-> (p/do! (db/remove-db! url)
@@ -578,3 +589,7 @@
   (when-let [repo (state/get-current-repo)]
     (push repo {:commit-message commit-message
                 :custom-commit? true})))
+
+(defn get-repo-name
+  [url]
+  (last (string/split url #"/")))

+ 48 - 0
src/main/frontend/namespaces.cljc

@@ -0,0 +1,48 @@
+(ns frontend.namespaces)
+
+;; copy from https://github.com/clj-commons/potemkin/issues/31#issuecomment-110689951
+(defmacro import-def
+  "import a single fn or var
+   (import-def a b) => (def b a/b)
+  "
+  [from-ns def-name]
+  (let [from-sym# (symbol (str from-ns) (str def-name))]
+    `(def ~def-name ~from-sym#)))
+
+(defmacro import-vars
+  "import multiple defs from multiple namespaces
+   works for vars and fns. not macros.
+   (same syntax as potemkin.namespaces/import-vars)
+   (import-vars
+     [m.n.ns1 a b]
+     [x.y.ns2 d e f]) =>
+   (def a m.n.ns1/a)
+   (def b m.n.ns1/b)
+    ...
+   (def d m.n.ns2/d)
+    ... etc
+  "
+  [& imports]
+  (let [expanded-imports (for [[from-ns & defs] imports
+                               d defs]
+                           `(import-def ~from-ns ~d))]
+    `(do ~@expanded-imports)))
+
+;; FIXME:
+#_(defmacro import-ns
+  "import all the public defs from multiple namespaces
+   works for vars and fns. not macros.
+  (import-ns
+     m.n.ns1
+     x.y.ns2) =>
+   (def a m.n.ns1/a)
+   (def b m.n.ns1/b)
+    ...
+   (def d m.n.ns2/d)
+    ... etc
+  "
+  [& namespaces]
+  (let [expanded-imports (for [from-ns namespaces
+                               d ((ns-resolve 'cljs.analyzer.api 'ns-publics) from-ns)]
+                           `(import-def ~from-ns ~d))]
+    `(do ~@expanded-imports)))

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

@@ -63,6 +63,11 @@
        (swap! indices assoc repo result)
        result))))
 
+(defn reset-indice!
+  [repo]
+  (swap! indices assoc repo {:pages #js []
+                             :blocks #js []}))
+
 ;; Copied from https://gist.github.com/vaughnd/5099299
 (defn str-len-distance
   ;; normalized multiplier 0-1

+ 5 - 9
src/main/frontend/state.cljs

@@ -18,7 +18,6 @@
     :today nil
     :db/batch-txs (async/chan 100)
     :file/writes (async/chan 100)
-    :file/writing? false
     :notification/show? false
     :notification/content nil
     :repo/cloning? false
@@ -833,6 +832,11 @@
   []
   (:file/writes @state))
 
+(defn get-write-chan-length
+  []
+  (let [c (get-file-write-chan)]
+    (count (gobj/get c "buf"))))
+
 (defn add-tx!
   ;; TODO: replace f with data for batch transactions
   [f]
@@ -991,14 +995,6 @@
       (storage/set-transit! :db/latest-txs new-txs)
       (set-state! :db/latest-txs new-txs))))
 
-(defn set-file-writing!
-  [v]
-  (set-state! :file/writing? v))
-
-(defn file-in-writing!
-  []
-  (:file/writing? @state))
-
 (defn get-repo-latest-txs
   [repo file?]
   (get-in (:db/latest-txs @state) [repo file?]))

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.0.4.8-5")
+(defonce version "0.0.4.8-6")

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно