diff --git a/dev/tasks/docs.clj b/dev/tasks/docs.clj index 63e111de..9b91f9f9 100644 --- a/dev/tasks/docs.clj +++ b/dev/tasks/docs.clj @@ -94,6 +94,9 @@ ::v/diff {:file "portal/ui/viewer/diff.cljs" :examples [(vary-meta d/diff-data dissoc ::v/default)]} + ::v/diff-text + {:file "portal/ui/viewer/diff_text.cljs" + :examples [d/diff-text-data]} ::v/tree {:examples [(vary-meta d/hiccup dissoc ::v/default)]} ::v/code diff --git a/package-lock.json b/package-lock.json index a0697f54..b0a06075 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "anser": "^2.1.1", + "diff": "^5.2.0", "highlight.js": "^11.3.1", "marked": "^4.1.0", "papaparse": "^5.3.1", @@ -922,6 +923,15 @@ "node": ">=0.8.0" } }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", diff --git a/package.json b/package.json index 07fc698e..5da03cdd 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "anser": "^2.1.1", + "diff": "^5.2.0", "highlight.js": "^11.3.1", "marked": "^4.1.0", "papaparse": "^5.3.1", diff --git a/resources/viewers.edn b/resources/viewers.edn index 25f77250..b2f90a43 100644 --- a/resources/viewers.edn +++ b/resources/viewers.edn @@ -57,6 +57,7 @@ {:name :portal.viewer/diff, :doc "Diff a collection of values successively starting with the first two."} + {:name :portal.viewer/diff-text, :doc "Diff two strings."} {:name :portal.viewer/markdown, :doc "Parse string as markdown and view as html."} {:name :portal.viewer/hiccup, diff --git a/src/examples/data.cljc b/src/examples/data.cljc index 721cde13..7568aec5 100644 --- a/src/examples/data.cljc +++ b/src/examples/data.cljc @@ -2,10 +2,11 @@ (:require #?(:clj [clojure.java.io :as io]) #?(:org.babashka/nbb [clojure.core] :default [examples.hacker-news :as hn]) + [clojure.pprint :as pp] [examples.macros :refer [read-file]] [portal.colors :as c] [portal.viewer :as v]) - #?(:clj (:import [java.io File ByteArrayOutputStream] + #?(:clj (:import [java.io ByteArrayOutputStream File] [java.net URI] [java.util Date] [java.util UUID]) @@ -242,6 +243,11 @@ ::vector [::a ::added ::b] ::different-value ::new-key}])) +(def diff-text-data + (v/diff-text + [(with-out-str (pp/pprint (first diff-data))) + (with-out-str (pp/pprint (second diff-data)))])) + (def string-data (v/for {::json "{\"hello\": 123}" @@ -1121,6 +1127,7 @@ ::spec-data spec-data ::table-data table-data ::diff diff-data + ::diff-text diff-text-data ::basic-data basic-data ::themes c/themes ::clojure-data clojure-data diff --git a/src/portal/ui/app.cljs b/src/portal/ui/app.cljs index aa6b336d..64509f66 100644 --- a/src/portal/ui/app.cljs +++ b/src/portal/ui/app.cljs @@ -25,6 +25,7 @@ [portal.ui.viewer.date-time :as date-time] [portal.ui.viewer.deref :as deref] [portal.ui.viewer.diff :as diff] + [portal.ui.viewer.diff-text :as diff-text] [portal.ui.viewer.duration :as duration] [portal.ui.viewer.edn :as edn] [portal.ui.viewer.exception :as ex] @@ -489,6 +490,7 @@ csv/viewer html/viewer diff/viewer + diff-text/viewer md/viewer hiccup/viewer date-time/viewer diff --git a/src/portal/ui/commands.cljs b/src/portal/ui/commands.cljs index 6c31b925..d370fa67 100644 --- a/src/portal/ui/commands.cljs +++ b/src/portal/ui/commands.cljs @@ -924,34 +924,24 @@ (register! #'nav {:name 'clojure.datafy/nav :predicate (comp :collection state/get-selected-context)}) -(defn- get-style [] - (some-> js/document - (.getElementsByTagName "html") - (aget 0) - (.getAttribute "style") - not-empty)) - -(defn- get-vs-code-css-vars [] - (when-let [style (get-style)] - (persistent! - (reduce - (fn [vars rule] - (if-let [[attr value] (str/split rule #"\s*:\s*")] - (assoc! vars attr value) - vars)) - (transient {}) - (str/split style #"\s*;\s*"))))) - (defn- vs-code-vars "List all available css variable provided by vs-code." [state] (state/dispatch! state state/history-push - {:portal/value (get-vs-code-css-vars)})) + {:portal/value (theme/get-vs-code-css-vars)})) (register! #'vs-code-vars {:predicate theme/is-vs-code?}) +(defn- vs-code-copy-theme + [_] + (-> (theme/get-vs-code-css-vars) + (walk/postwalk-replace (::c/vs-code-embedded c/themes)) + (copy-edn!))) + +(register! #'vs-code-copy-theme {:predicate theme/is-vs-code?}) + (defn copy-str "Copy string to the clipboard." {:shortcuts [#{"shift" "c"}]} diff --git a/src/portal/ui/theme.cljs b/src/portal/ui/theme.cljs index e1688d14..c7df96dd 100644 --- a/src/portal/ui/theme.cljs +++ b/src/portal/ui/theme.cljs @@ -1,5 +1,6 @@ (ns portal.ui.theme (:require ["react" :as react] + [clojure.string :as str] [portal.colors :as c] [portal.ui.options :as opts] [portal.ui.react :refer [use-effect]])) @@ -11,8 +12,27 @@ (.getPropertyValue "--vscode-font-size") (not= ""))) +(defn- get-style [] + (some-> js/document + (.getElementsByTagName "html") + (aget 0) + (.getAttribute "style") + not-empty)) + +(defn ^:no-doc get-vs-code-css-vars [] + (when-let [style (get-style)] + (persistent! + (reduce + (fn [vars rule] + (if-let [[attr value] (str/split rule #"\s*:\s*")] + (assoc! vars (str "var(" attr ")") value) + vars)) + (transient {}) + (str/split style #"\s*;\s*"))))) + (defn- get-theme [theme-name] - (let [opts (opts/use-options)] + (let [opts (opts/use-options) + vars (get-vs-code-css-vars)] (merge {:font-family "monospace" :font-size "12pt" @@ -20,8 +40,10 @@ :max-depth 2 :padding 6 :border-radius 2} - (or (get c/themes theme-name) - (get (:themes opts) theme-name))))) + (update-vals + (or (get c/themes theme-name) + (get (:themes opts) theme-name)) + #(get vars % %))))) (defn- use-theme-detector [] (let [media-query (.matchMedia js/window "(prefers-color-scheme: dark)") @@ -34,6 +56,20 @@ (.removeListener media-query listener)))) dark-theme)) +(defn- use-vscode-theme-detector [] + (let [[change-id set-change-id!] (react/useState 0)] + (when (is-vs-code?) + (react/useEffect + (fn [] + (let [observer (js/MutationObserver. #(set-change-id! inc))] + (.observe observer + js/document.documentElement + #js {:attributes true + :attributeFilter #js ["style"]}) + #(.disconnect observer))) + #js [])) + change-id)) + (defn- default-theme [dark-theme] (cond (is-vs-code?) ::c/vs-code-embedded @@ -57,8 +93,9 @@ (defn with-theme [theme-name & children] (let [dark-theme (use-theme-detector) + theme-key (use-vscode-theme-detector) theme (get-theme (or theme-name (default-theme dark-theme)))] - (into [:r> (.-Provider theme-context) #js {:value theme}] children))) + (into [:r> (.-Provider theme-context) #js {:key theme-key :value theme}] children))) (defonce ^:no-doc order (cycle [::c/diff-remove ::c/diff-add ::c/keyword ::c/tag ::c/number ::c/uri])) diff --git a/src/portal/ui/viewer/diff_text.cljs b/src/portal/ui/viewer/diff_text.cljs new file mode 100644 index 00000000..48ce42fb --- /dev/null +++ b/src/portal/ui/viewer/diff_text.cljs @@ -0,0 +1,121 @@ +(ns portal.ui.viewer.diff-text + (:require ["diff" :as df] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [portal.colors :as c] + [portal.ui.icons :as icons] + [portal.ui.inspector :as ins] + [portal.ui.lazy :as l] + [portal.ui.styled :as d] + [portal.ui.theme :as theme])) + +;;; :spec +(s/def ::diff-text (s/cat :a string? :b string?)) +;;; + +(defn- diff-text? [value] + (s/valid? ::diff-text value)) + +(defn- changed? [^js item] + (or (some-> item .-added) + (some-> item .-removed))) + +(defn- inspect-text-diff [value] + (let [theme (theme/use-theme) + add (::c/diff-add theme) + remove (::c/diff-remove theme) + diff (df/diffLines (or (:- value) (first value)) + (or (:+ value) (second value))) + opts (ins/use-options) + bg (ins/get-background)] + [:pre + {:style {:margin 0 :white-space :pre-wrap :background bg}} + [d/div + {:style {:height (:padding theme) + :padding-left (:padding theme) + :border-top [1 :solid (::c/border theme)] + :border-left [5 :solid (::c/border theme)] + :border-right [1 :solid (::c/border theme)] + :border-top-left-radius (:border-radius theme) + :border-top-right-radius (:border-radius theme)}}] + [l/lazy-seq + (map-indexed + (fn [index [before ^js item after]] + (let [added (.-added item) + removed (.-removed item) + text (.-value item) + border-color (cond + added add + removed remove + :else (::c/border theme))] + ^{:key index} + [d/div + {:style + {:position :relative + :border-left [5 :solid border-color] + :border-right [1 :solid border-color] + :background (cond added (str add "22") + removed (str remove "22"))}} + (if-not (or removed added) + (if (:expanded? opts) + [ins/highlight-words text] + (let [lines (str/split-lines text)] + (if (< (count lines) 6) + [ins/highlight-words text] + [:<> + (when before + [:<> + [ins/highlight-words (str/join "\n" (take 3 lines))] + [d/div {:style {:background (::c/border theme) + :text-align :center}} + [icons/ellipsis-h]]]) + (when after + [:<> + [d/div {:style {:background (::c/border theme) + :text-align :center}} + [icons/ellipsis-h]] + [ins/highlight-words (str/join "\n" (take-last 3 lines))]])]))) + (cond + (changed? before) + (keep-indexed + (fn [idx ^js item] + (when (or (.-added item) (not (.-removed item))) + ^{:key idx} + [d/span {:style {:background (when (.-added item) + (if added + (str add "66") + (str remove "66")))}} + [ins/highlight-words (.-value item)]])) + (df/diffChars (.-value before) (str/trimr text))) + + (changed? after) + (keep-indexed + (fn [idx ^js item] + (when (or (.-added item) (not (.-removed item))) + ^{:key idx} + [d/span {:style {:background (when (.-added item) + (if added + (str add "66") + (str remove "66")))}} + [ins/highlight-words (.-value item)]])) + (df/diffChars (.-value after) (str/trimr text))) + + :else + (str/trimr text))) + + (when (or removed added) "\n")])) + (partition 3 1 (concat [nil] diff [nil])))] + [d/div + {:style {:height (:padding theme) + :padding-left (:padding theme) + :border-bottom [1 :solid (::c/border theme)] + :border-left [5 :solid (::c/border theme)] + :border-right [1 :solid (::c/border theme)] + :border-bottom-left-radius (:border-radius theme) + :border-bottom-right-radius (:border-radius theme)}}]])) + +(def viewer + {:predicate diff-text? + :component inspect-text-diff + :name :portal.viewer/diff-text + :doc "Diff two strings."}) \ No newline at end of file diff --git a/src/portal/viewer.cljc b/src/portal/viewer.cljc index b1eb9d63..55cca2ef 100644 --- a/src/portal/viewer.cljc +++ b/src/portal/viewer.cljc @@ -117,6 +117,11 @@ ([value] (default value ::diff)) ([value opts] (default value ::diff opts))) +(defn diff-text + "Diff two strings." + ([value] (default value ::diff-text)) + ([value opts] (default value ::diff-text opts))) + (defn prepl "View interlacing of stdout, stderr and tap values. Useful for build output." ([value] (default value ::prepl))