diff_merge.cljs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. (ns frontend.fs.diff-merge
  2. "Implementation of text (file) based content diff & merge for conflict resolution"
  3. (:require ["@logseq/diff-merge" :refer [attach_uuids Differ Merger]]
  4. [cljs-bean.core :as bean]
  5. [frontend.db.model :as db-model]
  6. [frontend.db.utils :as db-utils]
  7. [logseq.graph-parser.block :as gp-block]
  8. [logseq.graph-parser.mldoc :as gp-mldoc]
  9. [logseq.graph-parser.property :as gp-property]
  10. [logseq.graph-parser.utf8 :as utf8]
  11. [clojure.string :as string]))
  12. (defn diff
  13. "2-ways diff
  14. Accept: blocks in the struct with the required info
  15. Please refer to the `Block` struct in the link below
  16. https://github.com/logseq/diff-merge/blob/master/lib/mldoc.ts"
  17. [base incoming]
  18. (let [differ (Differ.)]
  19. (.diff_logseqMode differ (bean/->js base) (bean/->js incoming))))
  20. ;; (defonce getHTML visualizeAsHTML)
  21. (defonce attachUUID attach_uuids)
  22. (defn db->diff-blocks
  23. "db: datascript db
  24. page-name: string"
  25. [page-name]
  26. {:pre (string? page-name)}
  27. (let [walked (db-model/get-sorted-page-block-ids-and-levels page-name)
  28. blocks (db-utils/pull-many [:block/uuid :block/content :block/level] (map :id walked))
  29. levels (map :level walked)
  30. blocks (map (fn [block level]
  31. {:uuid (str (:block/uuid block)) ;; Force to be string
  32. :body (:block/content block)
  33. :level level})
  34. blocks levels)]
  35. blocks))
  36. ;; TODO: Switch to ast->diff-blocks-alt
  37. ;; Diverged from gp-block/extract-blocks for decoupling
  38. ;; The process of doing 2 way diff is like:
  39. ;; 1. Given a base ver. of page (AST in DB), and a branch ver. of page (externally modified file content)
  40. ;; 2. Transform both base ver (done by THIS fn). & branch ver. into the same format (diff-blocks)
  41. ;; 3. Apply diff-merge/diff on them, which returns the resolved uuids of the branch ver
  42. ;; 4. Attach these resolved uuids into the blocks newly parsed by graph-parser
  43. ;; Keep all the diff-merge fns, including diff-merge/ast->diff-blocks out of the graph-parser,
  44. ;; Only inject the step 4 into graph-parser as a hook
  45. (defn ast->diff-blocks
  46. "Prepare the blocks for diff-merge
  47. blocks: ast of blocks
  48. content: corresponding raw content"
  49. [blocks content format {:keys [user-config block-pattern]}]
  50. {:pre [(string? content) (contains? #{:markdown :org} format)]}
  51. (let [encoded-content (utf8/encode content)]
  52. (loop [headings []
  53. blocks (reverse blocks)
  54. properties {}
  55. end-pos (.-length encoded-content)]
  56. (if (seq blocks)
  57. (let [[block pos-meta] (first blocks)
  58. ;; fix start_pos
  59. pos-meta (assoc pos-meta :end_pos end-pos)]
  60. (cond
  61. (gp-block/heading-block? block)
  62. (let [content (gp-block/get-block-content encoded-content (second block) format pos-meta block-pattern)]
  63. (recur (conj headings {:body content
  64. :level (:level (second block))
  65. :uuid (:id properties)})
  66. (rest blocks) {} (:start_pos pos-meta))) ;; The current block's start pos is the next block's end pos
  67. (gp-property/properties-ast? block)
  68. (let [new-props (:properties (gp-block/extract-properties (second block) (assoc user-config :format format)))]
  69. ;; sending the current end pos to next, as it's not finished yet
  70. ;; supports multiple properties sub-block possible in future
  71. (recur headings (rest blocks) (merge properties new-props) (:end_pos pos-meta)))
  72. :else
  73. (recur headings (rest blocks) properties (:end_pos pos-meta))))
  74. (if (empty? properties)
  75. (reverse headings)
  76. ;; Add pre-blocks
  77. (let [[block _] (first blocks)
  78. pos-meta {:start_pos 0 :end_pos end-pos}
  79. content (gp-block/get-block-content encoded-content block format pos-meta block-pattern)
  80. uuid (:id properties)]
  81. (cons {:body content
  82. :level 1
  83. :uuid uuid}
  84. (reverse headings))))))))
  85. (defn- get-sub-content-from-pos-meta
  86. "Replace gp-block/get-block-content, return bare content, without any trim"
  87. [raw-content pos-meta]
  88. (let [{:keys [start_pos end_pos]} pos-meta]
  89. (utf8/substring raw-content start_pos end_pos)))
  90. ;; Diverged from ast->diff-blocks
  91. ;; Add :meta :raw-body to the block
  92. (defn- ast->diff-blocks-alt
  93. "Prepare the blocks for diff-merge
  94. blocks: ast of blocks
  95. content: corresponding raw content"
  96. [blocks content format {:keys [user-config block-pattern]}]
  97. {:pre [(string? content) (contains? #{:markdown :org} format)]}
  98. (let [utf8-encoded-content (utf8/encode content)]
  99. (loop [headings []
  100. blocks (reverse blocks)
  101. properties {}
  102. end-pos (.-length utf8-encoded-content)]
  103. (cond
  104. (seq blocks)
  105. (let [[block pos-meta] (first blocks)
  106. ;; fix start_pos for properties
  107. fixed-pos-meta (assoc pos-meta :end_pos end-pos)]
  108. (cond
  109. (gp-block/heading-block? block)
  110. (let [content (gp-block/get-block-content utf8-encoded-content (second block) format fixed-pos-meta block-pattern)
  111. content-raw (get-sub-content-from-pos-meta utf8-encoded-content fixed-pos-meta)]
  112. (recur (conj headings {:body content
  113. :meta {:raw-body (string/trimr content-raw)}
  114. :level (:level (second block))
  115. :uuid (:id properties)})
  116. (rest blocks)
  117. {}
  118. (:start_pos fixed-pos-meta))) ;; The current block's start pos is the next block's end pos
  119. (gp-property/properties-ast? block)
  120. (let [new-props (:properties (gp-block/extract-properties (second block) (assoc user-config :format format)))]
  121. ;; sending the current end pos to next, as it's not finished yet
  122. ;; supports multiple properties sub-block possible in future
  123. (recur headings (rest blocks) (merge properties new-props) (:end_pos fixed-pos-meta)))
  124. :else
  125. (recur headings (rest blocks) properties (:end_pos fixed-pos-meta))))
  126. (empty? properties)
  127. (reverse headings)
  128. ;; Add pre-blocks
  129. :else ;; ??? unreachable
  130. (let [[block _] (first blocks)
  131. pos-meta {:start_pos 0 :end_pos end-pos}
  132. content (gp-block/get-block-content utf8-encoded-content block format pos-meta block-pattern)
  133. content-raw (get-sub-content-from-pos-meta utf8-encoded-content pos-meta)
  134. uuid (:id properties)]
  135. (cons {:body content
  136. :meta {:raw-body (string/trimr content-raw)}
  137. :level 1
  138. :uuid uuid}
  139. (reverse headings)))))))
  140. (defn- rebuild-content
  141. "translate [[[op block]]] to merged content"
  142. [_base-diffblocks diffs _format]
  143. ;; [[[0 {:body "attrib:: xxx", :level 1, :uuid nil}] ...] ...]
  144. (let [ops-fn (fn [ops]
  145. (map (fn [[op {:keys [meta]}]]
  146. (when (or (= op 0) (= op 1)) ;; equal or insert
  147. (:raw-body meta)))
  148. ops))]
  149. (->> diffs
  150. (mapcat ops-fn)
  151. (filter seq)
  152. (string/join "\n"))))
  153. (defn three-way-merge
  154. [base income current format]
  155. (let [->ast (fn [text] (if (= format :org)
  156. (gp-mldoc/->edn text (gp-mldoc/default-config :org))
  157. (gp-mldoc/->edn text (gp-mldoc/default-config :markdown))))
  158. options (if (= format :org)
  159. {:block-pattern "*"}
  160. {:block-pattern "-"})
  161. merger (Merger.)
  162. base-ast (->ast base)
  163. base-diffblocks (ast->diff-blocks-alt base-ast base format options)
  164. income-ast (->ast income)
  165. income-diffblocks (ast->diff-blocks-alt income-ast income format options)
  166. current-ast (->ast current)
  167. current-diffblocks (ast->diff-blocks-alt current-ast current format options)
  168. branch-diffblocks [current-diffblocks income-diffblocks]
  169. merged (.mergeBlocks merger (bean/->js base-diffblocks) (bean/->js branch-diffblocks))
  170. ;; For extracting diff-merge test cases
  171. ;; _ (prn "input:")
  172. ;; _ (prn (js/JSON.stringify (bean/->js base-diffblocks)))
  173. ;; _ (prn (js/JSON.stringify (bean/->js branch-diffblocks)))
  174. ;; _ (prn "logseq diff merge version: " version)
  175. ;; _ (prn "output:")
  176. ;; _ (prn (js/JSON.stringify merged))
  177. merged-diff (bean/->clj merged)
  178. merged-content (rebuild-content base-diffblocks merged-diff format)]
  179. merged-content))